track which namespaces we control

This commit is contained in:
Robin Appelman 2025-11-01 16:42:34 +01:00
commit 32fc90debe
5 changed files with 59 additions and 14 deletions

1
Cargo.lock generated
View file

@ -576,6 +576,7 @@ name = "netnsd"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"either",
"futures", "futures",
"futures-concurrency", "futures-concurrency",
"humansize", "humansize",

View file

@ -20,6 +20,7 @@ futures = "0.3.31"
futures-concurrency = "7.6.3" futures-concurrency = "7.6.3"
humansize = { version = "2.1.3", features = ["no_alloc"] } humansize = { version = "2.1.3", features = ["no_alloc"] }
neli = "0.7.1" neli = "0.7.1"
either = "1.15.0"
[dev-dependencies] [dev-dependencies]
serde_test = "1.0.177" serde_test = "1.0.177"

View file

@ -1,11 +1,25 @@
use serde::de::{Error, Unexpected, Visitor}; use serde::de::{Error, Unexpected, Visitor};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::ffi::OsString;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::path::Path; use std::path::Path;
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct NamespaceName(String); pub struct NamespaceName(String);
impl TryFrom<OsString> for NamespaceName {
type Error = ();
fn try_from(value: OsString) -> Result<Self, Self::Error> {
let str = value.into_string().map_err(|_| ())?;
if validate_name(&str) {
Ok(NamespaceName(str))
} else {
Err(())
}
}
}
impl Display for NamespaceName { impl Display for NamespaceName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f) self.0.fmt(f)
@ -80,7 +94,7 @@ fn validate_name(name: &str) -> bool {
#[test] #[test]
fn test_de() { fn test_de() {
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token}; use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
assert_de_tokens(&NamespaceName("foo".into()), &[Token::String("foo")]); assert_de_tokens(&NamespaceName("foo".into()), &[Token::String("foo")]);

View file

@ -22,7 +22,7 @@ pub fn daemon(config: Config) -> MainResult {
} }
async fn daemon_async(mut config: Config) -> Result<(), DaemonError> { async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
let mut state = State::default(); let mut state = State::new()?;
state.update(&config)?; state.update(&config)?;
// now the namespaces are setup, we can tell systemd to start any service depending on them // now the namespaces are setup, we can tell systemd to start any service depending on them
@ -104,12 +104,18 @@ enum Event {
Info, Info,
} }
#[derive(Default)]
struct State { struct State {
namespaces: Vec<ActiveNamespace>, namespaces: Vec<ActiveNamespace>,
} }
impl State { impl State {
pub fn new() -> Result<Self, DaemonError> {
let namespaces = NetNs::existing()?.map(ActiveNamespace::new).collect::<Result<Vec<_>, _>>()?;
Ok(State {
namespaces
})
}
pub fn update(&mut self, config: &Config) -> Result<(), DaemonError> { pub fn update(&mut self, config: &Config) -> Result<(), DaemonError> {
for removed in self.namespaces.extract_if(.., |namespace| { for removed in self.namespaces.extract_if(.., |namespace| {
config.get_namespace(namespace.name()).is_none() config.get_namespace(namespace.name()).is_none()
@ -119,7 +125,7 @@ impl State {
for new in &config.namespaces { for new in &config.namespaces {
if !self.has_namespace(&new.name) { if !self.has_namespace(&new.name) {
self.namespaces.push(ActiveNamespace::new(new)?); self.namespaces.push(ActiveNamespace::new(new.name.clone())?);
} }
} }
@ -144,16 +150,13 @@ struct ActiveNamespace {
} }
impl ActiveNamespace { impl ActiveNamespace {
pub fn new(config: &NamespaceConfig) -> Result<Self, DaemonError> { pub fn new(name: NamespaceName) -> Result<Self, DaemonError> {
let ns = NetNs::new(config.name.clone())?; let ns = NetNs::new(name)?;
let mut namespace = ActiveNamespace { Ok(ActiveNamespace {
ns, ns,
proxies: Vec::default(), proxies: Vec::default(),
}; })
namespace.update_proxies(config)?;
Ok(namespace)
} }
pub fn update_proxies(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> { pub fn update_proxies(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> {

View file

@ -3,10 +3,12 @@ mod raw;
use crate::config::NamespaceName; use crate::config::NamespaceName;
use crate::link::{LinkError, link_up_ns}; use crate::link::{LinkError, link_up_ns};
use crate::namespace::raw::{NamespaceSetupError, create_network_namespace}; use crate::namespace::raw::{NamespaceSetupError, create_network_namespace};
use either::Either;
use nix::errno::Errno; use nix::errno::Errno;
use nix::mount::{MntFlags, MsFlags, mount, umount2}; use nix::mount::{MntFlags, MsFlags, mount, umount2};
use std::fs::{File, create_dir, remove_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::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
@ -15,9 +17,26 @@ use tracing::{debug, error, info};
pub struct NetNs { pub struct NetNs {
name: NamespaceName, name: NamespaceName,
path: PathBuf, path: PathBuf,
nsd_path: PathBuf,
} }
impl NetNs { impl NetNs {
pub fn existing() -> 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 => {
return Ok(Either::Left(empty()));
}
Err(error) => Err(NamespaceError::Scan {
path: "/var/run/netnsd".into(),
error,
}),
}?;
Ok(Either::Right(dir.flatten().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 that will be removed when dropped
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");
@ -41,7 +60,8 @@ impl NetNs {
} }
return Ok(NetNs { return Ok(NetNs {
name: name.clone(), name,
nsd_path,
path, path,
}); });
} }
@ -57,7 +77,7 @@ impl NetNs {
path: nsd_path.clone(), path: nsd_path.clone(),
})?; })?;
} }
Result::<_, NamespaceError>::Ok(NetNs { name, path }) Result::<_, NamespaceError>::Ok(NetNs { name, path, nsd_path })
})?; })?;
ns.setup_interfaces()?; ns.setup_interfaces()?;
@ -121,6 +141,10 @@ impl NetNs {
error, error,
path: self.path, path: self.path,
})?; })?;
remove_file(&self.nsd_path).map_err(|error| NamespaceError::Delete {
error,
path: self.nsd_path,
})?;
Ok(()) Ok(())
} }
} }
@ -145,6 +169,8 @@ pub enum NamespaceError {
UnMount(Errno), UnMount(Errno),
#[error("Failed to setup loopback inside namespace: {0:#}")] #[error("Failed to setup loopback inside namespace: {0:#}")]
Link(#[from] LinkError), Link(#[from] LinkError),
#[error("Failed to scan {} for namespaces: {error:#}", path.display())]
Scan { path: PathBuf, error: IoError },
} }
impl NamespaceError { impl NamespaceError {