add support for moving devices into the namespace

This commit is contained in:
Robin Appelman 2026-02-14 15:59:47 +01:00
commit 3fa69dc434
11 changed files with 411 additions and 88 deletions

76
src/namespace/handle.rs Normal file
View file

@ -0,0 +1,76 @@
use crate::namespace::NamespaceEnterError;
use nix::errno::Errno;
use nix::sched::{setns, CloneFlags};
use std::fs::File;
use std::io::Error as IoError;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use std::thread::scope;
use thiserror::Error;
pub struct NamespaceHandle {
path: PathBuf,
fd: OwnedFd,
}
impl NamespaceHandle {
/// Open the namespace handle for a path
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, NamespaceHandleError> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| NamespaceHandleError::Open {
error,
path: path.into(),
})?;
Ok(NamespaceHandle {
path: path.into(),
fd: file.into(),
})
}
/// Open the namespace handle for the namespace the current process is in
pub fn parent() -> Result<Self, NamespaceHandleError> {
Self::open("/proc/self/ns/net")
}
pub fn run_in<T: Send, F: FnOnce() -> T + Send>(&self, f: F) -> Result<T, NamespaceEnterError> {
scope(|scope| {
scope
.spawn(|| {
setns(&self.fd, CloneFlags::CLONE_NEWNET)?;
Ok(f())
})
.join()
.expect("namespace thread panicked")
})
.map_err(|error| NamespaceEnterError {
namespace: self.path.clone(),
error,
})
}
}
impl AsFd for NamespaceHandle {
fn as_fd(&self) -> BorrowedFd<'_> {
self.fd.as_fd()
}
}
impl AsRawFd for NamespaceHandle {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
impl AsRawFd for &NamespaceHandle {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
#[derive(Debug, Error)]
pub enum NamespaceHandleError {
#[error("Failed to open namespace handle {}: {error:#}", path.display())]
Open { path: PathBuf, error: IoError },
#[error("Failed to enter namespace: {0:#}")]
Enter(Errno),
}

View file

@ -1,27 +1,32 @@
mod handle;
mod raw;
use crate::config::NamespaceName;
use crate::link::{LinkError, link_up_ns};
use crate::namespace::raw::{NamespaceSetupError, create_network_namespace};
use crate::config::{DeviceName, NamespaceName};
use crate::link::{link_up, move_all_links_out, move_link_into, LinkError};
pub use crate::namespace::handle::{NamespaceHandle, NamespaceHandleError};
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError};
use either::Either;
use nix::errno::Errno;
use nix::mount::{MntFlags, MsFlags, mount, umount2};
use std::fs::{File, create_dir, read_dir, remove_file};
use nix::mount::{mount, umount2, MntFlags, MsFlags};
use std::fs::{create_dir, read_dir, remove_file, File};
use std::io::{Error as IoError, ErrorKind};
use std::iter::empty;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{debug, error, info};
use tracing::{debug, info};
pub struct NetNs {
name: NamespaceName,
path: PathBuf,
nsd_path: PathBuf,
handle: NamespaceHandle,
}
impl NetNs {
pub fn existing(include_broken: bool) -> Result<impl Iterator<Item = NamespaceName>, NamespaceError> {
pub fn existing(
include_broken: bool,
) -> Result<impl Iterator<Item = NamespaceName>, NamespaceError> {
let dir = match read_dir("/var/run/netnsd") {
Ok(dir) => Ok(dir),
Err(error) if error.kind() == ErrorKind::NotFound => {
@ -71,10 +76,12 @@ impl NetNs {
})?;
}
let handle = NamespaceHandle::open(&path)?;
return Ok(NetNs {
name,
nsd_path,
path,
handle,
});
}
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
@ -89,10 +96,12 @@ impl NetNs {
path: nsd_path.clone(),
})?;
}
let handle = NamespaceHandle::open(&path)?;
Result::<_, NamespaceError>::Ok(NetNs {
name,
path,
nsd_path,
handle,
})
})?;
@ -145,11 +154,15 @@ impl NetNs {
}
fn setup_interfaces(&self) -> Result<(), NamespaceError> {
link_up_ns(&self.path, "lo")?;
self.handle
.run_in(move || link_up("lo").map_err(NamespaceError::from))??;
Ok(())
}
pub fn delete(self) -> Result<(), NamespaceError> {
let parent_namespace = NamespaceHandle::parent()?;
move_all_links_out(self.handle(), &parent_namespace)?;
let name = self.path.file_name().unwrap().to_str().unwrap();
info!(name, "deleting network namespace");
match umount2(&self.path, MntFlags::MNT_DETACH) {
@ -167,6 +180,16 @@ impl NetNs {
})?;
Ok(())
}
/// Move a device into this namespace
pub fn move_device(&self, device: &DeviceName) -> Result<(), LinkError> {
move_link_into(device.as_ref(), self.handle())
}
/// Get the namespace handle
pub fn handle(&self) -> &NamespaceHandle {
&self.handle
}
}
#[derive(Debug, Error)]
@ -187,10 +210,14 @@ pub enum NamespaceError {
Mount(Errno),
#[error("Failed to unmount netns handle: {0:?}")]
UnMount(Errno),
#[error("Failed to setup loopback inside namespace: {0:#}")]
Link(#[from] LinkError),
#[error("Failed to scan {} for namespaces: {error:#}", path.display())]
Scan { path: PathBuf, error: IoError },
#[error(transparent)]
Handle(#[from] NamespaceHandleError),
#[error(transparent)]
Enter(#[from] NamespaceEnterError),
#[error(transparent)]
Link(#[from] LinkError),
}
impl NamespaceError {
@ -199,6 +226,13 @@ impl NamespaceError {
}
}
#[derive(Debug, Error)]
#[error("Error while entering namespace {}: {0:#}", namespace.display())]
pub struct NamespaceEnterError {
namespace: PathBuf,
error: Errno,
}
/// `remove_file`, but ignore "file not found" errors
fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
match remove_file(path) {
@ -207,7 +241,6 @@ fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
}
}
/// `remove_file`, but ignore errors if the file doesn't exist or is a mount point
fn remove_non_mount<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
match remove_file(path) {
@ -215,4 +248,4 @@ fn remove_non_mount<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
Err(err) if err.kind() == ErrorKind::ResourceBusy => Ok(()),
rest => rest,
}
}
}