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"
dependencies = [
"clap",
"either",
"futures",
"futures-concurrency",
"humansize",

View file

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

View file

@ -1,11 +1,25 @@
use serde::de::{Error, Unexpected, Visitor};
use serde::{Deserialize, Deserializer};
use std::ffi::OsString;
use std::fmt::{Display, Formatter};
use std::path::Path;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
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 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
@ -80,7 +94,7 @@ fn validate_name(name: &str) -> bool {
#[test]
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")]);

View file

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

View file

@ -3,10 +3,12 @@ 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, remove_file};
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;
@ -15,9 +17,26 @@ use tracing::{debug, error, info};
pub struct NetNs {
name: NamespaceName,
path: PathBuf,
nsd_path: PathBuf,
}
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
pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> {
let parent = Path::new("/var/run/netns");
@ -41,7 +60,8 @@ impl NetNs {
}
return Ok(NetNs {
name: name.clone(),
name,
nsd_path,
path,
});
}
@ -57,7 +77,7 @@ impl NetNs {
path: nsd_path.clone(),
})?;
}
Result::<_, NamespaceError>::Ok(NetNs { name, path })
Result::<_, NamespaceError>::Ok(NetNs { name, path, nsd_path })
})?;
ns.setup_interfaces()?;
@ -121,6 +141,10 @@ impl NetNs {
error,
path: self.path,
})?;
remove_file(&self.nsd_path).map_err(|error| NamespaceError::Delete {
error,
path: self.nsd_path,
})?;
Ok(())
}
}
@ -145,6 +169,8 @@ pub enum NamespaceError {
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 {