mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-03 09:04:07 +02:00
add support for moving devices into the namespace
This commit is contained in:
parent
6ecb4b384d
commit
3fa69dc434
11 changed files with 411 additions and 88 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -507,7 +507,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "netnsd"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"either",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "netnsd"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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<ForwardConfig>,
|
||||
#[serde(default)]
|
||||
pub devices: Vec<DeviceName>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
|
|
|||
|
|
@ -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<ValidatedName> for NamespaceName {
|
||||
fn from(value: ValidatedName) -> Self {
|
||||
NamespaceName(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<OsString> for NamespaceName {
|
||||
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>
|
||||
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<E>(self, v: String) -> Result<Self::Value, E>
|
||||
|
|
@ -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<Self, Self::Err> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<ActiveProxy>,
|
||||
devices: Vec<DeviceName>,
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
|
|
|||
188
src/link.rs
188
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<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 fn link_up_ns(namespace: impl AsRef<Path>, 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<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
|
||||
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::<String>(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(())
|
||||
}
|
||||
|
|
|
|||
76
src/namespace/handle.rs
Normal file
76
src/namespace/handle.rs
Normal 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),
|
||||
}
|
||||
|
|
@ -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<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") {
|
||||
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<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
|
||||
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
|
||||
fn remove_non_mount<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
|
||||
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(()),
|
||||
rest => rest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
src/up.rs
13
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue