use std::{
io::Read,
net::{Ipv4Addr, Ipv6Addr},
process::Command,
str::FromStr,
};
const UDHCPD_CONF: &str = "/tmp/mozim_test_udhcpd.conf";
const UDHCPD_PID_FILE_PATH: &str = "/tmp/mozim_test_udhcpd.pid";
const PID_FILE_PATH: &str = "/tmp/mozim_test_dnsmasq_pid";
const TEST_DHCPD_NETNS: &str = "mozim_test";
const LOG_FILE: &str = "/tmp/mozim_test_dnsmasq_log";
pub(crate) const TEST_NIC_CLI: &str = "dhcpcli";
const TEST_NIC_CLI_MAC: &str = "00:23:45:67:89:1a";
pub(crate) const TEST_PROXY_MAC1: &str = "00:11:22:33:44:55";
const TEST_NIC_SRV: &str = "dhcpsrv";
const TEST_DHCP_SRV_IP: &str = "192.0.2.1";
pub(crate) const TEST_DHCP_SRV_ADDR: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
const TEST_DHCP_SRV_IPV6: &str = "2001:db8:a::1";
pub(crate) const TEST_CLS_DST: Ipv4Addr = Ipv4Addr::new(203, 0, 113, 0);
pub(crate) const TEST_CLS_DST_LEN: u8 = 24;
pub(crate) const TEST_CLS_RT_ADDR: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 40);
pub(crate) const FOO1_HOSTNAME: &str = "foo1";
pub(crate) const FOO1_CLIENT_ID: &str =
"0123456789123456012345678912345601234567891234560123456789123456";
pub(crate) const FOO1_STATIC_IP: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 99);
pub(crate) const FOO1_STATIC_IPV6: Ipv6Addr =
Ipv6Addr::new(0x2001, 0xdb8, 0xa, 0x0, 0x0, 0x0, 0x0, 0x99);
pub(crate) const FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID: Ipv4Addr =
Ipv4Addr::new(192, 0, 2, 96);
pub(crate) const TEST_PROXY_IP1: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 51);
fn create_test_net_namespace() {
run_cmd(&format!("ip netns add {TEST_DHCPD_NETNS}"));
}
fn remove_test_net_namespace() {
run_cmd_ignore_failure(&format!("ip netns del {TEST_DHCPD_NETNS}"));
}
fn create_test_veth_nics() {
run_cmd(&format!(
"ip link add {TEST_NIC_CLI} address {TEST_NIC_CLI_MAC} type veth peer \
name {TEST_NIC_SRV}"
));
run_cmd(&format!("ip link set {TEST_NIC_CLI} up"));
run_cmd(&format!(
"ip link set {TEST_NIC_SRV} netns {TEST_DHCPD_NETNS}"
));
run_cmd(&format!(
"ip netns exec {TEST_DHCPD_NETNS} ip link set {TEST_NIC_SRV} up",
));
run_cmd(&format!(
"ip netns exec {TEST_DHCPD_NETNS} ip addr add {TEST_DHCP_SRV_IP}/24 \
dev {TEST_NIC_SRV}",
));
run_cmd(&format!(
"ip netns exec {TEST_DHCPD_NETNS} ip addr add {TEST_DHCP_SRV_IPV6}/64 \
dev {TEST_NIC_SRV}",
));
std::thread::sleep(std::time::Duration::from_secs(2));
}
fn remove_test_veth_nics() {
run_cmd_ignore_failure(&format!("ip link del {TEST_NIC_CLI}"));
}
fn start_dhcp_server() {
run_cmd(&format!("rm {LOG_FILE}"));
run_cmd(&format!("touch {LOG_FILE}"));
run_cmd(&format!("chmod 666 {LOG_FILE}"));
let dnsmasq_opts = format!(
r#"
--pid-file={PID_FILE_PATH}
--log-queries
--log-dhcp
--log-debug
--log-facility=/tmp/mozim_test_dnsmasq_log
--conf-file=/dev/null
--dhcp-leasefile=/tmp/mozim_test_dhcpd_lease
--no-hosts
--dhcp-host=id:{FOO1_CLIENT_ID},{FOO1_STATIC_IP},{FOO1_HOSTNAME}
--dhcp-host=id:00:03:00:01:{TEST_NIC_CLI_MAC},[{FOO1_STATIC_IPV6}],{FOO1_HOSTNAME}
--dhcp-host=id:{FOO1_HOSTNAME},{FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID}
--dhcp-host={TEST_PROXY_MAC1},{TEST_PROXY_IP1}
--dhcp-option=option:dns-server,8.8.8.8,1.1.1.1
--dhcp-option=option:mtu,1492
--dhcp-option=option:domain-name,example.com
--dhcp-option=option:ntp-server,192.0.2.1
--dhcp-option=121,{TEST_CLS_DST}/{TEST_CLS_DST_LEN},{TEST_CLS_RT_ADDR}
--dhcp-option=249,{TEST_CLS_DST}/{TEST_CLS_DST_LEN},{TEST_CLS_RT_ADDR}
--bind-interfaces
--except-interface=lo
--clear-on-reload
--interface=dhcpsrv
--dhcp-range=192.0.2.2,192.0.2.50,60
--dhcp-range=2001:db8:a::2,2001:db8:a::ff,64,2m
--no-ping
"#
);
let cmd = format!(
"ip netns exec {} dnsmasq {}",
TEST_DHCPD_NETNS,
dnsmasq_opts.replace('\n', " ")
);
let cmds: Vec<&str> = cmd.split(' ').collect();
Command::new(cmds[0])
.args(&cmds[1..])
.spawn()
.expect("Failed to start DHCP server")
.wait()
.ok();
std::thread::sleep(std::time::Duration::from_secs(1));
}
fn stop_dhcp_server() {
if !std::path::Path::new(PID_FILE_PATH).exists() {
return;
}
let mut fd = std::fs::File::open(PID_FILE_PATH)
.unwrap_or_else(|_| panic!("Failed to open {PID_FILE_PATH} file"));
let mut contents = String::new();
fd.read_to_string(&mut contents)
.unwrap_or_else(|_| panic!("Failed to read {PID_FILE_PATH} file"));
let pid = u32::from_str(contents.trim())
.unwrap_or_else(|_| panic!("Invalid PID content {contents}"));
run_cmd_ignore_failure(&format!("kill {pid}"));
}
fn write_udhcpd_conf() {
std::fs::write(
UDHCPD_CONF,
format!(
r#"
start 192.0.2.100
end 192.0.2.100
interface {TEST_NIC_SRV}
option subnet 255.255.255.0
option router {TEST_DHCP_SRV_IP}
option dns 8.8.8.8
lease_file /tmp/mozim_test_udhcpd.leases
pidfile /tmp/mozim_test_udhcpd.pid
opt lease 10
no_ping
"#
),
)
.unwrap();
}
fn start_udhcpd() {
write_udhcpd_conf();
Command::new("ip")
.args([
"netns",
"exec",
TEST_DHCPD_NETNS,
"busybox",
"udhcpd",
UDHCPD_CONF,
])
.spawn()
.expect("Failed to start udhcpd")
.wait()
.ok();
std::thread::sleep(std::time::Duration::from_secs(1));
}
fn stop_udhcpd() {
if !std::path::Path::new(UDHCPD_PID_FILE_PATH).exists() {
log::warn!("PID file {UDHCPD_PID_FILE_PATH} does not exist");
return;
}
let mut fd =
std::fs::File::open(UDHCPD_PID_FILE_PATH).unwrap_or_else(|_| {
panic!("Failed to open {UDHCPD_PID_FILE_PATH} file")
});
let mut contents = String::new();
fd.read_to_string(&mut contents).unwrap_or_else(|_| {
panic!("Failed to read {UDHCPD_PID_FILE_PATH} file")
});
let pid = u32::from_str(contents.trim())
.unwrap_or_else(|_| panic!("Invalid PID content {contents}"));
run_cmd_ignore_failure(&format!("kill {pid}"));
run_cmd_ignore_failure("killall udhcpd");
}
fn run_cmd(cmd: &str) -> String {
let cmds: Vec<&str> = cmd.split(' ').collect();
String::from_utf8(
Command::new(cmds[0])
.args(&cmds[1..])
.output()
.unwrap_or_else(|_| panic!("failed to execute command {cmd}"))
.stdout,
)
.expect("Failed to convert file command output to String")
}
fn run_cmd_ignore_failure(cmd: &str) -> String {
let cmds: Vec<&str> = cmd.split(' ').collect();
match Command::new(cmds[0]).args(&cmds[1..]).output() {
Ok(o) => String::from_utf8(o.stdout).unwrap_or_default(),
Err(e) => {
eprintln!("Failed to execute command {cmd}: {e}");
"".to_string()
}
}
}
pub(crate) fn set_client_ip(address: Ipv4Addr) {
run_cmd(&format!("ip addr add {address}/24 dev {TEST_NIC_CLI}",));
}
pub(crate) fn with_dhcp_env<T>(test: T)
where
T: FnOnce() + std::panic::UnwindSafe,
{
create_test_net_namespace();
create_test_veth_nics();
stop_dhcp_server();
start_dhcp_server();
let result = std::panic::catch_unwind(|| {
test();
});
stop_dhcp_server();
remove_test_veth_nics();
remove_test_net_namespace();
assert!(result.is_ok())
}
pub(crate) fn with_udhcpd_env<T>(test: T)
where
T: FnOnce() + std::panic::UnwindSafe,
{
create_test_net_namespace();
create_test_veth_nics();
stop_udhcpd();
start_udhcpd();
let result = std::panic::catch_unwind(test);
stop_udhcpd();
remove_test_veth_nics();
remove_test_net_namespace();
assert!(result.is_ok());
}
pub(crate) fn init_log() {
let mut log_builder = env_logger::Builder::new();
log_builder.filter(Some("mozim"), log::LevelFilter::Trace);
log_builder.try_init().ok();
}