Use an async context for module and refactor
This commit is contained in:
parent
96663abaef
commit
516bf4000c
|
@ -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"] }
|
||||
|
|
8
build.rs
8
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))
|
||||
|
|
|
@ -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!()
|
||||
}
|
|
@ -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!()
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
120
src/nss_api.rs
120
src/nss_api.rs
|
@ -1,75 +1,73 @@
|
|||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// 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]
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub fn set_if_valid<T>(ptr: *mut T, val: T) {
|
||||
if !ptr.is_null() {
|
||||
unsafe { *ptr = val };
|
||||
}
|
||||
}
|
||||
#![allow(dead_code)]
|
||||
include!(concat!(env!("OUT_DIR"), "/bindings.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<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(|| {
|
||||
vec![
|
||||
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
|
||||
pub struct MalcontentPolicyChecker {
|
||||
resolvers: RwLock<HashMap<Uid, Option<Arc<Resolver>>>>,
|
||||
resolvers: RwLock<HashMap<Uid, Option<Arc<TokioAsyncResolver>>>>,
|
||||
}
|
||||
|
||||
impl MalcontentPolicyChecker {
|
||||
|
@ -53,7 +55,7 @@ impl 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 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,
|
||||
|
|
155
tests/common.rs
155
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<IpAddr> {
|
||||
let process = Command::new("getent").arg("hosts").arg(host).output()?;
|
||||
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"
|
||||
|
@ -72,29 +132,26 @@ pub fn system_resolve(host: &str) -> Result<IpAddr> {
|
|||
Ok(IpAddr::from_str(&addr_string)?)
|
||||
}
|
||||
|
||||
pub 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")
|
||||
}
|
||||
}
|
||||
|
||||
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<IpAddr> {
|
||||
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<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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,99 +24,111 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = 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??
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue