Add TLS support to resolver, implement DBus ifaces
This commit is contained in:
parent
b5797b12f1
commit
c52195dd8b
|
@ -1,6 +1,9 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
# SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
|
||||||
/build
|
/build
|
||||||
/target
|
/target
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
|
|
@ -22,6 +22,7 @@ name = "nss_malcontent"
|
||||||
bindgen = "0.60"
|
bindgen = "0.60"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
event-listener = "2.5"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
rusty-hook = "0.11"
|
rusty-hook = "0.11"
|
||||||
rusty-forkfork = "0.4"
|
rusty-forkfork = "0.4"
|
||||||
|
@ -30,12 +31,14 @@ tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
const_format = "0.2"
|
gethostname = "0.2"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
once_cell = "1.13"
|
once_cell = "1.13"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
nix = { version = "0.24", features = ["socket", "user", "sched"] }
|
nix = { version = "0.24", features = ["socket", "user", "sched"] }
|
||||||
|
serde = "1.0"
|
||||||
tokio = { version = "1", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
trust-dns-resolver = "0.21"
|
trust-dns-resolver = { version = "0.21", features = ["dns-over-rustls"] }
|
||||||
trust-dns-proto = "0.21"
|
trust-dns-proto = "0.21"
|
||||||
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
|
||||||
|
zvariant = "3.6"
|
|
@ -113,6 +113,7 @@ exceptions = [
|
||||||
# list
|
# list
|
||||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||||
{ allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*" },
|
{ allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*" },
|
||||||
|
{ allow = ["OpenSSL"], name = "ring", version = "*" },
|
||||||
]
|
]
|
||||||
|
|
||||||
# Some crates don't have (easily) machine readable licensing information,
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
|
@ -135,6 +136,13 @@ exceptions = [
|
||||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||||
#]
|
#]
|
||||||
|
|
||||||
|
[[licenses.clarify]]
|
||||||
|
name = "ring"
|
||||||
|
expression = "MIT AND ISC AND OpenSSL"
|
||||||
|
license-files = [
|
||||||
|
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||||
|
]
|
||||||
|
|
||||||
[licenses.private]
|
[licenses.private]
|
||||||
# If true, ignores workspace crates that aren't published, or are only
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
# published to private registries.
|
# published to private registries.
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
use const_format::concatcp;
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const DBUS_INTERFACE: &str = "com.endlessm.ParentalControls.Dns";
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const DBUS_OBJECT_PATH: &str = "com/endlessm/ParentalControls/Dns";
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const DBUS_GET_RESTRICTIONS_METHOD: &str = concatcp!(DBUS_INTERFACE, ".", "GetRestrictions");
|
|
|
@ -4,7 +4,7 @@
|
||||||
use {
|
use {
|
||||||
crate::helpers::{set_if_valid, write_record_name_to_buf, write_vector_to_buf},
|
crate::helpers::{set_if_valid, write_record_name_to_buf, write_vector_to_buf},
|
||||||
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno},
|
||||||
crate::policy_checker::{PolicyChecker as _, POLICY_CHECKER},
|
crate::policy_checker::POLICY_CHECKER,
|
||||||
libc::{c_char, c_int, hostent, size_t, AF_INET, AF_INET6},
|
libc::{c_char, c_int, hostent, size_t, AF_INET, AF_INET6},
|
||||||
nix::errno,
|
nix::errno,
|
||||||
nix::errno::Errno,
|
nix::errno::Errno,
|
||||||
|
@ -46,7 +46,7 @@ pub async unsafe fn with(args: &mut Args) -> nss_status {
|
||||||
set_if_valid(args.errnop, errno::from_i32(0));
|
set_if_valid(args.errnop, errno::from_i32(0));
|
||||||
set_if_valid(args.h_errnop, HErrno::Success);
|
set_if_valid(args.h_errnop, HErrno::Success);
|
||||||
|
|
||||||
match POLICY_CHECKER.resolver(None) {
|
match POLICY_CHECKER.resolver(None).await {
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// no restrictions for user, the next NSS module will decide
|
// no restrictions for user, the next NSS module will decide
|
||||||
nss_status::NSS_STATUS_NOTFOUND
|
nss_status::NSS_STATUS_NOTFOUND
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
#![allow(non_camel_case_types)]
|
#![allow(non_camel_case_types)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
mod constants;
|
|
||||||
mod gethostbyaddr;
|
mod gethostbyaddr;
|
||||||
mod gethostbyname;
|
mod gethostbyname;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
use {
|
|
||||||
anyhow::Result,
|
|
||||||
nix::unistd::{getuid, Uid},
|
|
||||||
once_cell::sync::Lazy,
|
|
||||||
std::collections::HashMap,
|
|
||||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
|
||||||
std::sync::{Arc, RwLock},
|
|
||||||
trust_dns_resolver::config as dns_config,
|
|
||||||
trust_dns_resolver::TokioAsyncResolver,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type Restrictions<'a> = &'a [IpAddr];
|
|
||||||
|
|
||||||
pub trait PolicyChecker {
|
|
||||||
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static POLICY_CHECKER: Lazy<MalcontentPolicyChecker> =
|
|
||||||
Lazy::new(|| MalcontentPolicyChecker::new());
|
|
||||||
|
|
||||||
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
|
||||||
vec![
|
|
||||||
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
|
||||||
IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)),
|
|
||||||
IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1113)),
|
|
||||||
IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1003)),
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: accept notifications about config changes
|
|
||||||
pub struct MalcontentPolicyChecker {
|
|
||||||
resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MalcontentPolicyChecker {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
resolvers: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restrictions<'a>(&'a self, user: Uid) -> Result<Option<Restrictions<'a>>> {
|
|
||||||
if user.is_root() {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: for now, hardcoded DNS records, later go
|
|
||||||
// through D-Bus to malcontent and ask about user.
|
|
||||||
|
|
||||||
Ok(Some(CLOUDFLARE_PARENTALCONTROL_ADDRS.as_slice()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PolicyChecker for MalcontentPolicyChecker {
|
|
||||||
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> {
|
|
||||||
let user = user.unwrap_or_else(|| getuid());
|
|
||||||
{
|
|
||||||
let ro_resolvers = self.resolvers.read().unwrap();
|
|
||||||
if let Some(resolver) = ro_resolvers.get(&user) {
|
|
||||||
return Ok(resolver.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut rw_resolvers = self.resolvers.write().unwrap();
|
|
||||||
if rw_resolvers.contains_key(&user) {
|
|
||||||
return self.resolver(Some(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolver = match self.restrictions(user)? {
|
|
||||||
Some(addrs) => {
|
|
||||||
let resolver_config =
|
|
||||||
addrs
|
|
||||||
.iter()
|
|
||||||
.fold(dns_config::ResolverConfig::new(), |mut config, addr| {
|
|
||||||
config.add_name_server(dns_config::NameServerConfig {
|
|
||||||
socket_addr: SocketAddr::new(*addr, 53),
|
|
||||||
protocol: dns_config::Protocol::Udp,
|
|
||||||
tls_dns_name: None,
|
|
||||||
trust_nx_responses: true,
|
|
||||||
bind_addr: None,
|
|
||||||
});
|
|
||||||
config
|
|
||||||
});
|
|
||||||
|
|
||||||
let resolver = TokioAsyncResolver::tokio(
|
|
||||||
resolver_config,
|
|
||||||
dns_config::ResolverOpts::default(),
|
|
||||||
)?;
|
|
||||||
Some(Arc::new(resolver))
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
rw_resolvers.insert(user, resolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.resolver(Some(user))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use {
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::net::IpAddr,
|
||||||
|
zbus::{dbus_proxy, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[dbus_proxy(
|
||||||
|
default_service = "com.endlessm.ParentalControls",
|
||||||
|
interface = "com.endlessm.ParentalControls.Dns",
|
||||||
|
default_path = "/com/endlessm/ParentalControls/Dns",
|
||||||
|
gen_blocking = false
|
||||||
|
)]
|
||||||
|
trait MalcontentDns {
|
||||||
|
fn get_restrictions(&self, uid: u32) -> Result<Restrictions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, zvariant::Type)]
|
||||||
|
pub struct Restriction {
|
||||||
|
pub ip: IpAddr,
|
||||||
|
pub hostname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Restrictions = Vec<Restriction>;
|
|
@ -0,0 +1,111 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
mod dbus;
|
||||||
|
|
||||||
|
use {
|
||||||
|
self::dbus::*,
|
||||||
|
anyhow::Result,
|
||||||
|
nix::unistd::{getuid, Uid},
|
||||||
|
once_cell::sync::Lazy,
|
||||||
|
std::collections::HashMap,
|
||||||
|
std::net::{SocketAddr, TcpStream},
|
||||||
|
std::sync::{Arc, RwLock},
|
||||||
|
trust_dns_proto::rr::domain::Name as DomainName,
|
||||||
|
trust_dns_resolver::config as dns_config,
|
||||||
|
trust_dns_resolver::TokioAsyncResolver,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use self::dbus::{Restriction, Restrictions};
|
||||||
|
|
||||||
|
const DNS_UDP_PORT: u16 = 53;
|
||||||
|
const DNS_TLS_PORT: u16 = 853;
|
||||||
|
|
||||||
|
pub static POLICY_CHECKER: Lazy<PolicyChecker> = Lazy::new(|| PolicyChecker::new());
|
||||||
|
|
||||||
|
// TODO: accept notifications about config changes
|
||||||
|
pub struct PolicyChecker {
|
||||||
|
resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PolicyChecker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
resolvers: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restrictions<'a>(&'a self, user: Uid) -> Result<Restrictions> {
|
||||||
|
if user.is_root() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
let connection = zbus::Connection::session().await?;
|
||||||
|
let proxy = MalcontentDnsProxy::new(&connection).await?;
|
||||||
|
Ok(proxy.get_restrictions(user.as_raw()).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> {
|
||||||
|
let user = user.unwrap_or_else(|| getuid());
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
{
|
||||||
|
let ro_resolvers = self.resolvers.read().unwrap();
|
||||||
|
if let Some(resolver) = ro_resolvers.get(&user) {
|
||||||
|
return Ok(resolver.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, initialize and cache it
|
||||||
|
{
|
||||||
|
let mut rw_resolvers = self.resolvers.write().unwrap();
|
||||||
|
if let Some(resolver) = rw_resolvers.get(&user) {
|
||||||
|
return Ok(resolver.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// try first to prime resolver with DoT implementation,
|
||||||
|
// fallback to unencrypted only if not available.
|
||||||
|
let restrictions = self.restrictions(user).await?;
|
||||||
|
let resolver = if !restrictions.is_empty() {
|
||||||
|
let resolver = TokioAsyncResolver::tokio(
|
||||||
|
resolver_config_for(restrictions),
|
||||||
|
dns_config::ResolverOpts::default(),
|
||||||
|
)?;
|
||||||
|
Some(Arc::new(resolver))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
rw_resolvers.insert(user, resolver.clone());
|
||||||
|
Ok(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolver_config_for(restrictions: Vec<Restriction>) -> dns_config::ResolverConfig {
|
||||||
|
use dns_config::NameServerConfigGroup as NsConfig;
|
||||||
|
|
||||||
|
let resolver_config_group =
|
||||||
|
restrictions
|
||||||
|
.into_iter()
|
||||||
|
.fold(NsConfig::new(), |mut config, restr| {
|
||||||
|
let new_config =
|
||||||
|
if TcpStream::connect(SocketAddr::new(restr.ip, DNS_TLS_PORT)).is_ok() {
|
||||||
|
NsConfig::from_ips_tls(&[restr.ip], DNS_TLS_PORT, restr.hostname, true)
|
||||||
|
} else {
|
||||||
|
NsConfig::from_ips_clear(&[restr.ip], DNS_UDP_PORT, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
config.merge(new_config);
|
||||||
|
config
|
||||||
|
});
|
||||||
|
|
||||||
|
let basename = gethostname::gethostname()
|
||||||
|
.as_os_str()
|
||||||
|
.to_str()
|
||||||
|
.map(|hn| DomainName::from_labels(hn.split('.')).ok())
|
||||||
|
.flatten()
|
||||||
|
.map(|hn| hn.base_name());
|
||||||
|
|
||||||
|
dns_config::ResolverConfig::from_parts(basename, vec![], resolver_config_group)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
include!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/src/policy_checker/dbus.rs"
|
||||||
|
));
|
||||||
|
|
||||||
|
use {
|
||||||
|
event_listener::{Event, EventListener},
|
||||||
|
nix::unistd::Uid,
|
||||||
|
std::collections::HashMap,
|
||||||
|
zbus::dbus_interface,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct MalcontentDBusMock {
|
||||||
|
responses: HashMap<Uid, Vec<Restrictions>>,
|
||||||
|
invocations_left: usize,
|
||||||
|
finished: Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[dbus_interface(name = "com.endlessm.ParentalControls.Dns")]
|
||||||
|
impl MalcontentDBusMock {
|
||||||
|
fn get_restrictions(&mut self, user_id: u32) -> Restrictions {
|
||||||
|
let answers = self
|
||||||
|
.responses
|
||||||
|
.get_mut(&Uid::from_raw(user_id))
|
||||||
|
.expect(&format!(
|
||||||
|
"MockError: No mocked invocations available for user with id {}",
|
||||||
|
user_id
|
||||||
|
));
|
||||||
|
let restrictions = answers.pop().expect(&format!(
|
||||||
|
"MockError: DBus mock is saturated for user with id {}",
|
||||||
|
user_id
|
||||||
|
));
|
||||||
|
self.invocations_left -= 1;
|
||||||
|
if self.invocations_left == 0 {
|
||||||
|
self.finished.notify(1);
|
||||||
|
}
|
||||||
|
restrictions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MalcontentDBusMock {
|
||||||
|
pub fn new(mut responses: HashMap<Uid, Vec<Restrictions>>) -> Self {
|
||||||
|
let responses_size: usize = responses.values().map(|v| v.len()).sum();
|
||||||
|
for r in responses.values_mut() {
|
||||||
|
r.reverse(); // we pop responses from the back, so...
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = Self {
|
||||||
|
responses,
|
||||||
|
invocations_left: responses_size,
|
||||||
|
finished: Event::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ret.invocations_left == 0 {
|
||||||
|
ret.finished.notify(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn waiter(&self) -> EventListener {
|
||||||
|
self.finished.listen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MalcontentDBusMock {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
assert_eq!(
|
||||||
|
self.invocations_left, 0,
|
||||||
|
"MockError: During teardown, {} invocations are still left on the mock object",
|
||||||
|
self.invocations_left
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,12 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
||||||
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/constants.rs"));
|
|
||||||
|
mod dbus;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
|
self::dbus::MalcontentDBusMock,
|
||||||
anyhow::{anyhow, bail, ensure, Result},
|
anyhow::{anyhow, bail, ensure, Result},
|
||||||
futures_util::TryStreamExt,
|
|
||||||
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
||||||
nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
|
nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
|
||||||
nix::unistd::Uid,
|
nix::unistd::Uid,
|
||||||
|
@ -27,6 +28,8 @@ use {
|
||||||
tokio::task::JoinHandle,
|
tokio::task::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use self::dbus::{Restriction, Restrictions};
|
||||||
|
|
||||||
// Adapted from rusty_forkfork (which inherits it from rusty_fork)
|
// Adapted from rusty_forkfork (which inherits it from rusty_fork)
|
||||||
// to allow a custom pre-fork function
|
// to allow a custom pre-fork function
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -179,69 +182,16 @@ fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Restrictions = Vec<IpAddr>;
|
|
||||||
|
|
||||||
pub fn mock_dbus(responses: HashMap<Uid, Vec<Restrictions>>) -> JoinHandle<Result<()>> {
|
pub fn mock_dbus(responses: HashMap<Uid, Vec<Restrictions>>) -> JoinHandle<Result<()>> {
|
||||||
async fn dbus_loop(mut responses: HashMap<Uid, Vec<Restrictions>>) -> Result<()> {
|
async fn dbus_loop(responses: HashMap<Uid, Vec<Restrictions>>) -> Result<()> {
|
||||||
use zbus::MessageType::*;
|
let mock = MalcontentDBusMock::new(responses);
|
||||||
|
let waiter = mock.waiter();
|
||||||
let mut responses_size: usize = responses.values().map(|v| v.len()).sum();
|
let _connection = zbus::ConnectionBuilder::session()?
|
||||||
for r in responses.values_mut() {
|
.serve_at("/com/endlessm/ParentalControls/Dns", mock)?
|
||||||
r.reverse(); // we pop responses from the back, so...
|
.build()
|
||||||
}
|
.await?;
|
||||||
|
|
||||||
let connection = zbus::ConnectionBuilder::session()?.build().await?;
|
|
||||||
let mut stream = zbus::MessageStream::from(&connection);
|
|
||||||
while responses_size > 0 {
|
|
||||||
let msg = stream
|
|
||||||
.try_next()
|
|
||||||
.await?
|
|
||||||
.ok_or(anyhow!("Unparseable DBus message"))?;
|
|
||||||
|
|
||||||
if msg.header()?.message_type()? == MethodCall
|
|
||||||
&& msg
|
|
||||||
.interface()
|
|
||||||
.ok_or(anyhow!("Invoked method has no interface"))?
|
|
||||||
== DBUS_INTERFACE
|
|
||||||
&& msg
|
|
||||||
.member()
|
|
||||||
.ok_or(anyhow!("Invoked method has no member"))?
|
|
||||||
== DBUS_GET_RESTRICTIONS_METHOD
|
|
||||||
{
|
|
||||||
let user_id: u32 = msg.body()?;
|
|
||||||
match responses.get_mut(&Uid::from_raw(user_id)) {
|
|
||||||
Some(answers) => match answers.pop() {
|
|
||||||
Some(a) => {
|
|
||||||
responses_size = responses_size - 1;
|
|
||||||
let ips: Vec<String> = a.into_iter().map(|ip| ip.to_string()).collect();
|
|
||||||
connection.reply(&msg, &ips).await
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
connection
|
|
||||||
.reply_error(
|
|
||||||
&msg,
|
|
||||||
"MockExhausted",
|
|
||||||
&format!("DBus mock is saturated for user with id {}", user_id),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
connection
|
|
||||||
.reply_error(
|
|
||||||
&msg,
|
|
||||||
"MockExhausted",
|
|
||||||
&format!(
|
|
||||||
"No mocked invocations available for user with id {}",
|
|
||||||
user_id
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
waiter.wait();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,35 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use {
|
use {
|
||||||
crate::common::Eai,
|
crate::common::{Eai, Restriction, Restrictions},
|
||||||
anyhow::Result,
|
anyhow::Result,
|
||||||
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
|
||||||
nix::unistd::getuid,
|
nix::unistd::getuid,
|
||||||
once_cell::sync::Lazy,
|
once_cell::sync::Lazy,
|
||||||
std::collections::HashMap,
|
std::collections::HashMap,
|
||||||
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||||
//std::time::Duration,
|
std::time::Duration,
|
||||||
//tokio::time::timeout,
|
tokio::time::timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
|
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Restrictions> = Lazy::new(|| {
|
||||||
vec![
|
vec![
|
||||||
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
Restriction {
|
||||||
IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)),
|
ip: IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
|
||||||
IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1113)),
|
hostname: "cloudflare-dns.com".into(),
|
||||||
IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1003)),
|
},
|
||||||
|
Restriction {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)),
|
||||||
|
hostname: "cloudflare-dns.com".into(),
|
||||||
|
},
|
||||||
|
Restriction {
|
||||||
|
ip: IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1113)),
|
||||||
|
hostname: "cloudflare-dns.com".into(),
|
||||||
|
},
|
||||||
|
Restriction {
|
||||||
|
ip: IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1003)),
|
||||||
|
hostname: "cloudflare-dns.com".into(),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,7 +48,7 @@ fork_test! {
|
||||||
fn application_dns_is_nxdomain() -> Result<()> {
|
fn application_dns_is_nxdomain() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let _dbus = common::mock_dbus(HashMap::from([(
|
let dbus = common::mock_dbus(HashMap::from([(
|
||||||
getuid(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
@ -61,25 +73,26 @@ fork_test! {
|
||||||
freeaddrinfo(addr);
|
freeaddrinfo(addr);
|
||||||
};
|
};
|
||||||
|
|
||||||
//timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn getaddrinfo_resolution() -> Result<()> {
|
fn getaddrinfo_resolution() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
|
|
||||||
|
const HOSTNAME: &str = "gnome.org";
|
||||||
|
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let _dbus = common::mock_dbus(HashMap::from([(
|
let dbus = common::mock_dbus(HashMap::from([(
|
||||||
getuid(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
|
||||||
let hostname = std::ffi::CString::new("gnome.org").unwrap();
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut addr = std::ptr::null_mut();
|
let mut addr = std::ptr::null_mut();
|
||||||
let getaddrinfo_status = getaddrinfo(
|
let getaddrinfo_status = getaddrinfo(
|
||||||
hostname.as_ptr(),
|
std::ffi::CString::new(HOSTNAME)?.as_ptr(),
|
||||||
std::ptr::null(),
|
std::ptr::null(),
|
||||||
std::ptr::null(),
|
std::ptr::null(),
|
||||||
&mut addr,
|
&mut addr,
|
||||||
|
@ -95,8 +108,7 @@ fork_test! {
|
||||||
freeaddrinfo(addr);
|
freeaddrinfo(addr);
|
||||||
};
|
};
|
||||||
|
|
||||||
//timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,48 +117,64 @@ fork_test! {
|
||||||
fn wikipedia_is_unrestricted() -> Result<()> {
|
fn wikipedia_is_unrestricted() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
|
|
||||||
|
const HOSTNAME: &str = "wikipedia.org";
|
||||||
|
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let _dbus = common::mock_dbus(HashMap::from([(
|
|
||||||
getuid(),
|
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
|
||||||
)]));
|
|
||||||
|
|
||||||
const HOSTNAME: &str = "wikipedia.org";
|
|
||||||
|
|
||||||
for family in [libc::AF_INET, libc::AF_INET6] {
|
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||||
|
let dbus = common::mock_dbus(HashMap::from([(
|
||||||
|
getuid(),
|
||||||
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
|
)]));
|
||||||
|
|
||||||
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
||||||
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
||||||
assert_eq!(system_addr, our_addr);
|
assert_eq!(system_addr, our_addr);
|
||||||
}
|
|
||||||
|
|
||||||
//timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await???;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adultsite_is_restricted() -> Result<()> {
|
fn adultsite_is_restricted_ipv4() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
|
|
||||||
|
const HOSTNAME: &str = "nudity.testcategory.com";
|
||||||
|
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let _dbus = common::mock_dbus(HashMap::from([(
|
let dbus = common::mock_dbus(HashMap::from([(
|
||||||
getuid(),
|
getuid(),
|
||||||
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
)]));
|
)]));
|
||||||
|
|
||||||
const HOSTNAME: &str = "pornhub.com";
|
|
||||||
let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME)?;
|
let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME)?;
|
||||||
let our_addr = common::resolve_with_module(libc::AF_INET, HOSTNAME)?;
|
let our_addr = common::resolve_with_module(libc::AF_INET, HOSTNAME)?;
|
||||||
assert_ne!(system_addr, our_addr);
|
assert_ne!(system_addr, our_addr);
|
||||||
assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||||
|
|
||||||
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adultsite_is_restricted_ipv6() -> Result<()> {
|
||||||
|
common::setup()?;
|
||||||
|
|
||||||
|
const HOSTNAME: &str = "nudity.testcategory.com";
|
||||||
|
|
||||||
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
|
let dbus = common::mock_dbus(HashMap::from([(
|
||||||
|
getuid(),
|
||||||
|
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
|
||||||
|
)]));
|
||||||
|
|
||||||
let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME)?;
|
let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME)?;
|
||||||
let our_addr = common::resolve_with_module(libc::AF_INET6, HOSTNAME)?;
|
let our_addr = common::resolve_with_module(libc::AF_INET6, HOSTNAME)?;
|
||||||
assert_ne!(system_addr, our_addr);
|
assert_ne!(system_addr, our_addr);
|
||||||
assert_eq!(our_addr, IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
assert_eq!(our_addr, IpAddr::V6(Ipv6Addr::UNSPECIFIED));
|
||||||
|
|
||||||
//timeout(Duration::from_secs(1), dbus).await??
|
timeout(Duration::from_secs(1), dbus).await??
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,20 +183,17 @@ fork_test! {
|
||||||
fn privileged_user_bypasses_restrictions() -> Result<()> {
|
fn privileged_user_bypasses_restrictions() -> Result<()> {
|
||||||
common::setup()?;
|
common::setup()?;
|
||||||
|
|
||||||
|
const HOSTNAME: &str = "nudity.testcategory.com";
|
||||||
|
|
||||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||||
let _dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
|
|
||||||
|
|
||||||
const HOSTNAME: &str = "pornhub.com";
|
|
||||||
|
|
||||||
for family in [libc::AF_INET, libc::AF_INET6] {
|
for family in [libc::AF_INET, libc::AF_INET6] {
|
||||||
|
let dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
|
||||||
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
let system_addr = common::resolve_with_system(family, HOSTNAME)?;
|
||||||
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
let our_addr = common::resolve_with_module(family, HOSTNAME)?;
|
||||||
assert_eq!(system_addr, our_addr);
|
assert_eq!(system_addr, our_addr);
|
||||||
|
timeout(Duration::from_secs(1), dbus).await???
|
||||||
}
|
}
|
||||||
|
|
||||||
//timeout(Duration::from_secs(1), dbus).await??
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue