Use an async context for module and refactor

This commit is contained in:
Matteo Settenvini 2022-08-21 14:47:37 +02:00
parent 96663abaef
commit 516bf4000c
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
12 changed files with 542 additions and 213 deletions

View File

@ -18,17 +18,17 @@ bindgen = "0.60"
[dev-dependencies] [dev-dependencies]
futures-util = "0.3" futures-util = "0.3"
rusty-hook = "0.11" rusty-hook = "0.11"
rusty-forkfork = "0.4"
test-cdylib = "1.1" test-cdylib = "1.1"
tokio = { version = "1", features = ["rt", "sync", "macros", "time"] } tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
const_format = "0.2" const_format = "0.2"
libc = "0.2" libc = "0.2"
once_cell = "1.13" once_cell = "1.13"
gio = "0.15"
log = "0.4" log = "0.4"
nix = { version = "0.25", features = ["socket", "user", "sched"] } nix = { version = "0.25", features = ["socket", "user", "sched"] }
tokio = { version = "1", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
trust-dns-resolver = "0.21" trust-dns-resolver = "0.21"
zbus = { version = "3.0", default-features = false, features = ["tokio"] }

View File

@ -4,17 +4,11 @@
use {std::env, std::path::PathBuf}; use {std::env, std::path::PathBuf};
fn main() { fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap(); println!("cargo:rerun-if-changed=wrapper.hpp");
println!("cargo:rerun-if-changed=wrapper.h");
// Required by NSS 2 // Required by NSS 2
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libnss_malcontent.so.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() let bindings = bindgen::Builder::default()
.header("wrapper.hpp") .header("wrapper.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks)) .parse_callbacks(Box::new(bindgen::CargoCallbacks))

23
src/gethostbyaddr.rs Normal file
View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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!()
}

117
src/gethostbyname.rs Normal file
View File

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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!()
}

99
src/helpers.rs Normal file
View File

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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<std::io::Result<tokio::runtime::Handle>> = 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::<gaih_addrtuple>();
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::<libc::c_char>());
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::<gaih_addrtuple>());
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<T>(ptr: *mut T, val: T) {
if !ptr.is_null() {
unsafe { *ptr = val };
}
}
pub fn block_on<F>(f: F) -> Result<F::Output>
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),
}
}

View File

@ -6,6 +6,9 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
mod constants; mod constants;
mod gethostbyaddr;
mod gethostbyname;
mod helpers;
pub mod nss_api; pub mod nss_api;
mod nss_bindings;
mod policy_checker; mod policy_checker;
mod utils;

View File

