make managing namespaces more resilient against inconsitent states

This commit is contained in:
Robin Appelman 2026-02-13 23:22:45 +01:00
commit 9dd802050c
4 changed files with 49 additions and 15 deletions

View file

@ -101,7 +101,7 @@ struct State {
impl State { impl State {
pub fn new() -> Result<Self, DaemonError> { pub fn new() -> Result<Self, DaemonError> {
let namespaces = NetNs::existing()? let namespaces = NetNs::existing(false)?
.map(ActiveNamespace::new) .map(ActiveNamespace::new)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(State { namespaces }) Ok(State { namespaces })

View file

@ -2,7 +2,7 @@ use crate::namespace::NetNs;
use main_error::MainResult; use main_error::MainResult;
pub fn down() -> MainResult { pub fn down() -> MainResult {
for name in NetNs::existing()? { for name in NetNs::existing(true)? {
let ns = NetNs::new(name)?; let ns = NetNs::new(name)?;
ns.delete()? ns.delete()?
} }

View file

@ -1,12 +1,12 @@
mod raw; mod raw;
use crate::config::NamespaceName; use crate::config::NamespaceName;
use crate::link::{link_up_ns, LinkError}; use crate::link::{LinkError, link_up_ns};
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError}; use crate::namespace::raw::{NamespaceSetupError, create_network_namespace};
use either::Either; use either::Either;
use nix::errno::Errno; use nix::errno::Errno;
use nix::mount::{mount, umount2, MntFlags, MsFlags}; use nix::mount::{MntFlags, MsFlags, mount, umount2};
use std::fs::{create_dir, read_dir, remove_file, File}; use std::fs::{File, create_dir, read_dir, remove_file};
use std::io::{Error as IoError, ErrorKind}; use std::io::{Error as IoError, ErrorKind};
use std::iter::empty; use std::iter::empty;
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
@ -21,7 +21,7 @@ pub struct NetNs {
} }
impl NetNs { impl NetNs {
pub fn existing() -> 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") { let dir = match read_dir("/var/run/netnsd") {
Ok(dir) => Ok(dir), Ok(dir) => Ok(dir),
Err(error) if error.kind() == ErrorKind::NotFound => { Err(error) if error.kind() == ErrorKind::NotFound => {
@ -32,12 +32,14 @@ impl NetNs {
error, error,
}), }),
}?; }?;
Ok(Either::Right(dir.flatten().flat_map(|entry| { Ok(Either::Right(
NamespaceName::try_from(entry.file_name()).ok() dir.flatten()
}))) .filter(move |entry| include_broken || entry.path().is_symlink())
.flat_map(|entry| NamespaceName::try_from(entry.file_name()).ok()),
))
} }
/// Create a new named network namespace that will be removed when dropped /// Create a new named network namespace
pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> { pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> {
let parent = Path::new("/var/run/netns"); let parent = Path::new("/var/run/netns");
let nsd_parent = Path::new("/var/run/netnsd"); let nsd_parent = Path::new("/var/run/netnsd");
@ -47,12 +49,22 @@ impl NetNs {
let path = parent.join(&name); let path = parent.join(&name);
let nsd_path = nsd_parent.join(&name); let nsd_path = nsd_parent.join(&name);
remove_non_mount(&path).map_err(|error| NamespaceError::Delete {
error,
path: nsd_path.clone(),
})?;
match File::create_new(&path) { match File::create_new(&path) {
Ok(_) => {} Ok(_) => {}
Err(e) if e.kind() == ErrorKind::AlreadyExists => { Err(e) if e.kind() == ErrorKind::AlreadyExists => {
info!(%name, "using existing network namespace"); info!(%name, "using existing network namespace");
if !nsd_path.is_symlink() { if !nsd_path.is_symlink() {
remove_file_if_exists(&nsd_path).map_err(|error| NamespaceError::Delete {
error,
path: nsd_path.clone(),
})?;
symlink(&path, &nsd_path).map_err(|error| NamespaceError::Symlink { symlink(&path, &nsd_path).map_err(|error| NamespaceError::Symlink {
error, error,
path: nsd_path.clone(), path: nsd_path.clone(),
@ -140,12 +152,16 @@ impl NetNs {
pub fn delete(self) -> Result<(), NamespaceError> { pub fn delete(self) -> Result<(), NamespaceError> {
let name = self.path.file_name().unwrap().to_str().unwrap(); let name = self.path.file_name().unwrap().to_str().unwrap();
info!(name, "deleting network namespace"); info!(name, "deleting network namespace");
umount2(&self.path, MntFlags::MNT_DETACH).map_err(NamespaceError::UnMount)?; match umount2(&self.path, MntFlags::MNT_DETACH) {
remove_file(&self.path).map_err(|error| NamespaceError::Delete { Err(Errno::EINVAL) => Ok(()), // not a mountpoint, namespace doesn't exist
rest => rest,
}
.map_err(NamespaceError::UnMount)?;
remove_file_if_exists(&self.path).map_err(|error| NamespaceError::Delete {
error, error,
path: self.path, path: self.path,
})?; })?;
remove_file(&self.nsd_path).map_err(|error| NamespaceError::Delete { remove_file_if_exists(&self.nsd_path).map_err(|error| NamespaceError::Delete {
error, error,
path: self.nsd_path, path: self.nsd_path,
})?; })?;
@ -182,3 +198,21 @@ impl NamespaceError {
NamespaceError::Create { path, error } NamespaceError::Create { path, error }
} }
} }
/// `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) {
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
rest => rest,
}
}
/// `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) {
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
Err(err) if err.kind() == ErrorKind::ResourceBusy => Ok(()),
rest => rest,
}
}

View file

@ -3,7 +3,7 @@ use crate::namespace::NetNs;
use main_error::MainResult; use main_error::MainResult;
pub fn up(config: Config) -> MainResult { pub fn up(config: Config) -> MainResult {
let mut namespaces = NetNs::existing()? let mut namespaces = NetNs::existing(false)?
.map(NetNs::new) .map(NetNs::new)
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;