From 516bf4000cb625fa3c2aefe5723ab1d87fa3d330 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sun, 21 Aug 2022 14:47:37 +0200 Subject: [PATCH] Use an async context for module and refactor --- Cargo.toml | 4 +- build.rs | 8 +- src/gethostbyaddr.rs | 23 ++++ src/gethostbyname.rs | 117 ++++++++++++++++++ src/helpers.rs | 99 +++++++++++++++ src/lib.rs | 5 +- src/nss_api.rs | 120 +++++++++--------- src/{utils.rs => nss_bindings.rs} | 7 +- src/policy_checker.rs | 18 +-- tests/common.rs | 155 ++++++++++++++++------- tests/integration_test.rs | 196 ++++++++++++++++-------------- wrapper.hpp | 3 + 12 files changed, 542 insertions(+), 213 deletions(-) create mode 100644 src/gethostbyaddr.rs create mode 100644 src/gethostbyname.rs create mode 100644 src/helpers.rs rename src/{utils.rs => nss_bindings.rs} (56%) diff --git a/Cargo.toml b/Cargo.toml index d91fc1d..a6efcb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,17 +18,17 @@ bindgen = "0.60" [dev-dependencies] futures-util = "0.3" rusty-hook = "0.11" +rusty-forkfork = "0.4" test-cdylib = "1.1" tokio = { version = "1", features = ["rt", "sync", "macros", "time"] } -zbus = { version = "3.0", default-features = false, features = ["tokio"] } [dependencies] anyhow = "1.0" const_format = "0.2" libc = "0.2" once_cell = "1.13" -gio = "0.15" log = "0.4" nix = { version = "0.25", features = ["socket", "user", "sched"] } tokio = { version = "1", features = ["rt"] } trust-dns-resolver = "0.21" +zbus = { version = "3.0", default-features = false, features = ["tokio"] } diff --git a/build.rs b/build.rs index f9ec8c6..4104b6b 100644 --- a/build.rs +++ b/build.rs @@ -4,17 +4,11 @@ use {std::env, std::path::PathBuf}; fn main() { - let out_dir = std::env::var("OUT_DIR").unwrap(); - - println!("cargo:rerun-if-changed=wrapper.h"); + println!("cargo:rerun-if-changed=wrapper.hpp"); // Required by NSS 2 println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libnss_malcontent.so.2"); - // Enable dynamic loading of NSS module from OUT_DIR - // (see https://doc.rust-lang.org/cargo/reference/environment-variables.html#dynamic-library-paths) - println!("cargo:rustc-link-search={}", &out_dir); - let bindings = bindgen::Builder::default() .header("wrapper.hpp") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) diff --git a/src/gethostbyaddr.rs b/src/gethostbyaddr.rs new file mode 100644 index 0000000..ae4df35 --- /dev/null +++ b/src/gethostbyaddr.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: GPL-3.0-or-later + +use { + crate::nss_bindings::{nss_status, HErrno}, + libc::{hostent, size_t, socklen_t}, + nix::errno::Errno, + std::os::raw::{c_char, c_int, c_void}, +}; + +pub async unsafe fn gethostbyaddr2_r( + _addr: *const c_void, + _len: socklen_t, + _af: c_int, + _host: *mut hostent, + _buffer: *mut c_char, + _buflen: size_t, + _errnop: *mut Errno, + _h_errnop: *mut HErrno, + _ttlp: *mut i32, +) -> nss_status { + todo!() +} diff --git a/src/gethostbyname.rs b/src/gethostbyname.rs new file mode 100644 index 0000000..3c38696 --- /dev/null +++ b/src/gethostbyname.rs @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: GPL-3.0-or-later + +use { + crate::helpers::{ips_to_gaih_addr, set_if_valid}, + crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno}, + crate::policy_checker::{PolicyChecker as _, POLICY_CHECKER}, + libc::{hostent, size_t}, + nix::errno::Errno, + std::ffi::CStr, + std::os::raw::{c_char, c_int}, +}; + +// See https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https +const CANARY_HOSTNAME: &str = "use-application-dns.net"; + +pub async unsafe fn gethostbyname4_r( + name: *const c_char, + pat: *mut *mut gaih_addrtuple, + buffer: *mut c_char, + buflen: size_t, + errnop: *mut Errno, + h_errnop: *mut HErrno, + ttlp: *mut i32, +) -> nss_status { + match POLICY_CHECKER.resolver(None) { + Ok(None) => { + // no restrictions for user, the next NSS module will decide + nss_status::NSS_STATUS_NOTFOUND + } + Ok(Some(resolver)) => { + let name = match CStr::from_ptr(name).to_str() { + Ok(name) => name, + Err(_) => { + set_if_valid(errnop, Errno::EINVAL); + set_if_valid(h_errnop, HErrno::Internal); + return nss_status::NSS_STATUS_TRYAGAIN; + } + }; + + // disable application-based DNS for those applications + // (notably, Firefox) that support it + if name == CANARY_HOSTNAME { + set_if_valid(h_errnop, HErrno::HostNotFound); + return nss_status::NSS_STATUS_SUCCESS; + } + + match resolver.lookup_ip(name).await { + Ok(result) if result.iter().peekable().peek().is_none() => { + set_if_valid(h_errnop, HErrno::HostNotFound); + nss_status::NSS_STATUS_SUCCESS + } + Ok(result) => { + if pat == std::ptr::null_mut() { + set_if_valid(errnop, Errno::EINVAL); + set_if_valid(h_errnop, HErrno::Internal); + return nss_status::NSS_STATUS_TRYAGAIN; + } + + let ttl = result + .valid_until() + .duration_since(std::time::Instant::now()) + .as_secs(); + set_if_valid( + ttlp, + if ttl < (i32::MAX as u64) { + ttl as i32 + } else { + i32::MAX + }, + ); + + let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, buflen); + match ips_to_gaih_addr(result, buf) { + Ok(addrs) => { + // DEBUG: eprintln!("{:?} => {:?}", addrs, *addrs); + *pat = addrs; + nss_status::NSS_STATUS_SUCCESS + } + Err(err) => { + set_if_valid( + errnop, + err.raw_os_error() + .map(Errno::from_i32) + .unwrap_or(Errno::EAGAIN), + ); + set_if_valid(h_errnop, HErrno::Internal); + nss_status::NSS_STATUS_TRYAGAIN + } + } + } + Err(err) => { + log::warn!("{}", err); + nss_status::NSS_STATUS_UNAVAIL + } + } + } + Err(err) => { + log::error!("{}", err); + nss_status::NSS_STATUS_UNAVAIL + } + } +} + +pub async unsafe fn gethostbyname3_r( + _name: *const c_char, + _af: c_int, + _host: *mut hostent, + _buffer: *mut c_char, + _buflen: size_t, + _errnop: *mut Errno, + _h_errnop: *mut HErrno, + _ttlp: *mut i32, + _canonp: *mut *mut char, +) -> nss_status { + todo!() +} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..c51b8d9 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: GPL-3.0-or-later + +use { + crate::nss_bindings::gaih_addrtuple, + anyhow::{bail, Result}, + libc::{AF_INET, AF_INET6}, + nix::errno::Errno, + once_cell::sync::Lazy, + std::ffi::CString, + std::mem::{align_of, size_of}, + std::net::IpAddr, + trust_dns_resolver::lookup_ip::LookupIp, +}; + +static RUNTIME: Lazy> = Lazy::new(|| { + // The runtime should remain single-threaded, some + // programs depend on it (e.g. programs calling unshare()) + let rt = tokio::runtime::Builder::new_current_thread().build()?; + Ok(rt.handle().clone()) +}); + +pub unsafe fn ips_to_gaih_addr( + ips: LookupIp, + mut buf: &mut [u8], +) -> std::io::Result<*mut gaih_addrtuple> { + const GAIH_ADDRTUPLE_SZ: usize = size_of::(); + + let mut ret = std::ptr::null_mut(); + let query = ips.query(); + + let name = CString::new(query.name().to_utf8()).unwrap(); // TODO: .map_err() and fail more graciously + let mut prev_link: *mut *mut gaih_addrtuple = std::ptr::null_mut(); + + for addr in ips { + // First add the name to the buffer + let offset = buf.as_ptr().align_offset(align_of::()); + let name_src = name.as_bytes_with_nul(); + let name_dest = buf.as_mut_ptr().add(offset); + + let l = name_src.len(); + if buf.len() < offset + l { + return Err(Errno::ERANGE.into()); + } + + std::ptr::copy_nonoverlapping(name_src.as_ptr(), name_dest, l); + buf = &mut buf[(offset + l)..]; + + // Then add the tuple with the address + let offset = buf.as_ptr().align_offset(align_of::()); + let l = GAIH_ADDRTUPLE_SZ; + if buf.len() < offset + l { + return Err(Errno::ERANGE.into()); + } + + let tuple = &mut *(buf.as_mut_ptr().add(offset) as *mut gaih_addrtuple); + tuple.next = std::ptr::null_mut(); + tuple.name = name_dest as *mut i8; + tuple.scopeid = 0; // how to set tuple.scopeid ???? + set_if_valid(prev_link, &mut *tuple); // link from previous tuple to this tuple + prev_link = &mut (*tuple).next; + + match addr { + IpAddr::V4(addr) => { + tuple.family = AF_INET; + tuple.addr[0] = std::mem::transmute_copy(&addr.octets()); + } + IpAddr::V6(addr) => { + tuple.family = AF_INET6; + tuple.addr = std::mem::transmute_copy(&addr.octets()); + } + } + + if ret == std::ptr::null_mut() { + ret = tuple; + } + + buf = &mut buf[(offset + l)..]; + } + + Ok(ret) +} + +pub fn set_if_valid(ptr: *mut T, val: T) { + if !ptr.is_null() { + unsafe { *ptr = val }; + } +} + +pub fn block_on(f: F) -> Result +where + F: std::future::Future, +{ + use std::ops::Deref; + match RUNTIME.deref() { + Ok(rt_handle) => Ok(rt_handle.block_on(async { f.await })), + Err(e) => bail!("Unable to start tokio runtime: {}", e), + } +} diff --git a/src/lib.rs b/src/lib.rs index 224e314..5c2893e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,9 @@ #![allow(non_snake_case)] mod constants; +mod gethostbyaddr; +mod gethostbyname; +mod helpers; pub mod nss_api; +mod nss_bindings; mod policy_checker; -mod utils; diff --git a/src/nss_api.rs b/src/nss_api.rs index 22bfbfb..09ced33 100644 --- a/src/nss_api.rs +++ b/src/nss_api.rs @@ -1,75 +1,73 @@ // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: GPL-3.0-or-later -#![allow(dead_code)] -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); - use { - crate::policy_checker::{MalcontentPolicyChecker, PolicyChecker as _}, - crate::utils::set_if_valid, + crate::gethostbyaddr::gethostbyaddr2_r, + crate::gethostbyname::{gethostbyname3_r, gethostbyname4_r}, + crate::helpers::{block_on, set_if_valid}, + crate::nss_bindings::{gaih_addrtuple, nss_status, HErrno}, libc::{hostent, size_t, socklen_t, AF_INET}, nix::errno, nix::errno::Errno, - std::ffi::CStr, std::os::raw::{c_char, c_int, c_void}, std::ptr, }; -// See https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https -const CANARY_HOSTNAME: &[u8] = b"use-application-dns.net\0"; - // -------------- by host --------------- #[no_mangle] pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r( name: *const c_char, - _pat: *mut *mut gaih_addrtuple, - _buffer: *mut c_char, - _buflen: size_t, + pat: *mut *mut gaih_addrtuple, + buffer: *mut c_char, + buflen: size_t, errnop: *mut Errno, h_errnop: *mut HErrno, - _ttlp: *mut i32, + ttlp: *mut i32, ) -> nss_status { - let name = CStr::from_ptr(name); - let policy_checker = MalcontentPolicyChecker::new(); // TODO: make LazySync, use RefCell for interior mutability - match policy_checker.resolver(None) { - Ok(None) => nss_status::NSS_STATUS_NOTFOUND, - Ok(Some(resolver)) => { - if name == CStr::from_bytes_with_nul_unchecked(CANARY_HOSTNAME) { - set_if_valid(errnop, errno::from_i32(0)); - set_if_valid(h_errnop, HErrno::HostNotFound); - return nss_status::NSS_STATUS_SUCCESS; - } + set_if_valid(errnop, errno::from_i32(0)); + set_if_valid(h_errnop, HErrno::Success); - match resolver.lookup_ip(name.to_str().unwrap()) { - Ok(_result) => { - // TODO - // https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/posix/getaddrinfo.c;h=fed2d3bf130b6fc88e5c6840804bbadb8fe9a98b;hb=4ab2ab03d4351914ee53248dc5aef4a8c88ff8b9 - // https://chromium.googlesource.com/chromiumos/third_party/glibc/+/cvs/fedora-glibc-2_8_90-1/nss/nss_files/files-hosts.c - // https://github.com/systemd/systemd/blob/d5548eb618d426a3952746eb3a0e7d95d186ea7e/src/nss-resolve/nss-resolve.c - - nss_status::NSS_STATUS_SUCCESS - } - Err(_) => nss_status::NSS_STATUS_UNAVAIL, - } + match block_on(async { + gethostbyname4_r(name, pat, buffer, buflen, errnop, h_errnop, ttlp).await + }) { + Ok(status) => status, + Err(runtime_error) => { + log::error!("gethostbyname4_r: {}", runtime_error); + set_if_valid(h_errnop, HErrno::Internal); + nss_status::NSS_STATUS_TRYAGAIN } - Err(_err) => nss_status::NSS_STATUS_UNAVAIL, } } #[no_mangle] pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r( - _name: *const c_char, - _af: c_int, - _host: *mut hostent, - _buffer: *mut c_char, - _buflen: size_t, - _errnop: *mut Errno, - _h_errnop: *mut HErrno, - _ttlp: *mut i32, - _canonp: *mut *mut char, + name: *const c_char, + af: c_int, + host: *mut hostent, + buffer: *mut c_char, + buflen: size_t, + errnop: *mut Errno, + h_errnop: *mut HErrno, + ttlp: *mut i32, + canonp: *mut *mut char, ) -> nss_status { - todo!() + set_if_valid(errnop, errno::from_i32(0)); + set_if_valid(h_errnop, HErrno::Success); + + match block_on(async { + gethostbyname3_r( + name, af, host, buffer, buflen, errnop, h_errnop, ttlp, canonp, + ) + .await + }) { + Ok(status) => status, + Err(runtime_error) => { + log::error!("gethostbyname3_r: {}", runtime_error); + set_if_valid(h_errnop, HErrno::Internal); + nss_status::NSS_STATUS_TRYAGAIN + } + } } #[no_mangle] @@ -121,17 +119,29 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname_r( #[no_mangle] pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r( - _addr: *const c_void, - _len: socklen_t, - _af: c_int, - _host: *mut hostent, - _buffer: *mut c_char, - _buflen: size_t, - _errnop: *mut Errno, - _h_errnop: *mut HErrno, - _ttlp: *mut i32, + addr: *const c_void, + len: socklen_t, + af: c_int, + host: *mut hostent, + buffer: *mut c_char, + buflen: size_t, + errnop: *mut Errno, + h_errnop: *mut HErrno, + ttlp: *mut i32, ) -> nss_status { - todo!() + set_if_valid(errnop, errno::from_i32(0)); + set_if_valid(h_errnop, HErrno::Success); + + match block_on(async { + gethostbyaddr2_r(addr, len, af, host, buffer, buflen, errnop, h_errnop, ttlp).await + }) { + Ok(status) => status, + Err(runtime_error) => { + log::error!("gethostbyaddr2_r: {}", runtime_error); + set_if_valid(h_errnop, HErrno::Internal); + nss_status::NSS_STATUS_TRYAGAIN + } + } } #[no_mangle] diff --git a/src/utils.rs b/src/nss_bindings.rs similarity index 56% rename from src/utils.rs rename to src/nss_bindings.rs index 1e20a67..2b6bde6 100644 --- a/src/utils.rs +++ b/src/nss_bindings.rs @@ -1,8 +1,5 @@ // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: GPL-3.0-or-later -pub fn set_if_valid(ptr: *mut T, val: T) { - if !ptr.is_null() { - unsafe { *ptr = val }; - } -} +#![allow(dead_code)] +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/src/policy_checker.rs b/src/policy_checker.rs index 5f9e3f5..352a504 100644 --- a/src/policy_checker.rs +++ b/src/policy_checker.rs @@ -9,15 +9,18 @@ use { std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, std::sync::{Arc, RwLock}, trust_dns_resolver::config as dns_config, - trust_dns_resolver::Resolver, + trust_dns_resolver::TokioAsyncResolver, }; pub type Restrictions<'a> = &'a [IpAddr]; pub trait PolicyChecker { - fn resolver(&self, user: Option) -> Result>>; + fn resolver(&self, user: Option) -> Result>>; } +pub static POLICY_CHECKER: Lazy = + Lazy::new(|| MalcontentPolicyChecker::new()); + static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy> = Lazy::new(|| { vec![ IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)), @@ -27,10 +30,9 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy> = Lazy::new(|| { ] }); -// TODO: make unique per process // TODO: accept notifications about config changes pub struct MalcontentPolicyChecker { - resolvers: RwLock>>>, + resolvers: RwLock>>>, } impl MalcontentPolicyChecker { @@ -53,7 +55,7 @@ impl MalcontentPolicyChecker { } impl PolicyChecker for MalcontentPolicyChecker { - fn resolver(&self, user: Option) -> Result>> { + fn resolver(&self, user: Option) -> Result>> { let user = user.unwrap_or_else(|| getuid()); { let ro_resolvers = self.resolvers.read().unwrap(); @@ -84,8 +86,10 @@ impl PolicyChecker for MalcontentPolicyChecker { config }); - let resolver = - Resolver::new(resolver_config, dns_config::ResolverOpts::default())?; + let resolver = TokioAsyncResolver::tokio( + resolver_config, + dns_config::ResolverOpts::default(), + )?; Some(Arc::new(resolver)) } None => None, diff --git a/tests/common.rs b/tests/common.rs index f517b36..5bc5b13 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -27,38 +27,98 @@ use { tokio::task::JoinHandle, }; +// Adapted from rusty_forkfork (which inherits it from rusty_fork) +// to allow a custom pre-fork function +#[macro_export] +macro_rules! fork_test { + (#![rusty_fork(timeout_ms = $timeout:expr)] + $( + $(#[$meta:meta])* + fn $test_name:ident() $(-> $ret:ty)? $body:block + )*) => { $( + $(#[$meta])* + fn $test_name() { + // Eagerly convert everything to function pointers so that all + // tests use the same instantiation of `fork`. + fn body_fn() $(-> $ret)? $body + let body: fn () $(-> $ret)? = body_fn; + + fn supervise_fn(child: &mut rusty_forkfork::ChildWrapper, + _file: &mut ::std::fs::File) { + rusty_forkfork::fork_test::supervise_child(child, $timeout) + } + let supervise: + fn (&mut rusty_forkfork::ChildWrapper, &mut ::std::fs::File) = + supervise_fn; + + rusty_forkfork::fork( + rusty_forkfork::rusty_fork_test_name!($test_name), + rusty_forkfork::rusty_fork_id!(), + $crate::common::prefork_setup, + supervise, body).expect("forking test failed"); + } + )* }; + + ($( + $(#[$meta:meta])* + fn $test_name:ident() $(-> $ret:ty)? $body:block + )*) => { + fork_test! { + #![rusty_fork(timeout_ms = 0)] + + $($(#[$meta])* fn $test_name() $(-> $ret)? $body)* + } + }; +} + +pub fn prefork_setup(_child: &mut Command) { + let out_dir = PathBuf::from(env!("OUT_DIR")); + + PREFORK.call_once(|| { + std::env::set_var("LD_LIBRARY_PATH", &out_dir); + + let library_path = test_cdylib::build_current_project(); + let mut library_filename = library_path.file_name().unwrap().to_owned(); + library_filename.push(".2"); // required for NSS modules + + let dest = out_dir.join(library_filename); + std::fs::copy(library_path, &dest).unwrap(); + }); +} + pub type Eai = EaiRetcode; +static PREFORK: Once = Once::new(); static SETUP: Once = Once::new(); pub fn setup() -> Result<()> { - let out_dir = PathBuf::from(env!("OUT_DIR")); - let nss_config_status = unsafe { - SETUP.call_once(|| { - let library_path = test_cdylib::build_current_project(); - let mut library_filename = library_path.file_name().unwrap().to_owned(); - library_filename.push(".2"); // required for NSS modules + SETUP.call_once(|| { + let nss_config_status = unsafe { + let db = CString::new("hosts").unwrap(); + let resolvers = CString::new("malcontent dns").unwrap(); + __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) + }; - let dest = out_dir.join(library_filename); - std::fs::copy(library_path, dest).unwrap(); - }); - - let db = CString::new("hosts").unwrap(); - let resolvers = CString::new("malcontent dns").unwrap(); - __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) - }; - - ensure!( - nss_config_status == 0, - "Unable to configure NSS to load module: __nss_configure_lookup() returned {}", - nss_config_status - ); + if nss_config_status != 0 { + panic!( + "Unable to configure NSS to load module: __nss_configure_lookup() returned {}", + nss_config_status + ); + } + }); Ok(()) } -pub fn system_resolve(host: &str) -> Result { - let process = Command::new("getent").arg("hosts").arg(host).output()?; +pub fn resolve_with_system(family: libc::c_int, host: &str) -> Result { + 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" @@ -72,29 +132,26 @@ pub fn system_resolve(host: &str) -> Result { Ok(IpAddr::from_str(&addr_string)?) } -pub fn convert_addrinfo(sa: &SockaddrStorage) -> Result { - if let Some(addr) = sa.as_sockaddr_in() { - Ok(IpAddr::V4(Ipv4Addr::from(addr.ip()))) - } else if let Some(addr) = sa.as_sockaddr_in6() { - Ok(IpAddr::V6(Ipv6Addr::from(addr.ip()))) - } else { - bail!("addrinfo is not either an IPv4 or IPv6 address") - } -} - -pub fn resolve_system_and_us(hostname: &str) -> Result<(IpAddr, IpAddr)> { - setup()?; - let ip_from_system = system_resolve(hostname)?; - +pub fn resolve_with_module(family: libc::c_int, hostname: &str) -> Result { + assert!( + SETUP.is_completed(), + "Forgot to call common::setup() at beginning of test?" + ); let c_hostname = CString::new(hostname).unwrap(); unsafe { let mut addr = std::ptr::null_mut(); - let getaddrinfo_status = getaddrinfo( - c_hostname.as_ptr(), - std::ptr::null(), - std::ptr::null(), - &mut addr, - ); + 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!( @@ -106,9 +163,19 @@ pub fn resolve_system_and_us(hostname: &str) -> Result<(IpAddr, IpAddr)> { ); let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen)) .ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?; - let ip_from_us = convert_addrinfo(&addr_storage)?; + let ip = convert_addrinfo(&addr_storage)?; freeaddrinfo(addr); - Ok((ip_from_system, ip_from_us)) + Ok(ip) + } +} + +fn convert_addrinfo(sa: &SockaddrStorage) -> Result { + if let Some(addr) = sa.as_sockaddr_in() { + Ok(IpAddr::V4(Ipv4Addr::from(addr.ip()))) + } else if let Some(addr) = sa.as_sockaddr_in6() { + Ok(IpAddr::V6(Ipv6Addr::from(addr.ip()))) + } else { + bail!("addrinfo is not either an IPv4 or IPv6 address") } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 36d293f..4100efb 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -24,99 +24,111 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy> = Lazy::new(|| { ] }); -#[test] -#[ignore] -fn nss_module_is_loaded() -> Result<()> { - common::setup()?; +fork_test! { + #[test] + fn nss_module_is_loaded() -> Result<()> { + common::setup()?; + common::resolve_with_module(libc::AF_INET, "gnome.org")?; + Ok(()) + } - let hostname = std::ffi::CString::new("gnome.org").unwrap(); - unsafe { - let mut addr = std::ptr::null_mut(); - let getaddrinfo_status = getaddrinfo( - hostname.as_ptr(), - std::ptr::null(), - std::ptr::null(), - &mut addr, - ); + #[test] + fn application_dns_is_nxdomain() -> Result<()> { + common::setup()?; + tokio::runtime::Runtime::new().unwrap().block_on(async { + let dbus = common::mock_dbus(HashMap::from([( + getuid(), + vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], + )])); - let error = std::ffi::CStr::from_ptr(gai_strerror(getaddrinfo_status)); - assert_eq!( - getaddrinfo_status, - 0, - "Unable to resolve hostname, getaddrinfo failed: {}", - error.to_str().unwrap() - ); - freeaddrinfo(addr); - }; + let hostname = std::ffi::CString::new("use-application-dns.net").unwrap(); + unsafe { + let mut addr = std::ptr::null_mut(); + let getaddrinfo_status = getaddrinfo( + hostname.as_ptr(), + std::ptr::null(), + std::ptr::null(), + &mut addr, + ); + + let error = std::ffi::CStr::from_ptr(gai_strerror(getaddrinfo_status)); + assert_eq!( + getaddrinfo_status, + Eai::NoName.0, + "Should have gotten no hostname (NXDOMAIN), instead got {}", + error.to_str().unwrap() + ); + freeaddrinfo(addr); + }; + + timeout(Duration::from_secs(1), dbus).await?? + }) + } + + #[test] + fn wikipedia_is_unrestricted() -> Result<()> { + common::setup()?; + + 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] { + let system_addr = common::resolve_with_system(family, HOSTNAME)?; + let our_addr = common::resolve_with_module(family, HOSTNAME)?; + assert_eq!(system_addr, our_addr); + } + + timeout(Duration::from_secs(1), dbus).await?? + }) + } + + #[test] + fn adultsite_is_restricted() -> Result<()> { + common::setup()?; + + tokio::runtime::Runtime::new().unwrap().block_on(async { + let dbus = common::mock_dbus(HashMap::from([( + getuid(), + vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], + )])); + + const HOSTNAME: &str = "pornhub.com"; + let system_addr = common::resolve_with_system(libc::AF_INET, HOSTNAME)?; + let our_addr = common::resolve_with_module(libc::AF_INET, HOSTNAME)?; + assert_ne!(system_addr, our_addr); + assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::UNSPECIFIED)); + + let system_addr = common::resolve_with_system(libc::AF_INET6, HOSTNAME)?; + let our_addr = common::resolve_with_module(libc::AF_INET6, HOSTNAME)?; + assert_ne!(system_addr, our_addr); + assert_eq!(our_addr, IpAddr::V6(Ipv6Addr::UNSPECIFIED)); + + timeout(Duration::from_secs(1), dbus).await?? + }) + } + + #[test] + fn privileged_user_bypasses_restrictions() -> Result<()> { + common::setup()?; + + 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] { + let system_addr = common::resolve_with_system(family, HOSTNAME)?; + let our_addr = common::resolve_with_module(family, HOSTNAME)?; + assert_eq!(system_addr, our_addr); + } + + timeout(Duration::from_secs(1), dbus).await?? + }) + } - Ok(()) -} - -#[tokio::test] -#[ignore] -async fn application_dns_is_nxdomain() -> Result<()> { - let dbus = common::mock_dbus(HashMap::from([( - getuid(), - vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], - )])); - - common::setup()?; - let hostname = std::ffi::CString::new("use-application-dns.net").unwrap(); - unsafe { - let mut addr = std::ptr::null_mut(); - let getaddrinfo_status = getaddrinfo( - hostname.as_ptr(), - std::ptr::null(), - std::ptr::null(), - &mut addr, - ); - - let error = std::ffi::CStr::from_ptr(gai_strerror(getaddrinfo_status)); - assert_eq!( - getaddrinfo_status, - Eai::NoName.0, - "Should have gotten no hostname (NXDOMAIN), instead got {}", - error.to_str().unwrap() - ); - freeaddrinfo(addr); - }; - - timeout(Duration::from_secs(1), dbus).await?? -} - -#[tokio::test] -#[ignore] -async fn wikipedia_is_unrestricted() -> Result<()> { - let dbus = common::mock_dbus(HashMap::from([( - getuid(), - vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], - )])); - - let (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?; - assert_eq!(system_addr, our_addr); - timeout(Duration::from_secs(1), dbus).await?? -} - -#[tokio::test] -#[ignore] -async fn adultsite_is_restricted() -> Result<()> { - let dbus = common::mock_dbus(HashMap::from([( - getuid(), - vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()], - )])); - - let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; - assert_ne!(system_addr, our_addr); - assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); - timeout(Duration::from_secs(1), dbus).await?? -} - -#[tokio::test] -#[ignore] -async fn privileged_user_bypasses_restrictions() -> Result<()> { - let dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])])); - - let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; - assert_eq!(system_addr, our_addr); - timeout(Duration::from_secs(1), dbus).await?? } diff --git a/wrapper.hpp b/wrapper.hpp index 2bd13ab..2755427 100644 --- a/wrapper.hpp +++ b/wrapper.hpp @@ -14,6 +14,9 @@ enum class HErrno { TryAgain = TRY_AGAIN, NoRecovery = NO_RECOVERY, NoData = NO_DATA, + #ifdef __USE_MISC + Internal = NETDB_INTERNAL, + #endif }; enum class EaiRetcode {