malcontent-dns-parental-con.../tests/common.rs

183 lines
6.2 KiB
Rust

// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/constants.rs"));
use {
anyhow::{anyhow, bail, ensure, Result},
futures_util::TryStreamExt,
libc::{freeaddrinfo, gai_strerror, getaddrinfo},
nix::sys::socket::{SockaddrLike as _, SockaddrStorage},
nix::unistd::Uid,
std::collections::HashMap,
std::env,
std::ffi::{CStr, CString},
std::net::{IpAddr, Ipv4Addr, Ipv6Addr},
std::path::PathBuf,
std::process::Command,
std::str::FromStr,
std::sync::Once,
tokio::task,
tokio::task::JoinHandle,
};
pub type Eai = EaiRetcode;
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
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
);
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))
}
}
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 })
}