Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
52 changes: 52 additions & 0 deletions examples/mozim_dhcpv4_netns.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
enable_log();

let args: Vec<String> = std::env::args().collect();
if args.len() != 5 {
let msg = format!(
"Usage: {} <iface> <proxy-mac> <iface-index> <netns-path>",
args[0],
);
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidInput, msg).into()
);
}

let iface_index = args[3].parse::<u32>()?;
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()));
Comment on lines +21 to +23

@cathay4t cathay4t May 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this example is wrong.

The iface_index is in host network namespace which does not exist in container's namespace. This socket binding will fail after we switched to container namespace.

The whole point of DHCP proxy in container usage is running DHCP process in host network namespace.

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();
}
1 change: 1 addition & 0 deletions src/dhcpv4/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ impl DhcpV4Client {
self.config.iface_name.as_str(),
lease.yiaddr,
lease.siaddr,
self.config.socket_netns_path.as_deref(),
)
.await?,
);
Expand Down
8 changes: 8 additions & 0 deletions src/dhcpv4/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// MAC address of interface or proxy.
pub(crate) src_mac: [u8; ETH_ALEN],
pub(crate) client_id: Vec<u8>,
Expand All @@ -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(),
Expand Down Expand Up @@ -64,6 +67,11 @@ impl DhcpV4Config {
self
}

pub fn set_socket_netns_path(&mut self, path: Option<String>) -> &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)
Expand Down
129 changes: 108 additions & 21 deletions src/dhcpv4/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -96,21 +99,7 @@ pub(crate) struct DhcpRawSocket {

impl DhcpRawSocket {
pub(crate) fn new(config: &DhcpV4Config) -> Result<Self, DhcpError> {
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 {
Expand All @@ -119,6 +108,34 @@ impl DhcpRawSocket {
}
}

fn create_bound_raw_eth_socket(
config: &DhcpV4Config,
) -> Result<OwnedFd, DhcpError> {
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
Expand Down Expand Up @@ -237,15 +254,16 @@ impl DhcpUdpV4Socket {
iface_name: &str,
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
socket_netns_path: Option<&str>,
) -> Result<Self, DhcpError> {
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 })
Expand Down Expand Up @@ -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<StdUdpSocket, DhcpError> {
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)?;

Expand All @@ -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::<CString>() as libc::socklen_t,
iface_name_cstr.as_bytes_with_nul().len() as libc::socklen_t,
);
if rc != 0 {
return Err(DhcpError::new(
Expand All @@ -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<Self, DhcpError> {
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}");
}
}
}
46 changes: 46 additions & 0 deletions src/integ_tests/dhcpv4_netns.rs
Original file line number Diff line number Diff line change
@@ -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<DhcpV4Lease> {
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
}
Loading
Loading