mod raw; use crate::config::NamespaceName; use crate::link::{LinkError, link_up_ns}; use crate::namespace::raw::{NamespaceSetupError, create_network_namespace}; 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 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}; pub struct NetNs { name: NamespaceName, path: PathBuf, nsd_path: PathBuf, } impl NetNs { pub fn existing(include_broken: bool) -> Result, NamespaceError> { let dir = match read_dir("/var/run/netnsd") { Ok(dir) => Ok(dir), Err(error) if error.kind() == ErrorKind::NotFound => { return Ok(Either::Left(empty())); } Err(error) => Err(NamespaceError::Scan { path: "/var/run/netnsd".into(), error, }), }?; Ok(Either::Right( 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 pub fn new(name: NamespaceName) -> Result { let parent = Path::new("/var/run/netns"); let nsd_parent = Path::new("/var/run/netnsd"); setup_tmpfs(parent)?; setup_tmpfs(nsd_parent)?; let path = 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) { Ok(_) => {} Err(e) if e.kind() == ErrorKind::AlreadyExists => { info!(%name, "using existing network namespace"); 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 { error, path: nsd_path.clone(), })?; } return Ok(NetNs { name, nsd_path, path, }); } Err(e) => return Err(NamespaceError::from_create(path.clone(), e)), } info!(%name, "creating network namespace"); let ns = create_network_namespace(move |ns| { bind_namespace(&ns, &path)?; if !nsd_path.is_symlink() { symlink(&path, &nsd_path).map_err(|error| NamespaceError::Symlink { error, path: nsd_path.clone(), })?; } Result::<_, NamespaceError>::Ok(NetNs { name, path, nsd_path, }) })?; ns.setup_interfaces()?; Ok(ns) } } fn setup_tmpfs(path: &Path) -> Result<(), NamespaceError> { let created = match create_dir(path) { Ok(_) => Ok(true), Err(error) if error.kind() == ErrorKind::AlreadyExists => Ok(false), Err(error) => Err(NamespaceError::Parent { path: path.into(), error, }), }?; if created { mount( Some("tmpfs"), path.as_os_str(), Some("tmpfs"), MsFlags::empty(), Some("size=16k,mode=1755"), ) .map_err(|error| NamespaceError::ParentTmpfs { path: path.into(), error, })? } Ok(()) } fn bind_namespace(namespace: &Path, path: &Path) -> Result<(), NamespaceError> { debug!(namespace = %namespace.display(), path = %path.display(), "mounting namespace"); mount( Some(namespace.as_os_str()), path.as_os_str(), Option::<&str>::None, MsFlags::MS_BIND, Option::<&str>::None, ) .map_err(NamespaceError::Mount) } impl NetNs { pub fn name(&self) -> &NamespaceName { &self.name } fn setup_interfaces(&self) -> Result<(), NamespaceError> { link_up_ns(&self.path, "lo")?; Ok(()) } pub fn delete(self) -> Result<(), NamespaceError> { let name = self.path.file_name().unwrap().to_str().unwrap(); info!(name, "deleting network namespace"); match umount2(&self.path, MntFlags::MNT_DETACH) { 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, path: self.path, })?; remove_file_if_exists(&self.nsd_path).map_err(|error| NamespaceError::Delete { error, path: self.nsd_path, })?; Ok(()) } } #[derive(Debug, Error)] pub enum NamespaceError { #[error("Failed to create parent directory for namespaces {}: {error:#}", path.display())] Parent { path: PathBuf, error: IoError }, #[error("Failed to create parent tmpfs for namespaces {}: {error:#}", path.display())] ParentTmpfs { path: PathBuf, error: Errno }, #[error("Failed to create namespace file {}: {error:#}", path.display())] Create { path: PathBuf, error: IoError }, #[error("Failed to delete namespace file {}: {error:#}", path.display())] Delete { path: PathBuf, error: IoError }, #[error("Failed to register namespace symlink {}: {error:#}", path.display())] Symlink { path: PathBuf, error: IoError }, #[error("Failed to setup namespace: {0:#}")] Setup(#[from] NamespaceSetupError), #[error("Failed to bind-mount netns handle: {0:?}")] 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 }, } impl NamespaceError { fn from_create(path: PathBuf, error: IoError) -> Self { NamespaceError::Create { path, error } } } /// `remove_file`, but ignore "file not found" errors fn remove_file_if_exists>(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>(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, } }