From 3fa69dc4345f4b59a03c1e03916825f722c3562c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sat, 14 Feb 2026 15:59:47 +0100 Subject: [PATCH] add support for moving devices into the namespace --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 5 +- config.sample.toml | 2 + src/config/mod.rs | 5 +- src/config/name.rs | 83 ++++++++++++++++-- src/daemon.rs | 62 ++++++++++++- src/link.rs | 188 +++++++++++++++++++++++++++------------- src/namespace/handle.rs | 76 ++++++++++++++++ src/namespace/mod.rs | 57 +++++++++--- src/up.rs | 13 ++- 11 files changed, 409 insertions(+), 86 deletions(-) create mode 100644 src/namespace/handle.rs diff --git a/Cargo.lock b/Cargo.lock index 6275c13..26795f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ dependencies = [ [[package]] name = "netnsd" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "either", diff --git a/Cargo.toml b/Cargo.toml index ee25579..d6b4a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "netnsd" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.88.0" diff --git a/README.md b/README.md index 40ebe43..f127efb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ A declarative manager for Linux network namespaces. - Fully declarative 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 @@ -53,6 +54,8 @@ You can specify a different configuration path with the `--config` option. [[namespace]] # name of the namespace to create name = "test" +# move existing devices into the namespace +devices = ["somelink"] # You can define any number of port forwards to setup into the namespace [[namespace.forward]] diff --git a/config.sample.toml b/config.sample.toml index a4132ce..e128af0 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -2,6 +2,8 @@ [[namespace]] # name of the namespace to create name = "test" +# move existing devices into the namespace +devices = ["somelink"] # You can define any number of port forwards to setup into the namespace [[namespace.forward]] diff --git a/src/config/mod.rs b/src/config/mod.rs index 779fce1..2395ae3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,7 +2,7 @@ mod name; mod source; 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::target::ForwardTarget; use serde::Deserialize; @@ -80,7 +80,10 @@ impl RawConfig { #[derive(Deserialize, Debug)] pub struct NamespaceConfig { pub name: NamespaceName, + #[serde(default)] pub forward: Vec, + #[serde(default)] + pub devices: Vec, } #[derive(Deserialize, Debug)] diff --git a/src/config/name.rs b/src/config/name.rs index 32fbbd6..9490fbe 100644 --- a/src/config/name.rs +++ b/src/config/name.rs @@ -7,8 +7,18 @@ use std::str::FromStr; use thiserror::Error; #[derive(Debug, Clone, Eq, PartialEq, Hash)] +struct ValidatedName(String); + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)] +#[serde(from = "ValidatedName")] pub struct NamespaceName(String); +impl From for NamespaceName { + fn from(value: ValidatedName) -> Self { + NamespaceName(value.0) + } +} + impl TryFrom for NamespaceName { type Error = (); @@ -52,7 +62,60 @@ impl From for String { } } -impl<'de> Deserialize<'de> for NamespaceName { +#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)] +#[serde(from = "ValidatedName")] +pub struct DeviceName(String); + +impl From for DeviceName { + fn from(value: ValidatedName) -> Self { + DeviceName(value.0) + } +} + +impl TryFrom for DeviceName { + type Error = (); + + fn try_from(value: OsString) -> Result { + 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 for DeviceName { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl AsRef 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 for String { + fn from(value: DeviceName) -> Self { + value.0 + } +} + +impl<'de> Deserialize<'de> for ValidatedName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -60,7 +123,7 @@ impl<'de> Deserialize<'de> for NamespaceName { struct NamespaceNameVisitor; impl Visitor<'_> for NamespaceNameVisitor { - type Value = NamespaceName; + type Value = ValidatedName; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { formatter.write_str("A valid namespace name") @@ -73,7 +136,7 @@ impl<'de> Deserialize<'de> for NamespaceName { if !validate_name(v) { return Err(E::invalid_value(Unexpected::Str(v), &self)); } - Ok(NamespaceName(v.into())) + Ok(ValidatedName(v.into())) } fn visit_string(self, v: String) -> Result @@ -83,7 +146,7 @@ impl<'de> Deserialize<'de> for NamespaceName { if !validate_name(&v) { 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 { - type Err = InvalidNamespaceNameError; + type Err = InvalidNameError; fn from_str(s: &str) -> Result { if !validate_name(s) { - return Err(InvalidNamespaceNameError { name: s.into() }); + return Err(InvalidNameError { + name: s.into(), + kind: "namespace", + }); } Ok(NamespaceName(s.into())) } } #[derive(Debug, Error)] -#[error("invalid name for namespace: '{name}'")] -pub struct InvalidNamespaceNameError { +#[error("invalid name for {kind}: '{name}'")] +pub struct InvalidNameError { name: String, + kind: &'static str, } /// Check if a name follows the portable filename character set diff --git a/src/daemon.rs b/src/daemon.rs index d655bb5..27d3bbb 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,5 +1,8 @@ -use crate::config::{Config, ForwardConfig, NamespaceConfig, NamespaceName}; -use crate::namespace::{NamespaceError, NetNs}; +use crate::config::{Config, DeviceName, ForwardConfig, NamespaceConfig, NamespaceName}; +use crate::link::{LinkError, LinkManager}; +use crate::namespace::{ + NamespaceEnterError, NamespaceError, NamespaceHandle, NamespaceHandleError, NetNs, +}; use crate::proxy::{ActiveProxy, ProxyError}; use futures::FutureExt; use futures::StreamExt; @@ -124,6 +127,7 @@ impl State { for namespace in &mut self.namespaces { let config = config.get_namespace(namespace.name()).unwrap(); namespace.update_proxies(config)?; + namespace.update_devices(config)?; } Ok(()) @@ -139,6 +143,7 @@ impl State { struct ActiveNamespace { ns: NetNs, proxies: Vec, + devices: Vec, } impl ActiveNamespace { @@ -148,6 +153,7 @@ impl ActiveNamespace { Ok(ActiveNamespace { ns, proxies: Vec::default(), + devices: Vec::default(), }) } @@ -164,10 +170,56 @@ impl ActiveNamespace { 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 { 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 { self.ns.name() } @@ -183,4 +235,10 @@ pub enum DaemonError { Signal(IoError), #[error(transparent)] Proxy(#[from] ProxyError), + #[error(transparent)] + Handle(#[from] NamespaceHandleError), + #[error(transparent)] + Enter(#[from] NamespaceEnterError), + #[error(transparent)] + Link(#[from] LinkError), } diff --git a/src/link.rs b/src/link.rs index 4543393..0d251ba 100644 --- a/src/link.rs +++ b/src/link.rs @@ -1,3 +1,4 @@ +use crate::namespace::{NamespaceEnterError, NamespaceHandle}; use neli::consts::nl::NlmF; use neli::consts::rtnl::Ifla; use neli::consts::rtnl::RtAddrFamily; @@ -6,16 +7,14 @@ use neli::consts::socket::NlFamily; use neli::err::RouterError; use neli::nl::NlPayload; use neli::router::synchronous::NlRouter; -use neli::rtnl::Ifinfomsg; use neli::rtnl::IfinfomsgBuilder; +use neli::rtnl::{Ifinfomsg, RtattrBuilder}; +use neli::types::{Buffer, RtBuffer}; use neli::utils::Groups; -use nix::errno::Errno; -use nix::sched::{setns, CloneFlags}; -use std::fs::File; -use std::io::Error as IoError; -use std::path::{Path, PathBuf}; -use std::thread::spawn; +use nix::libc::c_int; +use std::os::fd::AsRawFd; use thiserror::Error; +use tracing::info; #[derive(Debug, Error)] pub enum LinkError { @@ -23,12 +22,10 @@ pub enum LinkError { Netlink, #[error("failed to code netlink response")] Parse, - #[error("unexpected panic in link setup")] - Panic, - #[error("failed to enter namespace in link setup: {0}")] - Namespace(Errno), - #[error("Failed to open namespace file {}: {error:#}", path.display())] - OpenNamespace { path: PathBuf, error: IoError }, + #[error("Link not found: {0}")] + NotFound(String), + #[error(transparent)] + Enter(#[from] NamespaceEnterError), } impl From> for LinkError { @@ -37,60 +34,133 @@ impl From> for LinkError { } } -/// Set a link to UP inside a namespace -pub fn link_up_ns(namespace: impl AsRef, link_name: &'static str) -> Result<(), LinkError> { - let namespace = namespace.as_ref(); - let ns_handle = File::open(namespace).map_err(|error| LinkError::OpenNamespace { - error, - path: namespace.into(), - })?; +pub struct LinkManager { + router: NlRouter, +} - spawn(move || { - setns(ns_handle, CloneFlags::CLONE_NEWNET).map_err(LinkError::Namespace)?; - link_up(link_name) - }) - .join() - .map_err(|_| LinkError::Panic)? +pub struct Link { + family: RtAddrFamily, + index: c_int, + pub name: String, +} + +impl Link { + fn msg_builder(&self) -> IfinfomsgBuilder { + IfinfomsgBuilder::default() + .ifi_family(self.family) + .ifi_index(self.index) + } +} + +impl LinkManager { + pub fn new() -> Result { + 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) -> Result { + 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>, 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::(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(&self, link: &Link, namespace: Fd) -> Result<(), LinkError> { + let ns_handle = namespace.as_raw_fd(); + + let mut info_attrs = RtBuffer::::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 pub fn link_up(link_name: &str) -> Result<(), LinkError> { - // I honestly don't really know how this code works - // It's mostly a copy from one of neli's examples and seems to do what it needs to - 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 manager = LinkManager::new()?; + let link = manager.get_link(link_name)?; - let recv = rtnl.send::<_, _, Rtm, Ifinfomsg>( - Rtm::Getlink, - NlmF::DUMP | NlmF::ACK, - NlPayload::Payload(ifinfomsg), + let up_msg = link.msg_builder().up().build().unwrap(); + manager.router.send::<_, _, Rtm, Ifinfomsg>( + Rtm::Setlink, + NlmF::ACK, + NlPayload::Payload(up_msg), )?; - for response in recv { - if let Some(payload) = response?.get_payload() { - let name = payload - .rtattrs() - .get_attr_handle() - .get_attr_payload_as_with_len::(Ifla::Ifname) - .map_err(|_| LinkError::Parse)?; - if name == link_name { - let up_msg = IfinfomsgBuilder::default() - .ifi_family(RtAddrFamily::Inet) - .ifi_index(*payload.ifi_index()) - .up() - .build() - .unwrap(); - rtnl.send::<_, _, Rtm, Ifinfomsg>( - Rtm::Setlink, - NlmF::ACK, - NlPayload::Payload(up_msg), - )?; + Ok(()) +} + +/// Move a link into a namespace +pub fn move_link_into(link_name: &str, namespace: &NamespaceHandle) -> Result<(), LinkError> { + let manager = LinkManager::new()?; + let link = manager.get_link(link_name)?; + info!(name = &link.name, "moving link into namespace"); + manager.move_link(&link, namespace) +} + +/// Move all links out of a namespace, except for lo +pub fn move_all_links_out( + namespace: &NamespaceHandle, + parent: &NamespaceHandle, +) -> Result<(), LinkError> { + namespace.run_in(|| { + 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(()) } diff --git a/src/namespace/handle.rs b/src/namespace/handle.rs new file mode 100644 index 0000000..3e73187 --- /dev/null +++ b/src/namespace/handle.rs @@ -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>(path: P) -> Result { + 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::open("/proc/self/ns/net") + } + + pub fn run_in T + Send>(&self, f: F) -> Result { + 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), +} diff --git a/src/namespace/mod.rs b/src/namespace/mod.rs index 5287afb..29e452a 100644 --- a/src/namespace/mod.rs +++ b/src/namespace/mod.rs @@ -1,27 +1,32 @@ +mod handle; mod raw; -use crate::config::NamespaceName; -use crate::link::{LinkError, link_up_ns}; -use crate::namespace::raw::{NamespaceSetupError, create_network_namespace}; +use crate::config::{DeviceName, NamespaceName}; +use crate::link::{link_up, move_all_links_out, move_link_into, LinkError}; +pub use crate::namespace::handle::{NamespaceHandle, NamespaceHandleError}; +use crate::namespace::raw::{create_network_namespace, NamespaceSetupError}; 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 nix::mount::{mount, umount2, MntFlags, MsFlags}; +use std::fs::{create_dir, read_dir, remove_file, 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}; +use tracing::{debug, info}; pub struct NetNs { name: NamespaceName, path: PathBuf, nsd_path: PathBuf, + handle: NamespaceHandle, } impl NetNs { - pub fn existing(include_broken: bool) -> Result, NamespaceError> { + 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 => { @@ -71,10 +76,12 @@ impl NetNs { })?; } + let handle = NamespaceHandle::open(&path)?; return Ok(NetNs { name, nsd_path, path, + handle, }); } Err(e) => return Err(NamespaceError::from_create(path.clone(), e)), @@ -89,10 +96,12 @@ impl NetNs { path: nsd_path.clone(), })?; } + let handle = NamespaceHandle::open(&path)?; Result::<_, NamespaceError>::Ok(NetNs { name, path, nsd_path, + handle, }) })?; @@ -145,11 +154,15 @@ impl NetNs { } 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(()) } 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(); info!(name, "deleting network namespace"); match umount2(&self.path, MntFlags::MNT_DETACH) { @@ -167,6 +180,16 @@ impl NetNs { })?; 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)] @@ -187,10 +210,14 @@ pub enum NamespaceError { 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 }, + #[error(transparent)] + Handle(#[from] NamespaceHandleError), + #[error(transparent)] + Enter(#[from] NamespaceEnterError), + #[error(transparent)] + Link(#[from] LinkError), } 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 fn remove_file_if_exists>(path: P) -> std::io::Result<()> { match remove_file(path) { @@ -207,7 +241,6 @@ fn remove_file_if_exists>(path: P) -> std::io::Result<()> { } } - /// `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) { @@ -215,4 +248,4 @@ fn remove_non_mount>(path: P) -> std::io::Result<()> { Err(err) if err.kind() == ErrorKind::ResourceBusy => Ok(()), rest => rest, } -} \ No newline at end of file +} diff --git a/src/up.rs b/src/up.rs index ad6e763..3bc8a5f 100644 --- a/src/up.rs +++ b/src/up.rs @@ -1,6 +1,7 @@ use crate::config::{Config, NamespaceName}; use crate::namespace::NetNs; use main_error::MainResult; +use tracing::error; pub fn up(config: Config) -> MainResult { let mut namespaces = NetNs::existing(false)? @@ -15,7 +16,13 @@ pub fn up(config: Config) -> MainResult { for new in config.namespaces { 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 { 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) +}