Move to rust stable, introduce mock for dbus in tests

This commit is contained in:
Matteo Settenvini 2022-08-18 13:59:11 +02:00
parent 37ef5d4e65
commit 96663abaef
Signed by: matteo
GPG Key ID: 8576CC1AD97D42DF
9 changed files with 234 additions and 55 deletions

View File

@ -15,7 +15,7 @@ FetchContent_Declare(
GIT_TAG v0.2.1 GIT_TAG v0.2.1
) )
set(Rust_TOOLCHAIN nightly) # set(Rust_TOOLCHAIN nightly)
FetchContent_MakeAvailable(Corrosion) FetchContent_MakeAvailable(Corrosion)
corrosion_import_crate(MANIFEST_PATH Cargo.toml) corrosion_import_crate(MANIFEST_PATH Cargo.toml)

View File

@ -12,21 +12,23 @@ license = "GPL-3.0-or-later"
crate-type = ["cdylib"] crate-type = ["cdylib"]
name = "nss_malcontent" name = "nss_malcontent"
[dev-dependencies.rusty-hook] [build-dependencies]
version = "0.11" bindgen = "0.60"
[build-dependencies.bindgen] [dev-dependencies]
version = "0.60" futures-util = "0.3"
rusty-hook = "0.11"
test-cdylib = "1.1"
tokio = { version = "1", features = ["rt", "sync", "macros", "time"] }
zbus = { version = "3.0", default-features = false, features = ["tokio"] }
[dev-dependencies.test-cdylib] [dependencies]
version = "1.1" anyhow = "1.0"
const_format = "0.2"
[dependencies.anyhow] libc = "0.2"
version = "1.0" once_cell = "1.13"
gio = "0.15"
[dependencies.libc] log = "0.4"
version = "0.2" nix = { version = "0.25", features = ["socket", "user", "sched"] }
tokio = { version = "1", features = ["rt"] }
[dependencies.nix] trust-dns-resolver = "0.21"
version = "0.25"
features = ["socket", "user", "sched"]

View File

@ -1,8 +1,6 @@
// 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
extern crate bindgen;
use {std::env, std::path::PathBuf}; use {std::env, std::path::PathBuf};
fn main() { fn main() {

13
src/constants.rs Normal file
View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
use const_format::concatcp;
#[allow(dead_code)]
const DBUS_INTERFACE: &str = "com.endlessm.ParentalControls.Dns";
#[allow(dead_code)]
const DBUS_OBJECT_PATH: &str = "com/endlessm/ParentalControls/Dns";
#[allow(dead_code)]
const DBUS_GET_RESTRICTIONS_METHOD: &str = concatcp!(DBUS_INTERFACE, ".", "GetRestrictions");

View File

@ -1,11 +1,11 @@
//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(non_upper_case_globals)] #![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)] #![allow(non_camel_case_types)]
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![feature(once_cell)]
mod constants;
pub mod nss_api; pub mod nss_api;
mod policy_checker; mod policy_checker;
mod utils; mod utils;

View File

@ -31,17 +31,27 @@ pub unsafe extern "C" fn _nss_malcontent_gethostbyname4_r(
_ttlp: *mut i32, _ttlp: *mut i32,
) -> nss_status { ) -> nss_status {
let name = CStr::from_ptr(name); let name = CStr::from_ptr(name);
let policy_checker = MalcontentPolicyChecker::new(); // TODO: make LazySync let policy_checker = MalcontentPolicyChecker::new(); // TODO: make LazySync, use RefCell for interior mutability
match policy_checker.restrictions(None) { match policy_checker.resolver(None) {
Ok(None) => nss_status::NSS_STATUS_NOTFOUND, Ok(None) => nss_status::NSS_STATUS_NOTFOUND,
Ok(Some(_addrs)) => { Ok(Some(resolver)) => {
if name == CStr::from_bytes_with_nul_unchecked(CANARY_HOSTNAME) { 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::HostNotFound);
return nss_status::NSS_STATUS_SUCCESS; return nss_status::NSS_STATUS_SUCCESS;
} }
nss_status::NSS_STATUS_UNAVAIL 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,
}
} }
Err(_err) => nss_status::NSS_STATUS_UNAVAIL, Err(_err) => nss_status::NSS_STATUS_UNAVAIL,
} }

View File

