Start implementing and testing gethostbyname4_r

This commit is contained in:
Matteo Settenvini 2022-08-15 22:55:35 +02:00
parent 9ce503e052
commit 37ef5d4e65
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
11 changed files with 287 additions and 49 deletions

View File

@ -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)

View File

@ -29,4 +29,4 @@ version = "0.2"
[dependencies.nix]
version = "0.25"
features = []
features = ["socket", "user", "sched"]

View File

@ -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()

View File

@ -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;

View File

@ -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,

47
src/policy_checker.rs Normal file
View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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<Uid>) -> Result<Option<Restrictions<'a>>>;
}
static CLOUDFLARE_PARENTALCONTROL_ADDRS: LazyLock<Vec<IpAddr>> = 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<Uid>) -> Result<Option<Restrictions<'a>>> {
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()))
}
}

8
src/utils.rs Normal file
View File

@ -0,0 +1,8 @@
// 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 };
}
}

View File

@ -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<IpAddr> {
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<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)?;
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))
}
}

View File

@ -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(())
}

View File

@ -1,6 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <nss.h>

40
wrapper.hpp Normal file
View File

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <nss.h>
#include <netdb.h>
// 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 */
};