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

2
Cargo.lock generated
View file

@ -507,7 +507,7 @@ dependencies = [
[[package]] [[package]]
name = "netnsd" name = "netnsd"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"clap", "clap",
"either", "either",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "netnsd" name = "netnsd"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
rust-version = "1.88.0" rust-version = "1.88.0"

View file

@ -6,7 +6,8 @@ A declarative manager for Linux network namespaces.
- Fully declarative configuration - Fully declarative configuration
- Hot reloading of configuration - Hot reloading of configuration
- Port forwarding into the namespace - Port forwarding into or out of the namespace
- Moving network devices to the namespace
## Usage ## Usage
@ -53,6 +54,8 @@ You can specify a different configuration path with the `--config` option.
[[namespace]] [[namespace]]
# name of the namespace to create # name of the namespace to create
name = "test" name = "test"
# move existing devices into the namespace
devices = ["somelink"]
# You can define any number of port forwards to setup into the namespace # You can define any number of port forwards to setup into the namespace
[[namespace.forward]] [[namespace.forward]]

View file

@ -2,6 +2,8 @@
[[namespace]] [[namespace]]
# name of the namespace to create # name of the namespace to create
name = "test" name = "test"
# move existing devices into the namespace
devices = ["somelink"]
# You can define any number of port forwards to setup into the namespace # You can define any number of port forwards to setup into the namespace
[[namespace.forward]] [[namespace.forward]]

View file

@ -2,7 +2,7 @@ mod name;
mod source; mod source;
mod target; mod target;
pub use crate::config::name::NamespaceName; pub use crate::config::name::{DeviceName, NamespaceName};
pub use crate::config::source::ForwardSource; pub use crate::config::source::ForwardSource;
pub use crate::config::target::ForwardTarget; pub use crate::config::target::ForwardTarget;
use serde::Deserialize; use serde::Deserialize;
@ -80,7 +80,10 @@ impl RawConfig {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct NamespaceConfig { pub struct NamespaceConfig {
pub name: NamespaceName, pub name: NamespaceName,
#[serde(default)]
pub forward: Vec<ForwardConfig>, pub forward: Vec<ForwardConfig>,
#[serde(default)]
pub devices: Vec<DeviceName>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]

View file

@ -7,8 +7,18 @@ use std::str::FromStr;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct ValidatedName(String);
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
#[serde(from = "ValidatedName")]
pub struct NamespaceName(String); pub struct NamespaceName(String);
impl From<ValidatedName> for NamespaceName {
fn from(value: ValidatedName) -> Self {
NamespaceName(value.0)
}
}
impl TryFrom<OsString> for NamespaceName { impl TryFrom<OsString> for NamespaceName {
type Error = (); type Error = ();
@ -52,7 +62,60 @@ impl From<NamespaceName> for String {
} }
} }
impl<'de> Deserialize<'de> for NamespaceName { #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
#[serde(from = "ValidatedName")]
pub struct DeviceName(String);
impl From<ValidatedName> for DeviceName {
fn from(value: ValidatedName) -> Self {
DeviceName(value.0)
}
}
impl TryFrom<OsString> for DeviceName {
type Error = ();
fn try_from(value: OsString) -> Result<Self, Self::Error> {
let str = value.into_string().map_err(|_| ())?;
if validate_name(&str) {
Ok(DeviceName(str))
} else {
Err(())
}
}
}
impl Display for DeviceName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl AsRef<str> for DeviceName {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl AsRef<Path> for DeviceName {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl PartialEq<&str> for DeviceName {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl From<DeviceName> for String {
fn from(value: DeviceName) -> Self {
value.0
}
}
impl<'de> Deserialize<'de> for ValidatedName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -60,7 +123,7 @@ impl<'de> Deserialize<'de> for NamespaceName {
struct NamespaceNameVisitor; struct NamespaceNameVisitor;
impl Visitor<'_> for NamespaceNameVisitor { impl Visitor<'_> for NamespaceNameVisitor {
type Value = NamespaceName; type Value = ValidatedName;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("A valid namespace name") formatter.write_str("A valid namespace name")
@ -73,7 +136,7 @@ impl<'de> Deserialize<'de> for NamespaceName {
if !validate_name(v) { if !validate_name(v) {
return Err(E::invalid_value(Unexpected::Str(v), &self)); return Err(E::invalid_value(Unexpected::Str(v), &self));
} }
Ok(NamespaceName(v.into())) Ok(ValidatedName(v.into()))
} }
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
@ -83,7 +146,7 @@ impl<'de> Deserialize<'de> for NamespaceName {
if !validate_name(&v) { if !validate_name(&v) {
return Err(E::invalid_value(Unexpected::Str(&v), &self)); return Err(E::invalid_value(Unexpected::Str(&v), &self));
} }
Ok(NamespaceName(v)) Ok(ValidatedName(v))
} }
} }
@ -92,20 +155,24 @@ impl<'de> Deserialize<'de> for NamespaceName {
} }
impl FromStr for NamespaceName { impl FromStr for NamespaceName {
type Err = InvalidNamespaceNameError; type Err = InvalidNameError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if !validate_name(s) { if !validate_name(s) {
return Err(InvalidNamespaceNameError { name: s.into() }); return Err(InvalidNameError {
name: s.into(),
kind: "namespace",
});
} }
Ok(NamespaceName(s.into())) Ok(NamespaceName(s.into()))
} }
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("invalid name for namespace: '{name}'")] #[error("invalid name for {kind}: '{name}'")]
pub struct InvalidNamespaceNameError { pub struct InvalidNameError {
name: String, name: String,
kind: &'static str,
} }
/// Check if a name follows the portable filename character set /// Check if a name follows the portable filename character set

View file

@ -1,5 +1,8 @@
use crate::config::{Config, ForwardConfig, NamespaceConfig, NamespaceName}; use crate::config::{Config, DeviceName, ForwardConfig, NamespaceConfig, NamespaceName};
use crate::namespace::{NamespaceError, NetNs}; use crate::link::{LinkError, LinkManager};
use crate::namespace::{
NamespaceEnterError, NamespaceError, NamespaceHandle, NamespaceHandleError, NetNs,
};
use crate::proxy::{ActiveProxy, ProxyError}; use crate::proxy::{ActiveProxy, ProxyError};
use futures::FutureExt; use futures::FutureExt;
use futures::StreamExt; use futures::StreamExt;
@ -124,6 +127,7 @@ impl State {
for namespace in &mut self.namespaces { for namespace in &mut self.namespaces {
let config = config.get_namespace(namespace.name()).unwrap(); let config = config.get_namespace(namespace.name()).unwrap();
namespace.update_proxies(config)?; namespace.update_proxies(config)?;
namespace.update_devices(config)?;
} }
Ok(()) Ok(())
@ -139,6 +143,7 @@ impl State {
struct ActiveNamespace { struct ActiveNamespace {
ns: NetNs, ns: NetNs,
proxies: Vec<ActiveProxy>, proxies: Vec<ActiveProxy>,
devices: Vec<DeviceName>,
} }
impl ActiveNamespace { impl ActiveNamespace {
@ -148,6 +153,7 @@ impl ActiveNamespace {
Ok(ActiveNamespace { Ok(ActiveNamespace {
ns, ns,
proxies: Vec::default(), proxies: Vec::default(),
devices: Vec::default(),
}) })
} }
@ -164,10 +170,56 @@ impl ActiveNamespace {
Ok(()) Ok(())
} }
pub fn update_devices(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> {
let parent_namespace = NamespaceHandle::parent()?;
let removed: Vec<_> = self
.devices
.extract_if(.., |existing| {
!config.devices.iter().any(|new| existing == new)
})
.collect();
self.ns.handle().run_in(move || {
let link_manager = LinkManager::new()?;
for link in link_manager.get_links()?.flatten() {
if removed.iter().any(|name| *name == link.name.as_str()) {
info!(namespace = %config.name, link = link.name , "moving link out of namespace");
link_manager.move_link(&link, &parent_namespace)?
}
}
Ok::<_, LinkError>(())
})??;
let mut added = Vec::new();
for new in &config.devices {
if !self.has_device(new) {
added.push(new.clone());
}
}
let link_manager = LinkManager::new()?;
for link in link_manager.get_links()?.flatten() {
if added.iter().any(|name| *name == link.name.as_str()) {
info!(namespace = %config.name, link = link.name , "moving link into namespace");
link_manager.move_link(&link, self.ns.handle())?
}
}
for new in added {
self.devices.push(new);
}
Ok(())
}
fn has_forward(&self, config: &ForwardConfig) -> bool { fn has_forward(&self, config: &ForwardConfig) -> bool {
self.proxies.iter().any(|existing| existing == config) self.proxies.iter().any(|existing| existing == config)
} }
fn has_device(&self, name: &DeviceName) -> bool {
self.devices.iter().any(|existing| existing == name)
}
pub fn name(&self) -> &NamespaceName { pub fn name(&self) -> &NamespaceName {
self.ns.name() self.ns.name()
} }
@ -183,4 +235,10 @@ pub enum DaemonError {
Signal(IoError), Signal(IoError),
#[error(transparent)] #[error(transparent)]
Proxy(#[from] ProxyError), Proxy(#[from] ProxyError),
#[error(transparent)]
Handle(#[from] NamespaceHandleError),
#[error(transparent)]
Enter(#[from] NamespaceEnterError),
#[error(transparent)]
Link(#[from] LinkError),
} }

View file

@ -1,3 +1,4 @@
use crate::namespace::{NamespaceEnterError, NamespaceHandle};
use neli::consts::nl::NlmF; use neli::consts::nl::NlmF;
use neli::consts::rtnl::Ifla; use neli::consts::rtnl::Ifla;
use neli::consts::rtnl::RtAddrFamily; use neli::consts::rtnl::RtAddrFamily;
@ -6,16 +7,14 @@ use neli::consts::socket::NlFamily;
use neli::err::RouterError; use neli::err::RouterError;
use neli::nl::NlPayload; use neli::nl::NlPayload;
use neli::router::synchronous::NlRouter; use neli::router::synchronous::NlRouter;
use neli::rtnl::Ifinfomsg;
use neli::rtnl::IfinfomsgBuilder; use neli::rtnl::IfinfomsgBuilder;
use neli::rtnl::{Ifinfomsg, RtattrBuilder};
use neli::types::{Buffer, RtBuffer};
use neli::utils::Groups; use neli::utils::Groups;
use nix::errno::Errno; use nix::libc::c_int;
use nix::sched::{setns, CloneFlags}; use std::os::fd::AsRawFd;
use std::fs::File;
use std::io::Error as IoError;
use std::path::{Path, PathBuf};
use std::thread::spawn;
use thiserror::Error; use thiserror::Error;
use tracing::info;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum LinkError { pub enum LinkError {
@ -23,12 +22,10 @@ pub enum LinkError {
Netlink, Netlink,
#[error("failed to code netlink response")] #[error("failed to code netlink response")]
Parse, Parse,
#[error("unexpected panic in link setup")] #[error("Link not found: {0}")]
Panic, NotFound(String),
#[error("failed to enter namespace in link setup: {0}")] #[error(transparent)]
Namespace(Errno), Enter(#[from] NamespaceEnterError),
#[error("Failed to open namespace file {}: {error:#}", path.display())]
OpenNamespace { path: PathBuf, error: IoError },
} }
impl<T, P> From<RouterError<T, P>> for LinkError { impl<T, P> From<RouterError<T, P>> for LinkError {
@ -37,60 +34,133 @@ impl<T, P> From<RouterError<T, P>> for LinkError {
} }
} }
/// Set a link to UP inside a namespace pub struct LinkManager {
pub fn link_up_ns(namespace: impl AsRef<Path>, link_name: &'static str) -> Result<(), LinkError> { router: NlRouter,
let namespace = namespace.as_ref(); }
let ns_handle = File::open(namespace).map_err(|error| LinkError::OpenNamespace {
error,
path: namespace.into(),
})?;
spawn(move || { pub struct Link {
setns(ns_handle, CloneFlags::CLONE_NEWNET).map_err(LinkError::Namespace)?; family: RtAddrFamily,
link_up(link_name) index: c_int,
}) pub name: String,
.join() }
.map_err(|_| LinkError::Panic)?
impl Link {
fn msg_builder(&self) -> IfinfomsgBuilder {
IfinfomsgBuilder::default()
.ifi_family(self.family)
.ifi_index(self.index)
}
}
impl LinkManager {
pub fn new() -> Result<Self, LinkError> {
let (router, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty())?;
router.enable_ext_ack(true)?;
router.enable_strict_checking(true)?;
Ok(LinkManager { router })
}
pub fn get_link(&self, name: impl AsRef<str>) -> Result<Link, LinkError> {
let name = name.as_ref();
for link in self.get_links()? {
let link = link?;
if link.name == name {
return Ok(link);
}
}
Err(LinkError::NotFound(name.into()))
}
pub fn get_links(&self) -> Result<impl Iterator<Item = Result<Link, LinkError>>, LinkError> {
let ifinfomsg = IfinfomsgBuilder::default()
.ifi_family(RtAddrFamily::Inet)
.build()
.unwrap();
let recv = self.router.send::<_, _, Rtm, Ifinfomsg>(
Rtm::Getlink,
NlmF::DUMP | NlmF::ACK,
NlPayload::Payload(ifinfomsg),
)?;
Ok(recv
.map(|response| {
if let Some(payload) = response?.get_payload() {
let name = payload
.rtattrs()
.get_attr_handle()
.get_attr_payload_as_with_len::<String>(Ifla::Ifname)
.map_err(|_| LinkError::Parse)?;
Ok(Some(Link {
family: *payload.ifi_family(),
index: *payload.ifi_index(),
name,
}))
} else {
Ok(None)
}
})
.filter_map(|item| item.transpose()))
}
/// Move a link to a namespace
pub fn move_link<Fd: AsRawFd>(&self, link: &Link, namespace: Fd) -> Result<(), LinkError> {
let ns_handle = namespace.as_raw_fd();
let mut info_attrs = RtBuffer::<Ifla, Buffer>::new();
info_attrs.push(
RtattrBuilder::default()
.rta_type(Ifla::NetNsFd)
.rta_payload(Buffer::from(ns_handle.to_ne_bytes().as_slice()))
.build()
.expect("invalid rtattr"),
);
let msg = link.msg_builder().rtattrs(info_attrs).build().unwrap();
self.router.send::<_, _, Rtm, Ifinfomsg>(
Rtm::Setlink,
NlmF::ACK,
NlPayload::Payload(msg),
)?;
Ok(())
}
} }
/// Set a link to UP /// Set a link to UP
pub fn link_up(link_name: &str) -> Result<(), LinkError> { pub fn link_up(link_name: &str) -> Result<(), LinkError> {
// I honestly don't really know how this code works let manager = LinkManager::new()?;
// It's mostly a copy from one of neli's examples and seems to do what it needs to let link = manager.get_link(link_name)?;
let (rtnl, _) = NlRouter::connect(NlFamily::Route, None, Groups::empty())?;
rtnl.enable_ext_ack(true)?;
rtnl.enable_strict_checking(true)?;
let ifinfomsg = IfinfomsgBuilder::default()
.ifi_family(RtAddrFamily::Inet)
.build()
.unwrap();
let recv = rtnl.send::<_, _, Rtm, Ifinfomsg>( let up_msg = link.msg_builder().up().build().unwrap();
Rtm::Getlink, manager.router.send::<_, _, Rtm, Ifinfomsg>(
NlmF::DUMP | NlmF::ACK, Rtm::Setlink,
NlPayload::Payload(ifinfomsg), NlmF::ACK,
NlPayload::Payload(up_msg),
)?; )?;
for response in recv { Ok(())
if let Some(payload) = response?.get_payload() { }
let name = payload
.rtattrs() /// Move a link into a namespace
.get_attr_handle() pub fn move_link_into(link_name: &str, namespace: &NamespaceHandle) -> Result<(), LinkError> {
.get_attr_payload_as_with_len::<String>(Ifla::Ifname) let manager = LinkManager::new()?;
.map_err(|_| LinkError::Parse)?; let link = manager.get_link(link_name)?;
if name == link_name { info!(name = &link.name, "moving link into namespace");
let up_msg = IfinfomsgBuilder::default() manager.move_link(&link, namespace)
.ifi_family(RtAddrFamily::Inet) }
.ifi_index(*payload.ifi_index())
.up() /// Move all links out of a namespace, except for lo
.build() pub fn move_all_links_out(
.unwrap(); namespace: &NamespaceHandle,
rtnl.send::<_, _, Rtm, Ifinfomsg>( parent: &NamespaceHandle,
Rtm::Setlink, ) -> Result<(), LinkError> {
NlmF::ACK, namespace.run_in(|| {
NlPayload::Payload(up_msg), let manager = LinkManager::new()?;
)?; for link in manager.get_links()?.flatten() {
if link.name != "lo" {
info!(name = &link.name, "moving link out of namespace");
manager.move_link(&link, parent)?
} }
} }
} Ok::<_, LinkError>(())
})??;
Ok(()) Ok(())
} }

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; mod raw;
use crate::config::NamespaceName; use crate::config::{DeviceName, NamespaceName};
use crate::link::{LinkError, link_up_ns}; use crate::link::{link_up, move_all_links_out, move_link_into, LinkError};
use crate::namespace::raw::{NamespaceSetupError, create_network_namespace}; pub use crate::namespace::handle::{NamespaceHandle, NamespaceHandleError};
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError};
use either::Either; use either::Either;
use nix::errno::Errno; use nix::errno::Errno;
use nix::mount::{MntFlags, MsFlags, mount, umount2}; use nix::mount::{mount, umount2, MntFlags, MsFlags};
use std::fs::{File, create_dir, read_dir, remove_file}; use std::fs::{create_dir, read_dir, remove_file, 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;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error, info}; use tracing::{debug, info};
pub struct NetNs { pub struct NetNs {
name: NamespaceName, name: NamespaceName,
path: PathBuf, path: PathBuf,
nsd_path: PathBuf, nsd_path: PathBuf,
handle: NamespaceHandle,
} }
impl NetNs { 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") { 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 => {
@ -71,10 +76,12 @@ impl NetNs {
})?; })?;
} }
let handle = NamespaceHandle::open(&path)?;
return Ok(NetNs { return Ok(NetNs {
name, name,
nsd_path, nsd_path,
path, path,
handle,
}); });
} }
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)), Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
@ -89,10 +96,12 @@ impl NetNs {
path: nsd_path.clone(), path: nsd_path.clone(),
})?; })?;
} }
let handle = NamespaceHandle::open(&path)?;
Result::<_, NamespaceError>::Ok(NetNs { Result::<_, NamespaceError>::Ok(NetNs {
name, name,
path, path,
nsd_path, nsd_path,
handle,
}) })
})?; })?;
@ -145,11 +154,15 @@ impl NetNs {
} }
fn setup_interfaces(&self) -> Result<(), NamespaceError> { 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(()) Ok(())
} }
pub fn delete(self) -> Result<(), NamespaceError> { 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(); let name = self.path.file_name().unwrap().to_str().unwrap();
info!(name, "deleting network namespace"); info!(name, "deleting network namespace");
match umount2(&self.path, MntFlags::MNT_DETACH) { match umount2(&self.path, MntFlags::MNT_DETACH) {
@ -167,6 +180,16 @@ impl NetNs {
})?; })?;
Ok(()) 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)] #[derive(Debug, Error)]
@ -187,10 +210,14 @@ pub enum NamespaceError {
Mount(Errno), Mount(Errno),
#[error("Failed to unmount netns handle: {0:?}")] #[error("Failed to unmount netns handle: {0:?}")]
UnMount(Errno), UnMount(Errno),
#[error("Failed to setup loopback inside namespace: {0:#}")]
Link(#[from] LinkError),
#[error("Failed to scan {} for namespaces: {error:#}", path.display())] #[error("Failed to scan {} for namespaces: {error:#}", path.display())]
Scan { path: PathBuf, error: IoError }, Scan { path: PathBuf, error: IoError },
#[error(transparent)]
Handle(#[from] NamespaceHandleError),
#[error(transparent)]
Enter(#[from] NamespaceEnterError),
#[error(transparent)]
Link(#[from] LinkError),
} }
impl NamespaceError { 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 /// `remove_file`, but ignore "file not found" errors
fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> std::io::Result<()> { fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
match remove_file(path) { 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 /// `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<()> { fn remove_non_mount<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
match remove_file(path) { 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(()), Err(err) if err.kind() == ErrorKind::ResourceBusy => Ok(()),
rest => rest, rest => rest,
} }
} }

View file

@ -1,6 +1,7 @@
use crate::config::{Config, NamespaceName}; use crate::config::{Config, NamespaceName};
use crate::namespace::NetNs; use crate::namespace::NetNs;
use main_error::MainResult; use main_error::MainResult;
use tracing::error;
pub fn up(config: Config) -> MainResult { pub fn up(config: Config) -> MainResult {
let mut namespaces = NetNs::existing(false)? let mut namespaces = NetNs::existing(false)?
@ -15,7 +16,13 @@ pub fn up(config: Config) -> MainResult {
for new in config.namespaces { for new in config.namespaces {
if !has_namespace(&namespaces, &new.name) { if !has_namespace(&namespaces, &new.name) {
namespaces.push(NetNs::new(new.name)?); namespaces.push(NetNs::new(new.name.clone())?);
}
let namespace = get_namespace(&namespaces, &new.name).expect("namespace is just created");
for device in new.devices {
if let Err(error) = namespace.move_device(&device) {
error!(%error, "failed to move device into namespace");
}
} }
} }
@ -25,3 +32,7 @@ pub fn up(config: Config) -> MainResult {
fn has_namespace(namespaces: &[NetNs], name: &NamespaceName) -> bool { fn has_namespace(namespaces: &[NetNs], name: &NamespaceName) -> bool {
namespaces.iter().any(|existing| existing.name() == name) namespaces.iter().any(|existing| existing.name() == name)
} }
fn get_namespace<'a>(namespaces: &'a [NetNs], name: &NamespaceName) -> Option<&'a NetNs> {
namespaces.iter().find(|existing| existing.name() == name)
}