dns: NSS module implementation

This commit is contained in:
Matteo Settenvini 2024-01-06 10:29:09 +01:00
parent 3beee1cf84
commit 03fbdb2d26
Signed by: matteo
GPG key ID: 1C1B12600D81DE05
23 changed files with 4044 additions and 1 deletions

142
nss/tests/common/dbus.rs Normal file
View file

@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 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 {
std::sync::atomic::{AtomicUsize, Ordering},
tokio_util::sync::CancellationToken,
zbus::dbus_interface,
};
#[derive(Debug)]
pub struct MalcontentDBusMock {
responses: Vec<Restrictions>,
invocations_left: AtomicUsize,
}
#[dbus_interface(name = "com.endlessm.ParentalControls.Dns")]
impl MalcontentDBusMock {
fn get_dns(&mut self) -> Restrictions {
let restrictions = self
.responses
.pop()
.expect("MockError: DBus mock is saturated");
self.invocations_left.fetch_sub(1, Ordering::SeqCst);
restrictions
}
}
impl MalcontentDBusMock {
pub fn new(mut responses: Vec<Restrictions>) -> Self {
responses.reverse(); // We pop responses from the back, so...
Self {
invocations_left: AtomicUsize::new(responses.len()),
responses,
}
}
}
impl Drop for MalcontentDBusMock {
fn drop(&mut self) {
let invocations_left = self.invocations_left.load(Ordering::Acquire);
assert_eq!(
invocations_left, 0,
"MockError: During teardown, {} invocations are still left on the mock object",
invocations_left
);
}
}
pub struct DBusMockServer {
handle: std::thread::JoinHandle<Result<()>>,
cancellation: CancellationToken,
}
impl DBusMockServer {
pub fn new(responses: Vec<Restrictions>) -> Result<Self> {
let token = CancellationToken::new();
let cloned_token = token.clone();
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
std::env::set_var("TEST_DBUS_SOCKET", format!("{}", listener.local_addr()?));
let handle = std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(Self::spawn_async(responses, listener, cloned_token))
});
Ok(Self {
handle: handle,
cancellation: token,
})
}
pub fn stop(self) -> Result<()> {
self.cancellation.cancel();
self.handle.join().unwrap()
}
async fn spawn_async(
responses: Vec<Restrictions>,
listener: std::net::TcpListener,
cancellation_token: CancellationToken,
) -> Result<()> {
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
let guid = zbus::Guid::generate();
let mock = MalcontentDBusMock::new(responses);
let (stream, _) = listener
.accept()
.await
.expect("Server socket closed unexpectedly");
log::trace!("dbus mock server accepted client connection");
let _connection = zbus::ConnectionBuilder::tcp_stream(stream)
.server(&guid)
.p2p()
.auth_mechanisms(&[zbus::AuthMechanism::Anonymous])
.name("com.endlessm.ParentalControls")
.expect("Unable to serve given dbus name")
.serve_at("/com/endlessm/ParentalControls/Dns", mock)
.expect("Unable to server malcontent dbus mock object")
.build()
.await?;
tokio::select! {
_ = cancellation_token.cancelled() => {
Ok(())
}
_ = std::future::pending::<()>() => { unreachable!() }
}
}
}
pub struct DBusMockServerGuard {
mock: Option<DBusMockServer>,
}
impl DBusMockServerGuard {
pub fn new(responses: Vec<Restrictions>) -> Result<Self> {
Ok(Self {
mock: Some(DBusMockServer::new(responses)?),
})
}
}
impl Drop for DBusMockServerGuard {
fn drop(&mut self) {
self.mock
.take()
.unwrap()
.stop()
.expect("cannot stop dbus server mock");
}
}