@ -4,17 +4,21 @@
use { use {
anyhow::Result, anyhow::Result,
nix::unistd::{getuid, Uid}, nix::unistd::{getuid, Uid},
std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, once_cell::sync::Lazy,
std::sync::LazyLock, std::collections::HashMap,
std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
std::sync::{Arc, RwLock},
trust_dns_resolver::config as dns_config,
trust_dns_resolver::Resolver,
}; };
type Restrictions<'a> = &'a [IpAddr]; pub type Restrictions<'a> = &'a [IpAddr];
pub trait PolicyChecker { pub trait PolicyChecker {
fn restrictions<'a>(&'a self, user: Option<Uid>) -> Result<Option<Restrictions<'a>>>; fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>>;
} }
static CLOUDFLARE_PARENTALCONTROL_ADDRS: LazyLock<Vec<IpAddr>> = LazyLock::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)),
IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)), IpAddr::V4(Ipv4Addr::new(1, 0, 0, 3)),
@ -23,19 +27,21 @@ static CLOUDFLARE_PARENTALCONTROL_ADDRS: LazyLock<Vec<IpAddr>> = LazyLock::new(|
] ]
}); });
pub struct MalcontentPolicyChecker; // TODO: make unique per process
// TODO: accept notifications about config changes
pub struct MalcontentPolicyChecker {
resolvers: RwLock<HashMap<Uid, Option<Arc<Resolver>>>>,
}
impl MalcontentPolicyChecker { impl MalcontentPolicyChecker {
pub fn new() -> Self { pub fn new() -> Self {
Self {} Self {
resolvers: RwLock::new(HashMap::new()),
}
} }
}
impl PolicyChecker for MalcontentPolicyChecker { fn restrictions<'a>(&'a self, user: Uid) -> Result<Option<Restrictions<'a>>> {
fn restrictions<'a>(&'a self, user: Option<Uid>) -> Result<Option<Restrictions<'a>>> { if user.is_root() {
let uid = user.unwrap_or_else(|| getuid());
if uid.is_root() {
return Ok(None); return Ok(None);
}; };
@ -45,3 +51,48 @@ impl PolicyChecker for MalcontentPolicyChecker {
Ok(Some(CLOUDFLARE_PARENTALCONTROL_ADDRS.as_slice())) Ok(Some(CLOUDFLARE_PARENTALCONTROL_ADDRS.as_slice()))
} }
} }
impl PolicyChecker for MalcontentPolicyChecker {
fn resolver(&self, user: Option<Uid>) -> Result<Option<Arc<Resolver>>> {
let user = user.unwrap_or_else(|| getuid());
{
let ro_resolvers = self.resolvers.read().unwrap();
if let Some(resolver) = ro_resolvers.get(&user) {
return Ok(resolver.clone());
}
}
{
let mut rw_resolvers = self.resolvers.write().unwrap();
if rw_resolvers.contains_key(&user) {
return self.resolver(Some(user));
}
let resolver = match self.restrictions(user)? {
Some(addrs) => {
let resolver_config =
addrs
.iter()
.fold(dns_config::ResolverConfig::new(), |mut config, addr| {
config.add_name_server(dns_config::NameServerConfig {
socket_addr: SocketAddr::new(*addr, 53),
protocol: dns_config::Protocol::Udp,
tls_dns_name: None,
trust_nx_responses: true,
bind_addr: None,
});
config
});
let resolver =
Resolver::new(resolver_config, dns_config::ResolverOpts::default())?;
Some(Arc::new(resolver))
}
None => None,
};
rw_resolvers.insert(user, resolver);
}
self.resolver(Some(user))
}
}

View File

