use std::{
future::Future,
process::{Child, Command, Output},
sync::atomic::{AtomicU32, Ordering},
};
mod bridge;
mod veth;
pub use bridge::LabBridge;
pub use veth::LabVeth;
use tracing::warn;
use crate::{
Result, Route,
netlink::{AsyncProtocolInit, Connection, ProtocolState, namespace},
};
static NAMESPACE_COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_ns_name(prefix: &str) -> String {
let id = NAMESPACE_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
format!("nlink-lab-{prefix}-{pid}-{id}")
}
pub struct LabNamespace {
name: String,
}
impl LabNamespace {
pub fn new(prefix: &str) -> Result<Self> {
let name = unique_ns_name(prefix);
namespace::create(&name)?;
Ok(Self { name })
}
pub fn named(name: &str) -> Result<Self> {
namespace::create(name)?;
Ok(Self {
name: name.to_string(),
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn connection(&self) -> Result<Connection<Route>> {
namespace::connection_for(&self.name)
}
pub fn connection_for<P>(&self) -> Result<Connection<P>>
where
P: ProtocolState + Default,
{
namespace::connection_for(&self.name)
}
pub async fn connection_for_async<P>(&self) -> Result<Connection<P>>
where
P: AsyncProtocolInit,
{
namespace::connection_for_async(&self.name).await
}
pub fn spawn(&self, cmd: Command) -> Result<Child> {
namespace::spawn(&self.name, cmd)
}
pub fn spawn_output(&self, cmd: Command) -> Result<Output> {
namespace::spawn_output(&self.name, cmd)
}
pub fn exec(&self, cmd: &str, args: &[&str]) -> Result<String> {
let mut command = Command::new(cmd);
command.args(args);
let output = namespace::spawn_output(&self.name, command)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(crate::Error::InvalidMessage(format!(
"command failed: {cmd} {args:?}: {stderr}"
)));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
pub fn exec_ignore(&self, cmd: &str, args: &[&str]) {
let mut command = Command::new(cmd);
command.args(args);
let _ = namespace::spawn_output(&self.name, command);
}
pub fn connect_to(
&self,
peer_ns: &LabNamespace,
local_name: &str,
remote_name: &str,
) -> Result<()> {
let mut cmd = Command::new("ip");
cmd.args([
"link",
"add",
local_name,
"type",
"veth",
"peer",
"name",
remote_name,
]);
let output = namespace::spawn_output(&self.name, cmd)?;
if !output.status.success() {
return Err(crate::Error::InvalidMessage(
"failed to create veth pair".into(),
));
}
let mut cmd = Command::new("ip");
cmd.args(["link", "set", remote_name, "netns", &peer_ns.name]);
let output = namespace::spawn_output(&self.name, cmd)?;
if !output.status.success() {
return Err(crate::Error::InvalidMessage(
"failed to move veth peer".into(),
));
}
Ok(())
}
pub fn add_dummy(&self, name: &str) -> Result<()> {
self.exec("ip", &["link", "add", name, "type", "dummy"])?;
Ok(())
}
pub fn link_up(&self, name: &str) -> Result<()> {
self.exec("ip", &["link", "set", name, "up"])?;
Ok(())
}
pub fn add_addr(&self, dev: &str, addr: &str) -> Result<()> {
self.exec("ip", &["addr", "add", addr, "dev", dev])?;
Ok(())
}
}
impl Drop for LabNamespace {
fn drop(&mut self) {
if let Err(e) = namespace::delete(&self.name) {
warn!(
namespace = %self.name,
error = %e,
"LabNamespace::drop failed to delete namespace — may need manual cleanup via `ip netns del {}`",
self.name,
);
}
}
}
pub async fn with_namespace<F, Fut, T>(prefix: &str, f: F) -> Result<T>
where
F: FnOnce(LabNamespace) -> Fut,
Fut: Future<Output = Result<T>>,
{
let ns = LabNamespace::new(prefix)?;
f(ns).await
}
pub fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
pub fn has_module(name: &str) -> bool {
if name.is_empty() || name.contains('/') || name.contains('\0') {
return false;
}
std::path::Path::new("/sys/module").join(name).exists()
}
#[macro_export]
macro_rules! require_root {
() => {
if !$crate::lab::is_root() {
eprintln!("Skipping test: requires root");
return Ok(());
}
};
}
#[macro_export]
macro_rules! require_root_void {
() => {
if !$crate::lab::is_root() {
eprintln!("Skipping test: requires root");
return;
}
};
}
#[macro_export]
macro_rules! require_module {
($name:expr) => {
if !$crate::lab::has_module($name) {
eprintln!(
"Skipping test: kernel module '{}' not loaded or built-in",
$name
);
return Ok(());
}
};
}
#[macro_export]
macro_rules! require_module_void {
($name:expr) => {
if !$crate::lab::has_module($name) {
eprintln!(
"Skipping test: kernel module '{}' not loaded or built-in",
$name
);
return;
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unique_ns_name() {
let n1 = unique_ns_name("probe");
let n2 = unique_ns_name("probe");
assert_ne!(n1, n2);
assert!(n1.starts_with("nlink-lab-probe-"));
}
#[test]
fn has_module_returns_false_for_unknown_name() {
assert!(!has_module("nlink_definitely_not_a_real_module_xyzzy"));
}
#[test]
fn has_module_rejects_path_traversal() {
assert!(!has_module(""));
assert!(!has_module("/etc/passwd"));
assert!(!has_module("../../etc/passwd"));
assert!(!has_module("nf_conntrack/foo"));
assert!(!has_module("nf\0conntrack"));
}
}