148
nss/tests/common/mod.rs Normal file
View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
mod dbus;
use {
anyhow::{anyhow, bail, ensure, Result},
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
std::env,
std::ffi::{CStr, CString},
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
std::path::PathBuf,
std::process::Command,
std::str::FromStr,
};
pub use self::dbus::{DBusMockServerGuard, Restriction, Restrictions};
pub type Eai = EaiRetcode;
#[static_init::dynamic]
static GLOBAL_SETUP: () = {
let cdylib_path = test_cdylib::build_current_project();
let file_name = cdylib_path.file_name().unwrap();
let mut versioned_file_name = file_name.to_owned();
versioned_file_name.push(".2"); // required for NSS 3 modules
let dest = PathBuf::from(std::env::current_exe().unwrap()).with_file_name(versioned_file_name);
let _ = std::fs::remove_file(&dest);
std::fs::hard_link(&cdylib_path, &dest).expect(&format!(
"Cannot hard link {} to {}",
cdylib_path.display(),
dest.display()
));
};
/// Note that this should be called once per process; this implies that
/// each test calling this function is in a separate process too.
pub fn setup() -> Result<()> {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::max())
.try_init();
std::env::set_var("NSS_MALCONTENT_LOG", "trace");
// setup NSS to load our cdylib, now versioned
let db = CString::new("hosts").unwrap();
let resolvers = CString::new("malcontent dns").unwrap();
let nss_config_status = unsafe { __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) };
if nss_config_status != 0 {
panic!(
"Unable to configure NSS to load module: __nss_configure_lookup() returned {}",
nss_config_status
);
}
Ok(())
}
pub fn resolve_with_system(family: libc::c_int, host: &str) -> Result<IpAddr> {
let process = Command::new("getent")
.arg(match family {
libc::AF_INET => "ahostsv4",
libc::AF_INET6 => "ahostsv6",
_ => panic!("INET family must be either IPv4 or IPv6"),
})
.arg(host)
.output()?;
ensure!(
process.status.success(),
"Failed to run getent to check host IP"
);
let output = String::from_utf8(process.stdout)?;
log::trace!("system getent reports: {}", &output);
let addr_string = output
.as_str()
.split(' ')
.next()
.ok_or(anyhow!("Unparseable output from getent"))?;
Ok(IpAddr::from_str(&addr_string)?)
}
pub fn resolve_with_module(family: libc::c_int, hostname: &str) -> Result<Vec<IpAddr>> {
let c_hostname = CString::new(hostname).unwrap();
unsafe {
let mut addr = std::ptr::null_mut();
let hints = libc::addrinfo {
ai_family: family,
ai_flags: 0,
ai_socktype: 0,
ai_protocol: 0,
ai_addrlen: 0,
ai_addr: std::ptr::null_mut(),
ai_canonname: std::ptr::null_mut(),
ai_next: std::ptr::null_mut(),
};
let getaddrinfo_status =
getaddrinfo(c_hostname.as_ptr(), std::ptr::null(), &hints, &mut addr);
let error = CStr::from_ptr(gai_strerror(getaddrinfo_status));
assert_eq!(
getaddrinfo_status,
Eai::Success.0,
"Should have gotten hostname for {}, instead got {}",
hostname,
error.to_str().unwrap()
);
let ips = convert_addrinfo(addr)?;
freeaddrinfo(addr);
Ok(ips)
}
}
unsafe fn convert_addrinfo(addrs: *const libc::addrinfo) -> Result<Vec<IpAddr>> {
let mut ips = vec![];
let mut addr = addrs;
while addr != std::ptr::null() {
let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen))
.ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?;
if let Some(addr) = addr_storage.as_sockaddr_in() {
ips.push(IpAddr::V4(Ipv4Addr::from(addr.ip())));
} else if let Some(addr) = addr_storage.as_sockaddr_in6() {
ips.push(IpAddr::V6(Ipv6Addr::from(addr.ip())));
} else {
bail!("addrinfo is not either an IPv4 or IPv6 address")
}
addr = (*addr).ai_next;
}
ips.sort();
ips.dedup();
Ok(ips)
}