@ -6,20 +6,25 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
#![allow(dead_code)] #![allow(dead_code)]
use std::str::FromStr;
include!(concat!(env!("OUT_DIR"), "/bindings.rs")); include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/constants.rs"));
use { use {
anyhow::{anyhow, bail, ensure, Result}, anyhow::{anyhow, bail, ensure, Result},
futures_util::TryStreamExt,
libc::{freeaddrinfo, gai_strerror, getaddrinfo}, libc::{freeaddrinfo, gai_strerror, getaddrinfo},
nix::sys::socket::{SockaddrLike as _, SockaddrStorage}, nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
nix::unistd::Uid,
std::collections::HashMap,
std::env, std::env,
std::ffi::{CStr, CString}, std::ffi::{CStr, CString},
std::net::{IpAddr, Ipv4Addr, Ipv6Addr}, std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
std::path::PathBuf, std::path::PathBuf,
std::process::Command, std::process::Command,
std::str::FromStr,
std::sync::Once, std::sync::Once,
tokio::task,
tokio::task::JoinHandle,
}; };
pub type Eai = EaiRetcode; pub type Eai = EaiRetcode;
@ -106,3 +111,72 @@ pub fn resolve_system_and_us(hostname: &str) -> Result<(IpAddr, IpAddr)> {
Ok((ip_from_system, ip_from_us)) Ok((ip_from_system, ip_from_us))
} }
} }
pub type Restrictions = Vec<IpAddr>;
pub fn mock_dbus(responses: HashMap<Uid, Vec<Restrictions>>) -> JoinHandle<Result<()>> {
async fn dbus_loop(mut responses: HashMap<Uid, Vec<Restrictions>>) -> Result<()> {
use zbus::MessageType::*;
let mut responses_size: usize = responses.values().map(|v| v.len()).sum();
for r in responses.values_mut() {
r.reverse(); // we pop responses from the back, so...
}
let connection = zbus::ConnectionBuilder::session()?.build().await?;
let mut stream = zbus::MessageStream::from(&connection);
while responses_size > 0 {
let msg = stream
.try_next()
.await?
.ok_or(anyhow!("Unparseable DBus message"))?;
if msg.header()?.message_type()? == MethodCall
&& msg
.interface()
.ok_or(anyhow!("Invoked method has no interface"))?
== DBUS_INTERFACE
&& msg
.member()
.ok_or(anyhow!("Invoked method has no member"))?
== DBUS_GET_RESTRICTIONS_METHOD
{
let user_id: u32 = msg.body()?;
match responses.get_mut(&Uid::from(user_id)) {
Some(answers) => match answers.pop() {
Some(a) => {
responses_size = responses_size - 1;
let ips: Vec<String> = a.into_iter().map(|ip| ip.to_string()).collect();
connection.reply(&msg, &ips).await
}
None => {
connection
.reply_error(
&msg,
"MockExhausted",
&format!("DBus mock is saturated for user with id {}", user_id),
)
.await
}
},
None => {
connection
.reply_error(
&msg,
"MockExhausted",
&format!(
"No mocked invocations available for user with id {}",
user_id
),
)
.await
}
}?;
}
}
Ok(())
}
task::spawn(async { dbus_loop(responses).await })
}

View File

@ -7,10 +7,25 @@ use {
crate::common::Eai, crate::common::Eai,
anyhow::Result, anyhow::Result,
libc::{freeaddrinfo, gai_strerror, getaddrinfo}, libc::{freeaddrinfo, gai_strerror, getaddrinfo},
std::net::{IpAddr, Ipv4Addr}, nix::unistd::getuid,
once_cell::sync::Lazy,
std::collections::HashMap,
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
std::time::Duration,
tokio::time::timeout,
}; };
static CLOUDFLARE_PARENTALCONTROL_ADDRS: Lazy<Vec<IpAddr>> = Lazy::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)),
]
});
#[test] #[test]
#[ignore]
fn nss_module_is_loaded() -> Result<()> { fn nss_module_is_loaded() -> Result<()> {
common::setup()?; common::setup()?;
@ -37,10 +52,15 @@ fn nss_module_is_loaded() -> Result<()> {
Ok(()) Ok(())
} }
#[test] #[tokio::test]
fn application_dns_is_nxdomain() -> Result<()> { #[ignore]
common::setup()?; 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(); 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();
@ -60,32 +80,43 @@ fn application_dns_is_nxdomain() -> Result<()> {
); );
freeaddrinfo(addr); freeaddrinfo(addr);
}; };
Ok(())
timeout(Duration::from_secs(1), dbus).await??
} }
#[test] #[tokio::test]
#[ignore]
async fn wikipedia_is_unrestricted() -> Result<()> {
let dbus = common::mock_dbus(HashMap::from([(
getuid(),
vec![CLOUDFLARE_PARENTALCONTROL_ADDRS.clone()],
)]));
fn wikipedia_is_unrestricted() -> Result<()> {
let (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?; let (system_addr, our_addr) = common::resolve_system_and_us("wikipedia.org")?;
assert_eq!(system_addr, our_addr); assert_eq!(system_addr, our_addr);
Ok(()) timeout(Duration::from_secs(1), dbus).await??
} }
#[test] #[tokio::test]
#[ignore] #[ignore]
fn adultsite_is_restricted() -> Result<()> { 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")?; let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?;
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::new(0, 0, 0, 0)));
Ok(()) timeout(Duration::from_secs(1), dbus).await??
} }
#[test] #[tokio::test]
#[ignore] #[ignore]
fn root_user_bypasses_restrictions() -> Result<()> { async fn privileged_user_bypasses_restrictions() -> Result<()> {
// TODO fake root let dbus = common::mock_dbus(HashMap::from([(getuid(), vec![ /* no restriction */])]));
let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?; let (system_addr, our_addr) = common::resolve_system_and_us("pornhub.com")?;
assert_eq!(system_addr, our_addr); assert_eq!(system_addr, our_addr);
Ok(()) timeout(Duration::from_secs(1), dbus).await??
} }