// Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! Network namespace tests for Linux. //! //! These tests verify MTU detection behavior in complex networking scenarios //! using Linux network namespaces. They require root privileges and will //! skip automatically if not running as root. //! //! Run with: `sudo -E cargo test --package mtu --test netns` #![cfg(target_os = "linux")] #![expect(clippy::unwrap_used, reason = "OK in tests.")] use std::{ fs::File, io, net::{IpAddr, Ipv4Addr, Ipv6Addr}, os::fd::AsRawFd, process::{Command, Stdio}, sync::{ Once, atomic::{AtomicU32, Ordering}, }, }; use mtu::interface_and_mtu; static COUNTER: AtomicU32 = AtomicU32::new(0); static SKIP_MESSAGE: Once = Once::new(); fn unique_name(prefix: &str) -> String { let id = COUNTER.fetch_add(1, Ordering::Relaxed); format!("{prefix}_{}_{id}", std::process::id()) } fn is_root() -> bool { unsafe { libc::geteuid() == 0 } } fn ip(args: &[&str]) -> bool { Command::new("ip") .args(args) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .is_ok_and(|s| s.success()) } fn ip_netns(ns: &str, args: &[&str]) -> bool { let mut full_args = vec!["netns", "exec", ns, "ip"]; full_args.extend(args); ip(&full_args) } fn ip_netns_output(ns: &str, args: &[&str]) -> Option { let mut full_args = vec!["netns", "exec", ns, "ip"]; full_args.extend(args); Command::new("ip") .args(&full_args) .stderr(Stdio::null()) .output() .ok() .filter(|o| o.status.success()) .and_then(|o| String::from_utf8(o.stdout).ok()) } macro_rules! require_root { () => { if !is_root() { SKIP_MESSAGE.call_once(|| { eprintln!( "Skipping network namespace tests: not running as root.\n\ Run with: sudo -E cargo test --package mtu --test netns" ); }); return; } }; } /// RAII guard for a network namespace. struct NetNs { name: String, } impl NetNs { fn new(name: &str) -> Option { ip(&["netns", "add", name]).then(|| Self { name: name.into() }) } /// Run a closure inside this namespace. fn run(&self, f: F) -> io::Result where F: FnOnce() -> T, { // Open current namespace to restore later. let orig = File::open("/proc/self/ns/net")?; // Open target namespace. let path = format!("/var/run/netns/{}", self.name); let target = File::open(&path)?; // Enter target namespace. if unsafe { libc::setns(target.as_raw_fd(), libc::CLONE_NEWNET) } != 0 { return Err(io::Error::last_os_error()); } let result = f(); // Restore original namespace. if unsafe { libc::setns(orig.as_raw_fd(), libc::CLONE_NEWNET) } != 0 { return Err(io::Error::last_os_error()); } Ok(result) } } impl Drop for NetNs { fn drop(&mut self) { ip(&["netns", "delete", &self.name]); } } /// RAII guard for a veth pair. struct VethPair(String); impl VethPair { fn new(name: &str, peer: &str) -> Option { ip(&["link", "add", name, "type", "veth", "peer", "name", peer]).then(|| Self(name.into())) } } impl Drop for VethPair { fn drop(&mut self) { ip(&["link", "delete", &self.0]); } } /// A test network with a namespace and connected interfaces. struct TestNet { ns: Option, _veths: (Option, Option), } impl TestNet { /// Create a network with one interface pair. fn new(inside_ip: &str, outside_ip: &str, mtu: u32) -> Option { let name = unique_name("ns"); let vi = unique_name("vi"); let vo = unique_name("vo"); let mtu_s = mtu.to_string(); let ns = NetNs::new(&name)?; let veth = VethPair::new(&vi, &vo)?; ip(&["link", "set", &vi, "netns", &name]); ip_netns(&name, &["addr", "add", inside_ip, "dev", &vi]); ip_netns(&name, &["link", "set", &vi, "mtu", &mtu_s]); ip_netns(&name, &["link", "set", &vi, "up"]); ip_netns(&name, &["link", "set", "lo", "up"]); ip(&["addr", "add", outside_ip, "dev", &vo]); ip(&["link", "set", &vo, "mtu", &mtu_s]); ip(&["link", "set", &vo, "up"]); Some(Self { ns: Some(ns), _veths: (Some(veth), None), }) } /// Create a network with two interface pairs (different MTUs). fn dual( ip1_in: &str, ip1_out: &str, mtu1: u32, ip2_in: &str, ip2_out: &str, mtu2: u32, ) -> Option { let name = unique_name("ns"); let vi1 = unique_name("v1i"); let vo1 = unique_name("v1o"); let vi2 = unique_name("v2i"); let vo2 = unique_name("v2o"); let ns = NetNs::new(&name)?; let veth1 = VethPair::new(&vi1, &vo1)?; let veth2 = VethPair::new(&vi2, &vo2)?; ip(&["link", "set", &vi1, "netns", &name]); ip(&["link", "set", &vi2, "netns", &name]); ip_netns(&name, &["addr", "add", ip1_in, "dev", &vi1]); ip_netns(&name, &["link", "set", &vi1, "mtu", &mtu1.to_string()]); ip_netns(&name, &["link", "set", &vi1, "up"]); ip_netns(&name, &["addr", "add", ip2_in, "dev", &vi2]); ip_netns(&name, &["link", "set", &vi2, "mtu", &mtu2.to_string()]); ip_netns(&name, &["link", "set", &vi2, "up"]); ip_netns(&name, &["link", "set", "lo", "up"]); ip(&["addr", "add", ip1_out, "dev", &vo1]); ip(&["link", "set", &vo1, "up"]); ip(&["addr", "add", ip2_out, "dev", &vo2]); ip(&["link", "set", &vo2, "up"]); Some(Self { ns: Some(ns), _veths: (Some(veth1), Some(veth2)), }) } /// Create a loopback-only namespace. fn loopback() -> Option { let name = unique_name("ns"); let ns = NetNs::new(&name)?; ip_netns(&name, &["link", "set", "lo", "up"]); Some(Self { ns: Some(ns), _veths: (None, None), }) } /// Look up interface and MTU for a destination. fn lookup(&self, dest: IpAddr) -> io::Result<(String, usize)> { self.ns .as_ref() .expect("namespace exists") .run(|| interface_and_mtu(dest))? } } #[test] fn custom_mtu_v4() { require_root!(); let net = TestNet::new("10.0.0.1/24", "10.0.0.2/24", 1400).unwrap(); let (_, mtu) = net.lookup(Ipv4Addr::new(10, 0, 0, 2).into()).unwrap(); assert_eq!(mtu, 1400); } #[test] fn custom_mtu_v6() { require_root!(); let net = TestNet::new("fd00::1/64", "fd00::2/64", 1280).unwrap(); let (_, mtu) = net .lookup("fd00::2".parse::().unwrap().into()) .unwrap(); assert_eq!(mtu, 1280); } #[test] fn jumbo_mtu() { require_root!(); let net = TestNet::new("10.1.0.1/24", "10.1.0.2/24", 9000).unwrap(); let (_, mtu) = net.lookup(Ipv4Addr::new(10, 1, 0, 2).into()).unwrap(); assert_eq!(mtu, 9000); } #[test] fn multiple_interfaces() { require_root!(); let net = TestNet::dual( "10.10.0.1/24", "10.10.0.2/24", 1500, "10.20.0.1/24", "10.20.0.2/24", 9000, ) .unwrap(); let (_, mtu1) = net.lookup(Ipv4Addr::new(10, 10, 0, 2).into()).unwrap(); assert_eq!(mtu1, 1500); let (_, mtu2) = net.lookup(Ipv4Addr::new(10, 20, 0, 2).into()).unwrap(); assert_eq!(mtu2, 9000); } #[test] fn loopback_v4() { require_root!(); let net = TestNet::loopback().unwrap(); let (iface, mtu) = net.lookup(Ipv4Addr::LOCALHOST.into()).unwrap(); assert_eq!(iface, "lo"); assert_eq!(mtu, 65536); } #[test] fn loopback_v6() { require_root!(); let net = TestNet::loopback().unwrap(); let (iface, mtu) = net.lookup(Ipv6Addr::LOCALHOST.into()).unwrap(); assert_eq!(iface, "lo"); assert_eq!(mtu, 65536); } /// Verify that MTU lookup respects policy routing rules. #[test] fn policy_routing() { require_root!(); let name = unique_name("ns"); let vi_main = unique_name("vmi"); let vo_main = unique_name("vmo"); let vi_vpn = unique_name("vvi"); let vo_vpn = unique_name("vvo"); let ns = NetNs::new(&name).unwrap(); let _veth_main = VethPair::new(&vi_main, &vo_main).unwrap(); let _veth_vpn = VethPair::new(&vi_vpn, &vo_vpn).unwrap(); ip(&["link", "set", &vi_main, "netns", &name]); ip(&["link", "set", &vi_vpn, "netns", &name]); // Main interface: 10.0.1.0/24, MTU 1500. ip_netns(&name, &["addr", "add", "10.0.1.1/24", "dev", &vi_main]); ip_netns(&name, &["link", "set", &vi_main, "mtu", "1500"]); ip_netns(&name, &["link", "set", &vi_main, "up"]); // VPN interface: 10.0.2.0/24, MTU 1400. ip_netns(&name, &["addr", "add", "10.0.2.1/24", "dev", &vi_vpn]); ip_netns(&name, &["link", "set", &vi_vpn, "mtu", "1400"]); ip_netns(&name, &["link", "set", &vi_vpn, "up"]); ip_netns(&name, &["link", "set", "lo", "up"]); // Outside peers. ip(&["addr", "add", "10.0.1.2/24", "dev", &vo_main]); ip(&["link", "set", &vo_main, "mtu", "1500"]); ip(&["link", "set", &vo_main, "up"]); ip(&["addr", "add", "10.0.2.2/24", "dev", &vo_vpn]); ip(&["link", "set", &vo_vpn, "mtu", "1400"]); ip(&["link", "set", &vo_vpn, "up"]); // Route in main table via main interface. ip_netns( &name, &[ "route", "add", "10.30.0.0/24", "via", "10.0.1.2", "dev", &vi_main, ], ); // Route in table 100 via VPN interface. ip_netns( &name, &[ "route", "add", "10.30.0.0/24", "via", "10.0.2.2", "dev", &vi_vpn, "table", "100", ], ); // Policy rule: use table 100. ip_netns( &name, &["rule", "add", "from", "all", "lookup", "100", "prio", "100"], ); // Verify policy routing is active. let route = ip_netns_output(&name, &["route", "get", "10.30.0.1"]).unwrap(); assert!( route.contains("via 10.0.2.2"), "Policy routing not active: {route}" ); // Test MTU lookup. let (_, mtu) = ns .run(|| interface_and_mtu(Ipv4Addr::new(10, 30, 0, 1).into())) .unwrap() .unwrap(); assert_eq!(mtu, 1400, "Should return VPN interface MTU"); // Cleanup policy rule (namespace deletion handles the rest). ip_netns( &name, &["rule", "del", "from", "all", "lookup", "100", "prio", "100"], ); }