diff --git a/CMakeLists.txt b/CMakeLists.txt index ac97827..ac6b576 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ FetchContent_Declare( GIT_TAG v0.2.1 ) +set(Rust_TOOLCHAIN nightly) FetchContent_MakeAvailable(Corrosion) corrosion_import_crate(MANIFEST_PATH Cargo.toml) diff --git a/Cargo.toml b/Cargo.toml index fb7a74e..28a5b2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,4 +29,4 @@ version = "0.2" [dependencies.nix] version = "0.25" -features = [] +features = ["socket", "user", "sched"] diff --git a/build.rs b/build.rs index b9fce41..8686a2a 100644 --- a/build.rs +++ b/build.rs @@ -18,10 +18,14 @@ fn main() { println!("cargo:rustc-link-search={}", &out_dir); let bindings = bindgen::Builder::default() - .header("wrapper.h") + .header("wrapper.hpp") .parse_callbacks(Box::new(bindgen::CargoCallbacks)) .newtype_enum("nss_status") .allowlist_type("nss_status") + .newtype_enum("HErrno") + .allowlist_type("HErrno") + .newtype_enum("EaiRetcode") + .allowlist_type("EaiRetcode") .allowlist_type("gaih_addrtuple") .allowlist_function("__nss_configure_lookup") .generate() diff --git a/src/lib.rs b/src/lib.rs index 218169c..aa3eb32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,5 +4,8 @@ #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] +#![feature(once_cell)] -mod nss_api; +pub mod nss_api; +mod policy_checker; +mod utils; diff --git a/src/nss_api.rs b/src/nss_api.rs index aca8d7d..465a65b 100644 --- a/src/nss_api.rs +++ b/src/nss_api.rs @@ -5,35 +5,57 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); use { - ::std::os::raw::{c_char, c_int, c_void}, + crate::policy_checker::{MalcontentPolicyChecker, PolicyChecker as _}, + crate::utils::set_if_valid, 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 extern "C" fn _nss_malcontent_gethostbyname4_r( - _name: *const c_char, +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, - _errnop: *mut c_int, - _h_errnop: *mut c_int, + errnop: *mut Errno, + h_errnop: *mut HErrno, _ttlp: *mut i32, ) -> nss_status { - todo!() + let name = CStr::from_ptr(name); + let policy_checker = MalcontentPolicyChecker::new(); // TODO: make LazySync + match policy_checker.restrictions(None) { + Ok(None) => nss_status::NSS_STATUS_NOTFOUND, + Ok(Some(_addrs)) => { + 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; + } + + nss_status::NSS_STATUS_UNAVAIL + } + Err(_err) => nss_status::NSS_STATUS_UNAVAIL, + } } #[no_mangle] -pub extern "C" fn _nss_malcontent_gethostbyname3_r( +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 c_int, - _h_errnop: *mut c_int, + _errnop: *mut Errno, + _h_errnop: *mut HErrno, _ttlp: *mut i32, _canonp: *mut *mut char, ) -> nss_status { @@ -41,14 +63,14 @@ pub extern "C" fn _nss_malcontent_gethostbyname3_r( } #[no_mangle] -pub extern "C" fn _nss_malcontent_gethostbyname2_r( +pub unsafe extern "C" fn _nss_malcontent_gethostbyname2_r( name: *const c_char, af: c_int, host: *mut hostent, buffer: *mut c_char, buflen: size_t, - errnop: *mut c_int, - h_errnop: *mut c_int, + errnop: *mut Errno, + h_errnop: *mut HErrno, ) -> nss_status { _nss_malcontent_gethostbyname3_r( name, @@ -64,13 +86,13 @@ pub extern "C" fn _nss_malcontent_gethostbyname2_r( } #[no_mangle] -pub extern "C" fn _nss_malcontent_gethostbyname_r( +pub unsafe extern "C" fn _nss_malcontent_gethostbyname_r( name: *const c_char, host: *mut hostent, buffer: *mut c_char, buflen: size_t, - errnop: *mut c_int, - h_errnop: *mut c_int, + errnop: *mut Errno, + h_errnop: *mut HErrno, ) -> nss_status { _nss_malcontent_gethostbyname3_r( name, @@ -88,30 +110,30 @@ pub extern "C" fn _nss_malcontent_gethostbyname_r( // ----------------- by addr ----------------- #[no_mangle] -pub extern "C" fn _nss_malcontent_gethostbyaddr2_r( +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 c_int, - _h_errnop: *mut c_int, + _errnop: *mut Errno, + _h_errnop: *mut HErrno, _ttlp: *mut i32, ) -> nss_status { todo!() } #[no_mangle] -pub extern "C" fn _nss_malcontent_gethostbyaddr_r( +pub unsafe extern "C" fn _nss_malcontent_gethostbyaddr_r( addr: *const c_void, len: socklen_t, af: c_int, host: *mut hostent, buffer: *mut c_char, buflen: size_t, - errnop: *mut c_int, - h_errnop: *mut c_int, + errnop: *mut Errno, + h_errnop: *mut HErrno, ) -> nss_status { _nss_malcontent_gethostbyaddr2_r( addr, diff --git a/src/policy_checker.rs b/src/policy_checker.rs new file mode 100644 index 0000000..e79181d --- /dev/null +++ b/src/policy_checker.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: GPL-3.0-or-later + +use { + anyhow::Result, + nix::unistd::{getuid, Uid}, + std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, + std::sync::LazyLock, +}; + +type Restrictions<'a> = &'a [IpAddr]; + +pub trait PolicyChecker { + fn restrictions<'a>(&'a self, user: Option) -> Result>>; +} + +static CLOUDFLARE_PARENTALCONTROL_ADDRS: LazyLock> = LazyLock::new(|| { + vec![ + IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)), + IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)), + IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1113)), + IpAddr::V6(Ipv6Addr::new(2606, 4700, 4700, 0, 0, 0, 0, 1003)), + ] +}); + +pub struct MalcontentPolicyChecker; + +impl MalcontentPolicyChecker { + pub fn new() -> Self { + Self {} + } +} + +impl PolicyChecker for MalcontentPolicyChecker { + fn restrictions<'a>(&'a self, user: Option) -> Result>> { + let uid = user.unwrap_or_else(|| getuid()); + + if uid.is_root() { + return Ok(None); + }; + + // TODO: for now, hardcoded DNS records, later go + // through D-Bus to malcontent and ask about user. + + Ok(Some(CLOUDFLARE_PARENTALCONTROL_ADDRS.as_slice())) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..1e20a67 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,8 @@ +// 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 }; + } +} diff --git a/tests/common.rs b/tests/common.rs index 4e6b9cb..813cc2c 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -6,15 +6,23 @@ #![allow(non_snake_case)] #![allow(dead_code)] +use std::str::FromStr; + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + use { - anyhow::{ensure, Result}, + anyhow::{anyhow, bail, ensure, Result}, + libc::{freeaddrinfo, gai_strerror, getaddrinfo}, + nix::sys::socket::{SockaddrLike as _, SockaddrStorage}, std::env, - std::ffi::CString, + std::ffi::{CStr, CString}, + std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, std::path::PathBuf, + std::process::Command, std::sync::Once, }; -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +pub type Eai = EaiRetcode; static SETUP: Once = Once::new(); @@ -31,7 +39,7 @@ pub fn setup() -> Result<()> { }); let db = CString::new("hosts").unwrap(); - let resolvers = CString::new("malcontent [UNAVAIL=return] dns").unwrap(); + let resolvers = CString::new("malcontent dns").unwrap(); __nss_configure_lookup(db.as_ptr(), resolvers.as_ptr()) }; @@ -43,3 +51,58 @@ pub fn setup() -> Result<()> { Ok(()) } + +pub fn system_resolve(host: &str) -> Result { + let process = Command::new("getent").arg("hosts").arg(host).output()?; + ensure!( + process.status.success(), + "Failed to run getent to check host IP" + ); + let output = String::from_utf8(process.stdout)?; + let addr_string = output + .as_str() + .split(' ') + .next() + .ok_or(anyhow!("Unparseable output from getent"))?; + 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)?; + + 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 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 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)?; + freeaddrinfo(addr); + Ok((ip_from_system, ip_from_us)) + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 493ccc5..487fe7d 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -4,32 +4,88 @@ mod common; use { - anyhow::{bail, Result}, + crate::common::Eai, + anyhow::Result, libc::{freeaddrinfo, gai_strerror, getaddrinfo}, + std::net::{IpAddr, Ipv4Addr}, }; #[test] -#[should_panic(expected = "not yet implemented")] -fn nss_module_is_loaded() { - common::setup().unwrap(); +fn nss_module_is_loaded() -> Result<()> { + common::setup()?; let hostname = std::ffi::CString::new("gnome.org").unwrap(); unsafe { let mut addr = std::ptr::null_mut(); - match getaddrinfo( + let getaddrinfo_status = getaddrinfo( hostname.as_ptr(), std::ptr::null(), std::ptr::null(), &mut addr, - ) { - 0 => freeaddrinfo(addr), - status => { - let error = std::ffi::CStr::from_ptr(gai_strerror(status)); - panic!( - "Unable to resolve hostname, getaddrinfo returned {}", - error.to_str().unwrap() - ) - } - } + ); + + 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(()) +} + +#[test] +fn application_dns_is_nxdomain() -> Result<()> { + 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); + }; + Ok(()) +} + +#[test] + +fn wikipedia_is_unrestricted() -> Result<()> { + let (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?; + assert_eq!(system_addr, our_addr); + Ok(()) +} + +#[test] +#[ignore] +fn adultsite_is_restricted() -> Result<()> { + 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))); + Ok(()) +} + +#[test] +#[ignore] +fn root_user_bypasses_restrictions() -> Result<()> { + // TODO fake root + + let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; + assert_eq!(system_addr, our_addr); + Ok(()) } diff --git a/wrapper.h b/wrapper.h deleted file mode 100644 index 2228a7c..0000000 --- a/wrapper.h +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 Matteo Settenvini - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#include \ No newline at end of file diff --git a/wrapper.hpp b/wrapper.hpp new file mode 100644 index 0000000..2bd13ab --- /dev/null +++ b/wrapper.hpp @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022 Matteo Settenvini + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +// Work around enums in netdb.h defined as macros instead :-p + +enum class HErrno { + Success = 0, + HostNotFound = HOST_NOT_FOUND, + TryAgain = TRY_AGAIN, + NoRecovery = NO_RECOVERY, + NoData = NO_DATA, +}; + +enum class EaiRetcode { + Success = 0, + BadFlags = EAI_BADFLAGS, + NoName = EAI_NONAME, + Fail = EAI_FAIL, + Family = EAI_FAMILY, + SockType = EAI_SOCKTYPE, + Service = EAI_SERVICE, + Memory = EAI_MEMORY, + System = EAI_SYSTEM, + Overflow = EAI_OVERFLOW, + #ifdef __USE_GNU + NoData = EAI_NODATA, + AddrFamily = EAI_ADDRFAMILY, + InProgress = EAI_INPROGRESS, + Canceled = EAI_CANCELED, + NotCanceled = EAI_NOTCANCELED, + AllDone = EAI_ALLDONE, + Interrupted = EAI_INTR, + IdnEncode = EAI_IDN_ENCODE, + #endif /* __USE_GNU */ +};