Use unique name for dbus connection during integration testing

This commit is contained in:
Matteo Settenvini 2022-08-25 01:00:18 +02:00
parent c52195dd8b
commit 9978bfd783
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
5 changed files with 74 additions and 54 deletions

View File

@ -18,10 +18,14 @@ panic = "unwind" # We rely on this
crate-type = ["cdylib"] crate-type = ["cdylib"]
name = "nss_malcontent" name = "nss_malcontent"
[features]
integration_test = ["dep:zbus_names"]
[build-dependencies] [build-dependencies]
bindgen = "0.60" bindgen = "0.60"
[dev-dependencies] [dev-dependencies]
malcontent-nss = { path = ".", features = ["integration_test"] }
event-listener = "2.5" event-listener = "2.5"
futures-util = "0.3" futures-util = "0.3"
rusty-hook = "0.11" rusty-hook = "0.11"
@ -42,3 +46,4 @@ 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" zvariant = "3.6"
zbus_names = { version = "2.2", optional = true }

View File

@ -11,6 +11,7 @@ use {
std::collections::HashMap, std::collections::HashMap,
std::net::{SocketAddr, TcpStream}, std::net::{SocketAddr, TcpStream},
std::sync::{Arc, RwLock}, std::sync::{Arc, RwLock},
std::time::Duration,
trust_dns_proto::rr::domain::Name as DomainName, trust_dns_proto::rr::domain::Name as DomainName,
trust_dns_resolver::config as dns_config, trust_dns_resolver::config as dns_config,
trust_dns_resolver::TokioAsyncResolver, trust_dns_resolver::TokioAsyncResolver,
@ -35,14 +36,36 @@ impl PolicyChecker {
} }
} }
async fn restrictions<'a>(&'a self, user: Uid) -> Result<Restrictions> { async fn restrictions(&self, user: Uid) -> Result<Restrictions> {
if user.is_root() { if user.is_root() {
return Ok(vec![]); return Ok(vec![]);
}; };
let connection = zbus::Connection::session().await?; let connection = zbus::Connection::session().await?;
#[cfg(not(feature = "integration_test"))]
let proxy = MalcontentDnsProxy::new(&connection).await?; let proxy = MalcontentDnsProxy::new(&connection).await?;
Ok(proxy.get_restrictions(user.as_raw()).await?)
#[cfg(feature = "integration_test")]
let proxy = {
let dbus_name = std::env::var("TEST_DBUS_SERVICE_NAME").map_err(|_| {
anyhow::anyhow!("The test hasn't set the TEST_DBUS_SERVICE_NAME environment var")
})?;
MalcontentDnsProxy::builder(&connection)
.destination(zbus_names::UniqueName::try_from(dbus_name).unwrap())
.unwrap()
.build()
.await
.expect("Unable to build DBus proxy object")
};
let restrictions = proxy.get_restrictions(user.as_raw()).await;
log::trace!(
"malcontent-nss: user {} restrictions are {:?}",
user,
&restrictions
);
Ok(restrictions?)
} }
pub async fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> { pub async fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> {
@ -89,12 +112,17 @@ fn resolver_config_for(restrictions: Vec<Restriction>) -> dns_config::ResolverCo
restrictions restrictions
.into_iter() .into_iter()
.fold(NsConfig::new(), |mut config, restr| { .fold(NsConfig::new(), |mut config, restr| {
let new_config = let supports_tls = TcpStream::connect_timeout(
if TcpStream::connect(SocketAddr::new(restr.ip, DNS_TLS_PORT)).is_ok() { &SocketAddr::new(restr.ip, DNS_TLS_PORT),
NsConfig::from_ips_tls(&[restr.ip], DNS_TLS_PORT, restr.hostname, true) Duration::from_secs(1),
} else { )
NsConfig::from_ips_clear(&[restr.ip], DNS_UDP_PORT, true) .is_ok();
};
let new_config = if supports_tls {
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.merge(new_config);
config config

View File

@ -73,5 +73,21 @@ impl Drop for MalcontentDBusMock {
"MockError: During teardown, {} invocations are still left on the mock object", "MockError: During teardown, {} invocations are still left on the mock object",
self.invocations_left self.invocations_left
); );
self.finished.notify(1);
} }
} }
pub async fn mock_dbus(responses: HashMap<Uid, Vec<Restrictions>>) -> Result<zbus::Connection> {
let mock = MalcontentDBusMock::new(responses);
let connection = zbus::ConnectionBuilder::session()?
.serve_at("/com/endlessm/ParentalControls/Dns", mock)?
.build()
.await?;
std::env::set_var(
"TEST_DBUS_SERVICE_NAME",
connection.unique_name().unwrap().as_str(),
);
Ok(connection)
}

View File

@ -11,12 +11,9 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
mod dbus; mod dbus;
use { use {
self::dbus::MalcontentDBusMock,
anyhow::{anyhow, bail, ensure, Result}, anyhow::{anyhow, bail, ensure, Result},
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,
std::collections::HashMap,
std::env, std::env,
std::ffi::{CStr, CString}, std::ffi::{CStr, CString},
std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
@ -24,11 +21,9 @@ use {
std::process::Command, std::process::Command,
std::str::FromStr, std::str::FromStr,
std::sync::Once, std::sync::Once,
tokio::task,
tokio::task::JoinHandle,
}; };
pub use self::dbus::{Restriction, Restrictions}; pub use self::dbus::{mock_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
@ -181,19 +176,3 @@ fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> {
bail!("addrinfo is not either an IPv4 or IPv6 address") bail!("addrinfo is not either an IPv4 or IPv6 address")
} }
} }
pub fn mock_dbus(responses: HashMap<Uid, Vec<Restrictions>>) -> JoinHandle<Result<()>> {
async fn dbus_loop(responses: HashMap<Uid, Vec<Restrictions>>) -> Result<()> {
let mock = MalcontentDBusMock::new(responses);
let waiter = mock.waiter();
let _connection = zbus::ConnectionBuilder::session()?
.serve_at("/com/endlessm/ParentalControls/Dns", mock)?
.build()
.await?;
waiter.wait();
Ok(())
}
task::spawn(async { dbus_loop(responses).await })
}

View File

@ -11,8 +11,6 @@ use {
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,
tokio::time::timeout,
}; };
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Restrictions> = Lazy::new(|| { static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Restrictions> = Lazy::new(|| {
@ -48,10 +46,10 @@ 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()],
)])); )])).await?;
let hostname = std::ffi::CString::new("use-application-dns.net").unwrap(); let hostname = std::ffi::CString::new("use-application-dns.net").unwrap();
unsafe { unsafe {
@ -73,7 +71,7 @@ fork_test! {
freeaddrinfo(addr); freeaddrinfo(addr);
}; };
timeout(Duration::from_secs(1), dbus).await?? Ok(())
}) })
} }
@ -84,10 +82,10 @@ fork_test! {
const HOSTNAME: &str = "gnome.org"; 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()],
)])); )])).await?;
unsafe { unsafe {
let mut addr = std::ptr::null_mut(); let mut addr = std::ptr::null_mut();
@ -108,11 +106,10 @@ fork_test! {
freeaddrinfo(addr); freeaddrinfo(addr);
}; };
timeout(Duration::from_secs(1), dbus).await?? Ok(())
}) })
} }
#[test] #[test]
fn wikipedia_is_unrestricted() -> Result<()> { fn wikipedia_is_unrestricted() -> Result<()> {
common::setup()?; common::setup()?;
@ -121,16 +118,14 @@ fork_test! {
tokio::runtime::Runtime::new().unwrap().block_on(async { tokio::runtime::Runtime::new().unwrap().block_on(async {
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([( let _dbus = common::mock_dbus(HashMap::from([(
getuid(), getuid(),
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
)])); )])).await?;
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???;
} }
Ok(()) Ok(())
}) })
@ -143,17 +138,17 @@ fork_test! {
const HOSTNAME: &str = "nudity.testcategory.com"; 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()],
)])); )])).await?;
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?? Ok(())
}) })
} }
@ -164,22 +159,20 @@ fork_test! {
const HOSTNAME: &str = "nudity.testcategory.com"; 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()],
)])); )])).await;
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));
Ok(())
timeout(Duration::from_secs(1), dbus).await??
}) })
} }
#[test] #[test]
#[ignore]
fn privileged_user_bypasses_restrictions() -> Result<()> { fn privileged_user_bypasses_restrictions() -> Result<()> {
common::setup()?; common::setup()?;
@ -187,11 +180,10 @@ fork_test! {
tokio::runtime::Runtime::new().unwrap().block_on(async { tokio::runtime::Runtime::new().unwrap().block_on(async {
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 _dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])])).await?;
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???
} }
Ok(()) Ok(())
}) })