@ -1,75 +1,73 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu> // SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use { use {
crate::policy_checker::{MalcontentPolicyChecker, PolicyChecker as _}, crate::gethostbyaddr::gethostbyaddr2_r,
crate::utils::set_if_valid, 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}, libc::{hostent, size_t, socklen_t, AF_INET},
nix::errno, nix::errno,
nix::errno::Errno, nix::errno::Errno,
std::ffi::CStr,
std::os::raw::{c_char, c_int, c_void}, std::os::raw::{c_char, c_int, c_void},
std::ptr, 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 --------------- // -------------- by host ---------------
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r( pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r(
name: *const c_char, name: *const c_char,
_pat: *mut *mut gaih_addrtuple, pat: *mut *mut gaih_addrtuple,
_buffer: *mut c_char, buffer: *mut c_char,
_buflen: size_t, buflen: size_t,
errnop: *mut Errno, errnop: *mut Errno,
h_errnop: *mut HErrno, h_errnop: *mut HErrno,
_ttlp: *mut i32, ttlp: *mut i32,
) -> nss_status { ) -> 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(errnop, errno::from_i32(0));
set_if_valid(h_errnop, HErrno::HostNotFound); set_if_valid(h_errnop, HErrno::Success);
return nss_status::NSS_STATUS_SUCCESS;
}
match resolver.lookup_ip(name.to_str().unwrap()) { match block_on(async {
Ok(_result) => { gethostbyname4_r(name, pat, buffer, buflen, errnop, h_errnop, ttlp).await
// TODO }) {
// https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/posix/getaddrinfo.c;h=fed2d3bf130b6fc88e5c6840804bbadb8fe9a98b;hb=4ab2ab03d4351914ee53248dc5aef4a8c88ff8b9 Ok(status) => status,
// https://chromium.googlesource.com/chromiumos/third_party/glibc/+/cvs/fedora-glibc-2_8_90-1/nss/nss_files/files-hosts.c Err(runtime_error) => {
// https://github.com/systemd/systemd/blob/d5548eb618d426a3952746eb3a0e7d95d186ea7e/src/nss-resolve/nss-resolve.c log::error!("gethostbyname4_r: {}", runtime_error);
set_if_valid(h_errnop, HErrno::Internal);
nss_status::NSS_STATUS_SUCCESS nss_status::NSS_STATUS_TRYAGAIN
} }
Err(_) => nss_status::NSS_STATUS_UNAVAIL,
}
}
Err(_err) => nss_status::NSS_STATUS_UNAVAIL,
} }
} }
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r( pub unsafe extern "C" fn _nss_malcontent_gethostbyname3_r(
_name: *const c_char, name: *const c_char,
_af: c_int, af: c_int,
_host: *mut hostent, host: *mut hostent,
_buffer: *mut c_char, buffer: *mut c_char,
_buflen: size_t, buflen: size_t,
_errnop: *mut Errno, errnop: *mut Errno,
_h_errnop: *mut HErrno, h_errnop: *mut HErrno,
_ttlp: *mut i32, ttlp: *mut i32,
_canonp: *mut *mut char, canonp: *mut *mut char,
) -> nss_status { ) -> 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] #[no_mangle]
@ -121,17 +119,29 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname_r(
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r( pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr2_r(
_addr: *const c_void, addr: *const c_void,
_len: socklen_t, len: socklen_t,
_af: c_int, af: c_int,
_host: *mut hostent, host: *mut hostent,
_buffer: *mut c_char, buffer: *mut c_char,
_buflen: size_t, buflen: size_t,
_errnop: *mut Errno, errnop: *mut Errno,
_h_errnop: *mut HErrno, h_errnop: *mut HErrno,
_ttlp: *mut i32, ttlp: *mut i32,
) -> nss_status { ) -> 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] #[no_mangle]

View File

@ -1,8 +1,5 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu> // SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
pub fn set_if_valid<T>(ptr: *mut T, val: T) { #![allow(dead_code)]
if !ptr.is_null() { include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
unsafe { *ptr = val };
}
}

View File

@ -9,15 +9,18 @@ use {
std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
std::sync::{Arc, RwLock}, std::sync::{Arc, RwLock},
trust_dns_resolver::config as dns_config, trust_dns_resolver::config as dns_config,
trust_dns_resolver::Resolver, trust_dns_resolver::TokioAsyncResolver,
}; };
pub type Restrictions<'a> = &'a [IpAddr]; pub type Restrictions<'a> = &'a [IpAddr];
pub trait PolicyChecker { pub trait PolicyChecker {
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>>; 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(|| { static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
vec![ vec![
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)), IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)),
@ -27,10 +30,9 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
] ]
}); });
// TODO: make unique per process
// TODO: accept notifications about config changes // TODO: accept notifications about config changes
pub struct MalcontentPolicyChecker { pub struct MalcontentPolicyChecker {
resolvers: RwLock<HashMap<Uid, Option<Arc<Resolver>>>>, resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
} }
impl MalcontentPolicyChecker { impl MalcontentPolicyChecker {
@ -53,7 +55,7 @@ impl MalcontentPolicyChecker {
} }
impl PolicyChecker for MalcontentPolicyChecker { impl PolicyChecker for MalcontentPolicyChecker {
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>> { fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<TokioAsyncResolver>>> {
let user = user.unwrap_or_else(|| getuid()); let user = user.unwrap_or_else(|| getuid());
{ {
let ro_resolvers = self.resolvers.read().unwrap(); let ro_resolvers = self.resolvers.read().unwrap();
@ -84,8 +86,10 @@ impl PolicyChecker for MalcontentPolicyChecker {
config config
}); });
let resolver = let resolver = TokioAsyncResolver::tokio(
Resolver::new(resolver_config, dns_config::ResolverOpts::default())?; resolver_config,
dns_config::ResolverOpts::default(),
)?;
Some(Arc::new(resolver)) Some(Arc::new(resolver))
} }
None => None, None => None,

View File

@ -27,38 +27,98 @@ use {
tokio::task::JoinHandle, tokio::task::JoinHandle,
}; };
pub type Eai = EaiRetcode; // 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;
static SETUP: Once = Once::new(); 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;
pub fn setup() -> Result<()> { 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")); let out_dir = PathBuf::from(env!("OUT_DIR"));
let nss_config_status = unsafe {
SETUP.call_once(|| { PREFORK.call_once(|| {
std::env::set_var("LD_LIBRARY_PATH", &out_dir);
let library_path = test_cdylib::build_current_project(); let library_path = test_cdylib::build_current_project();
let mut library_filename = library_path.file_name().unwrap().to_owned(); let mut library_filename = library_path.file_name().unwrap().to_owned();
library_filename.push(".2"); // required for NSS modules library_filename.push(".2"); // required for NSS modules
let dest = out_dir.join(library_filename); let dest = out_dir.join(library_filename);
std::fs::copy(library_path, dest).unwrap(); 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<()> {
SETUP.call_once(|| {
let nss_config_status = unsafe {
let db = CString::new("hosts").unwrap(); let db = CString::new("hosts").unwrap();
let resolvers = CString::new("malcontent dns").unwrap(); let resolvers = CString::new("malcontent dns").unwrap();
__nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr())
}; };
ensure!( if nss_config_status != 0 {
nss_config_status == 0, panic!(
"Unable to configure NSS to load module: __nss_configure_lookup() returned {}", "Unable to configure NSS to load module: __nss_configure_lookup() returned {}",
nss_config_status nss_config_status
); );
}
});
Ok(()) Ok(())
} }
pub fn system_resolve(host: &str) -> Result<IpAddr> { pub fn resolve_with_system(family: libc::c_int, host: &str) -> Result<IpAddr> {
let process = Command::new("getent").arg("hosts").arg(host).output()?; 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!( ensure!(
process.status.success(), process.status.success(),
"Failed to run getent to check host IP" "Failed to run getent to check host IP"
@ -72,29 +132,26 @@ pub fn system_resolve(host: &str) -> Result<IpAddr> {
Ok(IpAddr::from_str(&addr_string)?) Ok(IpAddr::from_str(&addr_string)?)
} }
pub fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> { pub fn resolve_with_module(family: libc::c_int, hostname: &str) -> Result<IpAddr> {
if let Some(addr) = sa.as_sockaddr_in() { assert!(
Ok(IpAddr::V4(Ipv4Addr::from(addr.ip()))) SETUP.is_completed(),
} else if let Some(addr) = sa.as_sockaddr_in6() { "Forgot to call common::setup() at beginning of test?"
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)?;
let c_hostname = CString::new(hostname).unwrap(); let c_hostname = CString::new(hostname).unwrap();
unsafe { unsafe {
let mut addr = std::ptr::null_mut(); let mut addr = std::ptr::null_mut();
let getaddrinfo_status = getaddrinfo( let hints = libc::addrinfo {
c_hostname.as_ptr(), ai_family: family,
std::ptr::null(), ai_flags: 0,
std::ptr::null(), ai_socktype: 0,
&mut addr, 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)); let error = CStr::from_ptr(gai_strerror(getaddrinfo_status));
assert_eq!( 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)) let addr_storage = SockaddrStorage::from_raw((*addr).ai_addr, Some((*addr).ai_addrlen))
.ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?; .ok_or(anyhow!("Garbled addrinfo from getaddrinfo()"))?;
let ip_from_us = convert_addrinfo(&addr_storage)?; let ip = convert_addrinfo(&addr_storage)?;
freeaddrinfo(addr); freeaddrinfo(addr);
Ok((ip_from_system, ip_from_us)) Ok(ip)
}
}
fn convert_addrinfo(sa: &SockaddrStorage) -> Result<IpAddr> {
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")
} }
} }

View File

@ -24,43 +24,23 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::new(|| {
] ]
}); });
fork_test! {
#[test] #[test]
#[ignore]
fn nss_module_is_loaded() -> Result<()> { fn nss_module_is_loaded() -> Result<()> {
common::setup()?; common::setup()?;
common::resolve_with_module(libc::AF_INET, "gnome.org")?;
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,
);
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);
};
Ok(()) Ok(())
} }
#[tokio::test] #[test]
#[ignore] fn application_dns_is_nxdomain() -> Result<()> {
async fn application_dns_is_nxdomain() -> Result<()> { common::setup()?;
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()],
)])); )]));
common::setup()?;
let hostname = std::ffi::CString::new("use-application-dns.net").unwrap(); let hostname = std::ffi::CString::new("use-application-dns.net").unwrap();
unsafe { unsafe {
let mut addr = std::ptr::null_mut(); let mut addr = std::ptr::null_mut();
@ -82,41 +62,73 @@ async fn application_dns_is_nxdomain() -> Result<()> {
}; };
timeout(Duration::from_secs(1), dbus).await?? timeout(Duration::from_secs(1), dbus).await??
})
} }
#[tokio::test] #[test]
#[ignore] fn wikipedia_is_unrestricted() -> Result<()> {
async fn wikipedia_is_unrestricted() -> Result<()> { common::setup()?;
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 (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?; 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); assert_eq!(system_addr, our_addr);
timeout(Duration::from_secs(1), dbus).await??
} }
#[tokio::test] timeout(Duration::from_secs(1), dbus).await??
#[ignore] })
async fn adultsite_is_restricted() -> Result<()> { }
#[test]
fn adultsite_is_restricted() -> Result<()> {
common::setup()?;
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 (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; 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_ne!(system_addr, our_addr);
assert_eq!(our_addr, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); 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?? timeout(Duration::from_secs(1), dbus).await??
})
} }
#[tokio::test] #[test]
#[ignore] fn privileged_user_bypasses_restrictions() -> Result<()> {
async 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 */])])); let dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; 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); assert_eq!(system_addr, our_addr);
timeout(Duration::from_secs(1), dbus).await?? }
timeout(Duration::from_secs(1), dbus).await??
})
}
} }

View File

@ -14,6 +14,9 @@ enum class HErrno {
TryAgain = TRY_AGAIN, TryAgain = TRY_AGAIN,
NoRecovery = NO_RECOVERY, NoRecovery = NO_RECOVERY,
NoData = NO_DATA, NoData = NO_DATA,
#ifdef __USE_MISC
Internal = NETDB_INTERNAL,
#endif
}; };
enum class EaiRetcode { enum class EaiRetcode {