diff --git a/Cargo.toml b/Cargo.toml index dfc112a..10700f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" libc = "0.2.132" log = "0.4.17" etherparse = "0.19.0" -nix = { version = "0.30", features = ["poll", "time", "event", "socket", "fs"] } +nix = { version = "0.30", features = ["poll", "time", "event", "socket", "fs", "sched"] } rtnetlink = { version = "0.20.0", optional = true } tokio = { version = "1.19", features = ["macros", "rt", "net", "time"] } diff --git a/examples/mozim_dhcpv4_netns.rs b/examples/mozim_dhcpv4_netns.rs new file mode 100644 index 0000000..a2963aa --- /dev/null +++ b/examples/mozim_dhcpv4_netns.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +use mozim::{DhcpV4Client, DhcpV4Config, DhcpV4State}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + enable_log(); + + let args: Vec = std::env::args().collect(); + if args.len() != 5 { + let msg = format!( + "Usage: {} ", + args[0], + ); + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidInput, msg).into() + ); + } + + let iface_index = args[3].parse::()?; + let mut config = DhcpV4Config::new_proxy(&args[1], &args[2])?; + config.set_iface_index(iface_index); + config.set_socket_netns_path(Some(args[4].clone())); + config.set_timeout_sec(300); + + let mut cli = DhcpV4Client::init(config, None).await.unwrap(); + let mut got_lease = None; + + loop { + if let Ok(state) = cli.run().await { + println!("DHCP state {state}"); + if let DhcpV4State::Done(lease) = state { + println!("Got lease {lease:?}"); + got_lease = Some(lease); + continue; + } + if state == DhcpV4State::Rebinding { + if let Some(lease) = got_lease.as_ref() { + cli.release(lease).await?; + println!("DHCP lease released"); + return Ok(()); + } + } + } + } +} + +fn enable_log() { + env_logger::Builder::new() + .filter(Some("mozim"), log::LevelFilter::Trace) + .init(); +} diff --git a/src/dhcpv4/client.rs b/src/dhcpv4/client.rs index e496b3e..7b769c1 100644 --- a/src/dhcpv4/client.rs +++ b/src/dhcpv4/client.rs @@ -198,6 +198,7 @@ impl DhcpV4Client { self.config.iface_name.as_str(), lease.yiaddr, lease.siaddr, + self.config.socket_netns_path.as_deref(), ) .await?, ); diff --git a/src/dhcpv4/config.rs b/src/dhcpv4/config.rs index 2b79b82..8f51e25 100644 --- a/src/dhcpv4/config.rs +++ b/src/dhcpv4/config.rs @@ -12,6 +12,8 @@ pub struct DhcpV4Config { pub iface_name: String, /// Interface index to run DHCP against. pub iface_index: u32, + /// Optional Linux network namespace path used when DHCP sockets are opened. + pub socket_netns_path: Option, /// MAC address of interface or proxy. pub(crate) src_mac: [u8; ETH_ALEN], pub(crate) client_id: Vec, @@ -31,6 +33,7 @@ impl Default for DhcpV4Config { Self { iface_name: String::new(), iface_index: 0, + socket_netns_path: None, src_mac: [0u8; ETH_ALEN], client_id: Vec::new(), host_name: String::new(), @@ -64,6 +67,11 @@ impl DhcpV4Config { self } + pub fn set_socket_netns_path(&mut self, path: Option) -> &mut Self { + self.socket_netns_path = path; + self + } + pub fn set_iface_mac(&mut self, mac: &str) -> Result<&mut Self, DhcpError> { let src_mac = parse_mac(mac)?; self.set_iface_mac_raw(&src_mac) diff --git a/src/dhcpv4/socket.rs b/src/dhcpv4/socket.rs index bae39e6..a1dbd03 100644 --- a/src/dhcpv4/socket.rs +++ b/src/dhcpv4/socket.rs @@ -2,16 +2,19 @@ use std::{ ffi::CString, + fs::File, future::Future, - net::Ipv4Addr, + marker::PhantomData, + net::{Ipv4Addr, UdpSocket as StdUdpSocket}, os::{ - fd::{AsRawFd, OwnedFd}, + fd::{AsFd, AsRawFd, OwnedFd}, unix::io::RawFd, }, }; use nix::{ errno::Errno, + sched::{setns, CloneFlags}, sys::socket::{AddressFamily, MsgFlags, SockFlag, SockProtocol, SockType}, }; use tokio::{io::unix::AsyncFd, net::UdpSocket}; @@ -96,21 +99,7 @@ pub(crate) struct DhcpRawSocket { impl DhcpRawSocket { pub(crate) fn new(config: &DhcpV4Config) -> Result { - let iface_index = config.iface_index as libc::c_int; - let fd = create_raw_eth_socket()?; - - apply_dhcp_bpf(fd.as_raw_fd())?; - - bind_raw_socket( - fd.as_raw_fd(), - libc::ETH_P_ALL, - iface_index, - &config.src_mac, - )?; - - if config.is_proxy { - enable_promiscuous_mode(fd.as_raw_fd(), iface_index)?; - } + let fd = create_bound_raw_eth_socket(config)?; log::debug!("Raw socket created {}", fd.as_raw_fd()); Ok(DhcpRawSocket { @@ -119,6 +108,34 @@ impl DhcpRawSocket { } } +fn create_bound_raw_eth_socket( + config: &DhcpV4Config, +) -> Result { + let _netns_guard = + if let Some(netns_path) = config.socket_netns_path.as_deref() { + Some(NetnsGuard::enter(netns_path)?) + } else { + None + }; + let iface_index = config.iface_index as libc::c_int; + let fd = create_raw_eth_socket()?; + + apply_dhcp_bpf(fd.as_raw_fd())?; + + bind_raw_socket( + fd.as_raw_fd(), + libc::ETH_P_ALL, + iface_index, + &config.src_mac, + )?; + + if config.is_proxy { + enable_promiscuous_mode(fd.as_raw_fd(), iface_index)?; + } + + Ok(fd) +} + impl DhcpV4Socket for DhcpRawSocket { fn is_raw(&self) -> bool { true @@ -237,15 +254,16 @@ impl DhcpUdpV4Socket { iface_name: &str, src_ip: Ipv4Addr, dst_ip: Ipv4Addr, + socket_netns_path: Option<&str>, ) -> Result { log::debug!( "Creating UDP socket from {src_ip}:{} to {dst_ip}:{}", CLIENT_PORT, SERVER_PORT ); - let socket = UdpSocket::bind((src_ip, CLIENT_PORT)).await?; - bind_socket_to_iface(socket.as_raw_fd(), iface_name)?; - socket.connect((dst_ip, SERVER_PORT)).await?; + let socket = + create_udp_socket(iface_name, src_ip, dst_ip, socket_netns_path)?; + let socket = UdpSocket::from_std(socket)?; log::debug!("Finished UDP socket creation"); Ok(Self { socket }) @@ -274,6 +292,24 @@ impl DhcpV4Socket for DhcpUdpV4Socket { } } +fn create_udp_socket( + iface_name: &str, + src_ip: Ipv4Addr, + dst_ip: Ipv4Addr, + socket_netns_path: Option<&str>, +) -> Result { + let _netns_guard = if let Some(netns_path) = socket_netns_path { + Some(NetnsGuard::enter(netns_path)?) + } else { + None + }; + let socket = StdUdpSocket::bind((src_ip, CLIENT_PORT))?; + bind_socket_to_iface(socket.as_raw_fd(), iface_name)?; + socket.connect((dst_ip, SERVER_PORT))?; + socket.set_nonblocking(true)?; + Ok(socket) +} + fn bind_socket_to_iface(fd: RawFd, iface_name: &str) -> Result<(), DhcpError> { let iface_name_cstr = CString::new(iface_name)?; @@ -283,7 +319,7 @@ fn bind_socket_to_iface(fd: RawFd, iface_name: &str) -> Result<(), DhcpError> { libc::SOL_SOCKET, libc::SO_BINDTODEVICE, iface_name_cstr.as_ptr() as *const libc::c_void, - std::mem::size_of::() as libc::socklen_t, + iface_name_cstr.as_bytes_with_nul().len() as libc::socklen_t, ); if rc != 0 { return Err(DhcpError::new( @@ -298,3 +334,54 @@ fn bind_socket_to_iface(fd: RawFd, iface_name: &str) -> Result<(), DhcpError> { } Ok(()) } + +struct NetnsGuard { + host_netns: File, + // Prevent Send so this guard cannot be held across await points. + // setns() only affects the calling thread; moving across threads would + // silently switch the wrong thread's namespace. + _not_send: PhantomData<*const ()>, +} + +impl NetnsGuard { + // This switches only the current thread and must not be held across await. + fn enter(netns_path: &str) -> Result { + let host_netns = File::open("/proc/self/ns/net").map_err(|e| { + DhcpError::new( + ErrorKind::IoError, + format!("Failed to open current network namespace: {e}"), + ) + })?; + let target_netns = File::open(netns_path).map_err(|e| { + DhcpError::new( + ErrorKind::IoError, + format!( + "Failed to open socket network namespace {netns_path}: {e}" + ), + ) + })?; + + setns(target_netns.as_fd(), CloneFlags::CLONE_NEWNET).map_err(|e| { + DhcpError::new( + ErrorKind::IoError, + format!( + "Failed to join socket network namespace {netns_path}: {e}" + ), + ) + })?; + + Ok(Self { + host_netns, + _not_send: PhantomData, + }) + } +} + +impl Drop for NetnsGuard { + fn drop(&mut self) { + if let Err(e) = setns(self.host_netns.as_fd(), CloneFlags::CLONE_NEWNET) + { + log::error!("Failed to restore original network namespace: {e}"); + } + } +} diff --git a/src/integ_tests/dhcpv4_netns.rs b/src/integ_tests/dhcpv4_netns.rs new file mode 100644 index 0000000..18ded8b --- /dev/null +++ b/src/integ_tests/dhcpv4_netns.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::env::{ + init_log, test_client_iface_index, with_dhcp_client_netns_env, + TEST_DHCP_CLI_NETNS_PATH, TEST_NIC_CLI, TEST_PROXY_IP1, TEST_PROXY_MAC1, +}; +use crate::{DhcpV4Client, DhcpV4Config, DhcpV4Lease, DhcpV4State}; + +#[test] +fn test_dhcpv4_proxy_socket_netns() { + init_log(); + with_dhcp_client_netns_env(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .enable_io() + .build() + .unwrap(); + + let lease = rt.block_on(get_lease()); + + assert!(lease.is_some()); + if let Some(lease) = lease { + assert_eq!(lease.yiaddr, TEST_PROXY_IP1); + } + }) +} + +async fn get_lease() -> Option { + let mut config = + DhcpV4Config::new_proxy(TEST_NIC_CLI, TEST_PROXY_MAC1).unwrap(); + config.set_iface_index(test_client_iface_index()); + config.set_socket_netns_path(Some(TEST_DHCP_CLI_NETNS_PATH.to_string())); + config.set_timeout_sec(5); + + let mut cli = DhcpV4Client::init(config, None).await.unwrap(); + + while let Ok(state) = cli.run().await { + if let DhcpV4State::Done(lease) = state { + cli.release(&lease).await.unwrap(); + return Some(*lease); + } else { + println!("DHCP state {state}"); + } + } + None +} diff --git a/src/integ_tests/env.rs b/src/integ_tests/env.rs index d9eace4..30bc01b 100644 --- a/src/integ_tests/env.rs +++ b/src/integ_tests/env.rs @@ -9,6 +9,9 @@ use std::{ const PID_FILE_PATH: &str = "/tmp/mozim_test_dnsmasq_pid"; const TEST_DHCPD_NETNS: &str = "mozim_test"; +const TEST_DHCP_CLI_NETNS: &str = "mozim_test_client"; +pub(crate) const TEST_DHCP_CLI_NETNS_PATH: &str = + "/var/run/netns/mozim_test_client"; 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"; @@ -36,10 +39,18 @@ fn create_test_net_namespace() { run_cmd(&format!("ip netns add {TEST_DHCPD_NETNS}")); } +fn create_test_client_net_namespace() { + run_cmd(&format!("ip netns add {TEST_DHCP_CLI_NETNS}")); +} + fn remove_test_net_namespace() { run_cmd_ignore_failure(&format!("ip netns del {TEST_DHCPD_NETNS}")); } +fn remove_test_client_net_namespace() { + run_cmd_ignore_failure(&format!("ip netns del {TEST_DHCP_CLI_NETNS}")); +} + fn create_test_veth_nics() { run_cmd(&format!( "ip link add {TEST_NIC_CLI} address {TEST_NIC_CLI_MAC} type veth peer \ @@ -64,10 +75,25 @@ fn create_test_veth_nics() { std::thread::sleep(std::time::Duration::from_secs(2)); } +fn move_test_client_nic_to_namespace() { + run_cmd(&format!( + "ip link set {TEST_NIC_CLI} netns {TEST_DHCP_CLI_NETNS}", + )); + run_cmd(&format!( + "ip netns exec {TEST_DHCP_CLI_NETNS} ip link set {TEST_NIC_CLI} up", + )); +} + fn remove_test_veth_nics() { run_cmd_ignore_failure(&format!("ip link del {TEST_NIC_CLI}")); } +fn remove_test_client_veth_nics() { + run_cmd_ignore_failure(&format!( + "ip netns exec {TEST_DHCP_CLI_NETNS} ip link del {TEST_NIC_CLI}" + )); +} + fn start_dhcp_server() { run_cmd(&format!("rm {LOG_FILE}")); run_cmd(&format!("touch {LOG_FILE}")); @@ -179,6 +205,45 @@ where assert!(result.is_ok()) } +pub(crate) fn with_dhcp_client_netns_env(test: T) +where + T: FnOnce() + std::panic::UnwindSafe, +{ + stop_dhcp_server(); + remove_test_client_veth_nics(); + remove_test_veth_nics(); + remove_test_client_net_namespace(); + remove_test_net_namespace(); + + create_test_net_namespace(); + create_test_client_net_namespace(); + create_test_veth_nics(); + move_test_client_nic_to_namespace(); + stop_dhcp_server(); + start_dhcp_server(); + + let result = std::panic::catch_unwind(|| { + test(); + }); + + stop_dhcp_server(); + remove_test_client_veth_nics(); + remove_test_veth_nics(); + remove_test_client_net_namespace(); + remove_test_net_namespace(); + assert!(result.is_ok()) +} + +pub(crate) fn test_client_iface_index() -> u32 { + run_cmd(&format!( + "ip netns exec {TEST_DHCP_CLI_NETNS} cat \ + /sys/class/net/{TEST_NIC_CLI}/ifindex" + )) + .trim() + .parse() + .unwrap_or_else(|e| panic!("failed to parse {TEST_NIC_CLI} ifindex: {e}")) +} + pub(crate) fn init_log() { let mut log_builder = env_logger::Builder::new(); log_builder.filter(Some("mozim"), log::LevelFilter::Trace); diff --git a/src/integ_tests/mod.rs b/src/integ_tests/mod.rs index 1b2bcc9..7da6951 100644 --- a/src/integ_tests/mod.rs +++ b/src/integ_tests/mod.rs @@ -3,6 +3,8 @@ #[cfg(test)] mod dhcpv4; #[cfg(test)] +mod dhcpv4_netns; +#[cfg(test)] mod dhcpv4_proxy; #[cfg(test)] mod dhcpv6;