mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-03 17:14:06 +02:00
Compare commits
16 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95d02f5959 | |||
| fd0dda795c | |||
| 19e26b476b | |||
| 1d00c3973d | |||
| 7fca98398b | |||
| 88f168b9e1 | |||
| 68bdbfab9b | |||
| 7588b5db00 | |||
| 35c8f5cc6c | |||
| 41258f44bd | |||
| fbb61b6447 | |||
| 7c00f3ca0d | |||
| d42beb0f64 | |||
| 3fa69dc434 | |||
| 6ecb4b384d | |||
| 9dd802050c |
20 changed files with 1035 additions and 548 deletions
554
Cargo.lock
generated
554
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
29
Cargo.toml
29
Cargo.toml
|
|
@ -1,28 +1,29 @@
|
|||
[package]
|
||||
name = "netnsd"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt", "signal", "net", "io-util"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["signal", "net"] }
|
||||
toml = "0.9.8"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt", "signal", "net", "io-util"] }
|
||||
tokio-stream = { version = "0.1.18", features = ["signal", "net"] }
|
||||
toml = "1.0.1"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.20"
|
||||
clap = { version = "4.5.58", features = ["derive"] }
|
||||
thiserror = "2.0.18"
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.22"
|
||||
main_error = "0.1.2"
|
||||
nix = { version = "0.30.1", features = ["mount", "sched", "user", "signal"] }
|
||||
sd-notify = "0.4.5"
|
||||
nix = { version = "0.31.1", features = ["mount", "sched", "user", "signal"] }
|
||||
sd-notify = "0.5.0"
|
||||
futures = "0.3.31"
|
||||
futures-concurrency = "7.6.3"
|
||||
neli = "0.7.1"
|
||||
futures-concurrency = "7.7.1"
|
||||
neli = "0.7.4"
|
||||
either = "1.15.0"
|
||||
uzers = "0.12.1"
|
||||
sysinfo = "0.37.2"
|
||||
uzers = "0.12.2"
|
||||
sysinfo = "0.38.1"
|
||||
landlock = "0.4.4"
|
||||
cidr = { version = "0.3.2", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_test = "1.0.177"
|
||||
12
README.md
12
README.md
|
|
@ -5,8 +5,11 @@ A declarative manager for Linux network namespaces.
|
|||
## Features
|
||||
|
||||
- Fully declarative configuration
|
||||
- Standalone binary with no runtime dependencies
|
||||
- Hot reloading of configuration
|
||||
- Port forwarding into the namespace
|
||||
- Port forwarding into or out of the namespace
|
||||
- Moving network devices to the namespace
|
||||
- Setting up routing inside the namespace
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -53,6 +56,13 @@ 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"]
|
||||
|
||||
# create a route inside the namespace
|
||||
[[namespace.route]]
|
||||
destination = "default" # either "default" or an ip range in CIDR notation
|
||||
device = "somelink"
|
||||
|
||||
# You can define any number of port forwards to setup into the namespace
|
||||
[[namespace.forward]]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
[[namespace]]
|
||||
# name of the namespace to create
|
||||
name = "test"
|
||||
# move existing devices into the namespace
|
||||
devices = ["somelink"]
|
||||
|
||||
# create a route inside the namespace
|
||||
[[namespace.route]]
|
||||
destination = "default" # either "default" or an ip range in CIDR notation
|
||||
device = "somelink"
|
||||
|
||||
# You can define any number of port forwards to setup into the namespace
|
||||
[[namespace.forward]]
|
||||
|
|
|
|||
32
flake.lock
generated
32
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1763938834,
|
||||
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
|
||||
"lastModified": 1774313767,
|
||||
"narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
|
||||
"rev": "3d9df76e29656c679c744968b17fbaf28f0e923d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -22,11 +22,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764593611,
|
||||
"narHash": "sha256-6SdexcO69Dlu14YN2xuB1A6JHWSrcqMj7Na9oK7IT2M=",
|
||||
"lastModified": 1777298791,
|
||||
"narHash": "sha256-MEQeYwRQcV7RvlKMVrFy07dmoY8t2s/SIK7EpCNLOu8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "flakelight",
|
||||
"rev": "0d63256401341f528dd628f1a8e96d3afecade7a",
|
||||
"rev": "c4b125c5453559de6a228be70f71abeb017f3265",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -44,11 +44,11 @@
|
|||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764619631,
|
||||
"narHash": "sha256-WojMP5S9qLmOLecEQ+7+yc33Ly1ydoRsODNG6hlLqiQ=",
|
||||
"lastModified": 1776692281,
|
||||
"narHash": "sha256-DCCSbpTUDqRiHsTS/sIgCTwa1r4gsJlWSFE6difdWxk=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "0fae557bf52d8493840aca52d433c473ecc305ef",
|
||||
"revCount": 67,
|
||||
"rev": "d1d0e0bb5b0acebed06ccced9cc27f82aafab058",
|
||||
"revCount": 71,
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/icewind/mill-scale"
|
||||
},
|
||||
|
|
@ -59,11 +59,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764522689,
|
||||
"narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=",
|
||||
"lastModified": 1777428379,
|
||||
"narHash": "sha256-ypxFOeDz+CqADEQNL72haqGjvZQdBR5Vc7pyx2JDttI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f",
|
||||
"rev": "755f5aa91337890c432639c60b6064bb7fe67769",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -88,11 +88,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764557621,
|
||||
"narHash": "sha256-kX5PoY8hQZ80+amMQgOO9t8Tc1JZ70gYRnzaVD4AA+o=",
|
||||
"lastModified": 1774535687,
|
||||
"narHash": "sha256-dpKS/8+uB0EoI4mCrpio+xs8Xxry6ZhLLwV8VIbbfrs=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "93316876c2229460a5d6f5f052766cc4cef538ce",
|
||||
"rev": "75900435aa883f84b038316864b3f60956681523",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -45,11 +45,37 @@ in {
|
|||
type = types.oneOf [types.port types.str];
|
||||
description = "target port or address inside the namespace";
|
||||
};
|
||||
reverse = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "forward from inside the namespace to outside instead";
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "ports to forward into the namespace";
|
||||
default = [];
|
||||
};
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "devices to move into the namespace";
|
||||
};
|
||||
route = mkOption {
|
||||
type = types.listOf (types.submodule ({config, ...}: {
|
||||
options = {
|
||||
device = mkOption {
|
||||
type = types.str;
|
||||
description = "device to route the traffic trough";
|
||||
};
|
||||
destination = mkOption {
|
||||
type = types.str;
|
||||
description = "What traffic to route. Either \"default\" or an ip range in CIDR notation";
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "routes to setup inside the namespace";
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = "namespaces to setup";
|
||||
|
|
@ -61,7 +87,7 @@ in {
|
|||
# symlink instead of passing `configFile` directly to netnsd to allow changing the config without changing the path
|
||||
environment.etc."netnsd/netnsd.toml".source = configFile;
|
||||
|
||||
environment.systemPackages = with pkgs; [cfg.package];
|
||||
environment.systemPackages = [cfg.package];
|
||||
|
||||
systemd.services.netnsd = {
|
||||
reloadTriggers = [configFile];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
rustc = rust-bin.stable.latest.minimal;
|
||||
};
|
||||
in
|
||||
rustPlatform.buildRustPackage rec {
|
||||
rustPlatform.buildRustPackage {
|
||||
pname = cargoPackage.name;
|
||||
inherit (cargoPackage) version;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@ 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;
|
||||
use cidr::AnyIpCidr;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use toml::from_str;
|
||||
|
||||
|
|
@ -80,7 +85,12 @@ impl RawConfig {
|
|||
#[derive(Deserialize, Debug)]
|
||||
pub struct NamespaceConfig {
|
||||
pub name: NamespaceName,
|
||||
#[serde(default)]
|
||||
pub forward: Vec<ForwardConfig>,
|
||||
#[serde(default)]
|
||||
pub devices: Vec<DeviceName>,
|
||||
#[serde(default, rename = "route")]
|
||||
pub routes: Vec<RouteConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
|
@ -91,6 +101,27 @@ pub struct ForwardConfig {
|
|||
pub reverse: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct RouteConfig {
|
||||
#[serde(deserialize_with = "parse_cidr")]
|
||||
pub destination: AnyIpCidr,
|
||||
pub device: DeviceName,
|
||||
}
|
||||
|
||||
impl Display for RouteConfig {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} dev {}", self.destination, self.device)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cidr<'de, D: Deserializer<'de>>(deserializer: D) -> Result<AnyIpCidr, D::Error> {
|
||||
let str = Cow::<'de, str>::deserialize(deserializer)?;
|
||||
match str.as_ref() {
|
||||
"default" => Ok(AnyIpCidr::Any),
|
||||
str => AnyIpCidr::from_str(str).map_err(D::Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("Error while reading config from {}: {error:#}", path.display())]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
143
src/daemon.rs
143
src/daemon.rs
|
|
@ -1,5 +1,10 @@
|
|||
use crate::config::{Config, ForwardConfig, NamespaceConfig, NamespaceName};
|
||||
use crate::namespace::{NamespaceError, NetNs};
|
||||
use crate::config::{
|
||||
Config, DeviceName, ForwardConfig, NamespaceConfig, NamespaceName, RouteConfig,
|
||||
};
|
||||
use crate::link::{LinkError, LinkManager};
|
||||
use crate::namespace::{
|
||||
NamespaceEnterError, NamespaceError, NamespaceHandle, NamespaceHandleError, NetNs,
|
||||
};
|
||||
use crate::proxy::{ActiveProxy, ProxyError};
|
||||
use futures::FutureExt;
|
||||
use futures::StreamExt;
|
||||
|
|
@ -25,7 +30,15 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
|||
state.update(&config)?;
|
||||
|
||||
// now the namespaces are setup, we can tell systemd to start any service depending on them
|
||||
notify(false, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||
notify(&[
|
||||
NotifyState::Ready,
|
||||
NotifyState::Status(&format!(
|
||||
"Started with {} namespaces",
|
||||
state.namespaces.len()
|
||||
)),
|
||||
])
|
||||
.map_err(DaemonError::Notify)?;
|
||||
info!("ready");
|
||||
|
||||
let reload_signal = signal(SignalKind::hangup()).map_err(DaemonError::Signal)?;
|
||||
let reload_signal = SignalStream::new(reload_signal).map(|_| Event::Reload);
|
||||
|
|
@ -52,7 +65,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
|||
|
||||
match NotifyState::monotonic_usec_now() {
|
||||
Ok(notify_time) => {
|
||||
notify(false, &[NotifyState::Reloading, notify_time])
|
||||
notify(&[NotifyState::Reloading, notify_time])
|
||||
.map_err(DaemonError::Notify)?;
|
||||
}
|
||||
Err(error) => {
|
||||
|
|
@ -70,7 +83,15 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
|||
}
|
||||
}
|
||||
|
||||
notify(false, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||
notify(&[
|
||||
NotifyState::Ready,
|
||||
NotifyState::Status(&format!(
|
||||
"Reloaded with {} namespaces",
|
||||
state.namespaces.len()
|
||||
)),
|
||||
])
|
||||
.map_err(DaemonError::Notify)?;
|
||||
info!("reloaded");
|
||||
}
|
||||
Event::Info => {
|
||||
for namespace in &state.namespaces {
|
||||
|
|
@ -83,7 +104,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
|||
}
|
||||
}
|
||||
|
||||
let _ = notify(false, &[NotifyState::Stopping]);
|
||||
let _ = notify(&[NotifyState::Stopping]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -101,7 +122,7 @@ struct State {
|
|||
|
||||
impl State {
|
||||
pub fn new() -> Result<Self, DaemonError> {
|
||||
let namespaces = NetNs::existing()?
|
||||
let namespaces = NetNs::existing(false)?
|
||||
.map(ActiveNamespace::new)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(State { namespaces })
|
||||
|
|
@ -124,6 +145,8 @@ impl State {
|
|||
for namespace in &mut self.namespaces {
|
||||
let config = config.get_namespace(namespace.name()).unwrap();
|
||||
namespace.update_proxies(config)?;
|
||||
namespace.update_devices(config)?;
|
||||
namespace.update_links(config)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -139,6 +162,8 @@ impl State {
|
|||
struct ActiveNamespace {
|
||||
ns: NetNs,
|
||||
proxies: Vec<ActiveProxy>,
|
||||
devices: Vec<DeviceName>,
|
||||
routes: Vec<RouteConfig>,
|
||||
}
|
||||
|
||||
impl ActiveNamespace {
|
||||
|
|
@ -148,6 +173,8 @@ impl ActiveNamespace {
|
|||
Ok(ActiveNamespace {
|
||||
ns,
|
||||
proxies: Vec::default(),
|
||||
devices: Vec::default(),
|
||||
routes: Vec::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -164,10 +191,106 @@ 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(())
|
||||
}
|
||||
|
||||
pub fn update_links(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> {
|
||||
let removed: Vec<_> = self
|
||||
.routes
|
||||
.extract_if(.., |existing| {
|
||||
!config.routes.iter().any(|new| existing == new)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut added = Vec::new();
|
||||
for new in &config.routes {
|
||||
if !self.has_route(new) {
|
||||
added.push(new.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.ns.handle().run_in(|| {
|
||||
let link_manager = LinkManager::new()?;
|
||||
for link in link_manager.get_links()?.flatten() {
|
||||
if let Some(route) = removed
|
||||
.iter()
|
||||
.find(|route| route.device == link.name.as_str())
|
||||
{
|
||||
info!(namespace = %config.name, %route, "deleting route");
|
||||
link_manager.delete_route(&link, route.destination)?;
|
||||
}
|
||||
}
|
||||
|
||||
for link in link_manager.get_links()?.flatten() {
|
||||
if let Some(route) = added
|
||||
.iter()
|
||||
.find(|route| route.device == link.name.as_str())
|
||||
{
|
||||
info!(namespace = %config.name, %route, "adding route");
|
||||
link_manager.add_route(&link, route.destination)?;
|
||||
}
|
||||
}
|
||||
Ok::<_, DaemonError>(())
|
||||
})??;
|
||||
|
||||
for new in added {
|
||||
self.routes.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)
|
||||
}
|
||||
|
||||
fn has_route(&self, route: &RouteConfig) -> bool {
|
||||
self.routes.iter().any(|existing| existing == route)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &NamespaceName {
|
||||
self.ns.name()
|
||||
}
|
||||
|
|
@ -183,4 +306,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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use crate::namespace::NetNs;
|
|||
use main_error::MainResult;
|
||||
|
||||
pub fn down() -> MainResult {
|
||||
for name in NetNs::existing()? {
|
||||
for name in NetNs::existing(true)? {
|
||||
let ns = NetNs::new(name)?;
|
||||
ns.delete()?
|
||||
}
|
||||
|
|
|
|||
311
src/link.rs
311
src/link.rs
|
|
@ -1,96 +1,265 @@
|
|||
use cidr::{AnyIpCidr, Family};
|
||||
use neli::consts::nl::NlmF;
|
||||
use neli::consts::rtnl::Ifla;
|
||||
use neli::consts::rtnl::RtAddrFamily;
|
||||
use neli::consts::rtnl::Rtm;
|
||||
use neli::consts::rtnl::{Ifla, RtScope, RtTable, Rta};
|
||||
use neli::consts::rtnl::{RtAddrFamily, Rtn, Rtprot};
|
||||
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, Rtmsg};
|
||||
use neli::rtnl::{IfinfomsgBuilder, RtmsgBuilder};
|
||||
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::fmt::{Debug, Display, Formatter};
|
||||
use std::os::fd::AsRawFd;
|
||||
use thiserror::Error;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LinkError {
|
||||
#[error("failed to communicate with netlink")]
|
||||
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("Failed to communicate with netlink: {0}")]
|
||||
Netlink(String),
|
||||
#[error("Failed to parse netlink response: {0}")]
|
||||
Parse(String),
|
||||
#[error("Link not found: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
impl<T, P> From<RouterError<T, P>> for LinkError {
|
||||
fn from(_value: RouterError<T, P>) -> Self {
|
||||
LinkError::Netlink
|
||||
impl<T, P> From<RouterError<T, P>> for LinkError
|
||||
where
|
||||
T: Debug,
|
||||
P: Debug,
|
||||
{
|
||||
fn from(value: RouterError<T, P>) -> Self {
|
||||
LinkError::Netlink(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Display for Link {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
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(|e| LinkError::Parse(e.to_string()))?;
|
||||
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(ns_handle)
|
||||
.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(())
|
||||
}
|
||||
|
||||
pub fn up(&self, link: &Link) -> Result<(), LinkError> {
|
||||
let up_msg = link.msg_builder().up().build().unwrap();
|
||||
self.router.send::<_, _, Rtm, Ifinfomsg>(
|
||||
Rtm::Setlink,
|
||||
NlmF::ACK,
|
||||
NlPayload::Payload(up_msg),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(link = %link, destination = %destination))]
|
||||
pub fn add_route(&self, link: &Link, destination: AnyIpCidr) -> Result<(), LinkError> {
|
||||
let rt_msg = route_message_for(link, destination);
|
||||
|
||||
let res = self.router.send::<_, _, Rtm, Rtmsg>(
|
||||
Rtm::Newroute,
|
||||
NlmF::CREATE | NlmF::EXCL | NlmF::REQUEST | NlmF::ACK,
|
||||
NlPayload::Payload(rt_msg),
|
||||
)?;
|
||||
|
||||
for msg in res {
|
||||
match msg {
|
||||
Err(RouterError::Nlmsgerr(err)) if *err.error() == -17 => {
|
||||
info!("route already exists");
|
||||
// already exists
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(link = %link, destination = %destination))]
|
||||
pub fn delete_route(&self, link: &Link, destination: AnyIpCidr) -> Result<(), LinkError> {
|
||||
let rt_msg = route_message_for(link, destination);
|
||||
|
||||
let res = self.router.send::<_, _, Rtm, Rtmsg>(
|
||||
Rtm::Delroute,
|
||||
NlmF::REQUEST | NlmF::ACK,
|
||||
NlPayload::Payload(rt_msg),
|
||||
)?;
|
||||
|
||||
for msg in res {
|
||||
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)?;
|
||||
manager.up(&link)
|
||||
}
|
||||
|
||||
let recv = rtnl.send::<_, _, Rtm, Ifinfomsg>(
|
||||
Rtm::Getlink,
|
||||
NlmF::DUMP | NlmF::ACK,
|
||||
NlPayload::Payload(ifinfomsg),
|
||||
)?;
|
||||
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),
|
||||
)?;
|
||||
}
|
||||
/// Move a link into a namespace
|
||||
pub fn move_link_into<Fd: AsRawFd>(link_name: &str, namespace: &Fd) -> Result<(), LinkError> {
|
||||
let manager = LinkManager::new()?;
|
||||
// todo, might already be in target ns
|
||||
let link = manager.get_link(link_name)?;
|
||||
info!(name = &link.name, "moving link into namespace");
|
||||
manager.move_link(&link, namespace)
|
||||
}
|
||||
|
||||
/// Move all links from the current namespace (except lo) into a namespace
|
||||
pub fn move_all_links<Fd: AsRawFd>(namespace: &Fd) -> Result<(), LinkError> {
|
||||
let manager = LinkManager::new()?;
|
||||
for link in manager.get_links()?.flatten() {
|
||||
if link.name != "lo" {
|
||||
info!(name = &link.name, "moving link");
|
||||
manager.move_link(&link, namespace)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok::<_, LinkError>(())
|
||||
}
|
||||
|
||||
fn route_message_for(link: &Link, destination: AnyIpCidr) -> Rtmsg {
|
||||
let mut info_attrs = RtBuffer::<Rta, Buffer>::new();
|
||||
match &destination {
|
||||
AnyIpCidr::V4(addr) => {
|
||||
info_attrs.push(
|
||||
RtattrBuilder::default()
|
||||
.rta_type(Rta::Dst)
|
||||
.rta_payload(addr.first_address().octets())
|
||||
.build()
|
||||
.expect("invalid rtattr"),
|
||||
);
|
||||
}
|
||||
AnyIpCidr::V6(addr) => {
|
||||
info_attrs.push(
|
||||
RtattrBuilder::default()
|
||||
.rta_type(Rta::Dst)
|
||||
.rta_payload(addr.first_address().octets())
|
||||
.build()
|
||||
.expect("invalid rtattr"),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
info_attrs.push(
|
||||
RtattrBuilder::default()
|
||||
.rta_type(Rta::Oif)
|
||||
.rta_payload(link.index)
|
||||
.build()
|
||||
.expect("invalid rtattr"),
|
||||
);
|
||||
|
||||
let family = match &destination.family() {
|
||||
None => RtAddrFamily::Inet,
|
||||
Some(Family::Ipv4) => RtAddrFamily::Inet,
|
||||
Some(Family::Ipv6) => RtAddrFamily::Inet6,
|
||||
};
|
||||
|
||||
RtmsgBuilder::default()
|
||||
.rtm_table(RtTable::Main)
|
||||
.rtm_scope(RtScope::Universe)
|
||||
.rtm_family(family)
|
||||
.rtattrs(info_attrs)
|
||||
.rtm_src_len(0)
|
||||
.rtm_tos(0)
|
||||
.rtm_protocol(Rtprot::Boot)
|
||||
.rtm_type(Rtn::Unicast)
|
||||
.rtm_dst_len(destination.network_length().unwrap_or_default())
|
||||
.build()
|
||||
.expect("rt msg")
|
||||
}
|
||||
|
|
|
|||
12
src/main.rs
12
src/main.rs
|
|
@ -5,10 +5,10 @@ use crate::proxy::proxy;
|
|||
use crate::up::up;
|
||||
use clap::{Parser, Subcommand};
|
||||
use main_error::MainResult;
|
||||
use std::path::PathBuf;
|
||||
use nix::errno::Errno;
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::sys::signal::{Signal, kill};
|
||||
use nix::unistd::Pid;
|
||||
use std::path::PathBuf;
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
|
|
@ -94,10 +94,12 @@ fn reload() -> MainResult {
|
|||
match kill(Pid::from_raw(proc.pid().as_u32() as i32), Signal::SIGHUP) {
|
||||
Ok(_) => {
|
||||
info!("Sent reload command to daemon")
|
||||
},
|
||||
}
|
||||
Err(Errno::EPERM) => {
|
||||
error!("Sending signal not permitted, try are you running the command as root?");
|
||||
},
|
||||
error!(
|
||||
"Sending signal not permitted, try are you running the command as root?"
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
error!(%error, "Unexpected error");
|
||||
}
|
||||
|
|
|
|||
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,8 +1,12 @@
|
|||
mod handle;
|
||||
mod raw;
|
||||
mod sysctl;
|
||||
|
||||
use crate::config::NamespaceName;
|
||||
use crate::link::{link_up_ns, LinkError};
|
||||
use crate::config::{DeviceName, NamespaceName};
|
||||
use crate::link::{link_up, move_all_links, move_link_into, LinkError};
|
||||
pub use crate::namespace::handle::{NamespaceHandle, NamespaceHandleError};
|
||||
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError};
|
||||
use crate::namespace::sysctl::{CtlError, NamespaceCtl};
|
||||
use either::Either;
|
||||
use nix::errno::Errno;
|
||||
use nix::mount::{mount, umount2, MntFlags, MsFlags};
|
||||
|
|
@ -12,16 +16,19 @@ 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() -> 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 => {
|
||||
|
|
@ -32,12 +39,14 @@ impl NetNs {
|
|||
error,
|
||||
}),
|
||||
}?;
|
||||
Ok(Either::Right(dir.flatten().flat_map(|entry| {
|
||||
NamespaceName::try_from(entry.file_name()).ok()
|
||||
})))
|
||||
Ok(Either::Right(
|
||||
dir.flatten()
|
||||
.filter(move |entry| include_broken || entry.path().is_symlink())
|
||||
.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
|
||||
pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> {
|
||||
let parent = Path::new("/var/run/netns");
|
||||
let nsd_parent = Path::new("/var/run/netnsd");
|
||||
|
|
@ -47,22 +56,34 @@ impl NetNs {
|
|||
let path = parent.join(&name);
|
||||
let nsd_path = nsd_parent.join(&name);
|
||||
|
||||
remove_non_mount(&path).map_err(|error| NamespaceError::Delete {
|
||||
error,
|
||||
path: nsd_path.clone(),
|
||||
})?;
|
||||
|
||||
match File::create_new(&path) {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
|
||||
info!(%name, "using existing network namespace");
|
||||
|
||||
if !nsd_path.is_symlink() {
|
||||
remove_file_if_exists(&nsd_path).map_err(|error| NamespaceError::Delete {
|
||||
error,
|
||||
path: nsd_path.clone(),
|
||||
})?;
|
||||
|
||||
symlink(&path, &nsd_path).map_err(|error| NamespaceError::Symlink {
|
||||
error,
|
||||
path: nsd_path.clone(),
|
||||
})?;
|
||||
}
|
||||
|
||||
let handle = NamespaceHandle::open(&path)?;
|
||||
return Ok(NetNs {
|
||||
name,
|
||||
nsd_path,
|
||||
path,
|
||||
handle,
|
||||
});
|
||||
}
|
||||
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
|
||||
|
|
@ -77,10 +98,12 @@ impl NetNs {
|
|||
path: nsd_path.clone(),
|
||||
})?;
|
||||
}
|
||||
let handle = NamespaceHandle::open(&path)?;
|
||||
Result::<_, NamespaceError>::Ok(NetNs {
|
||||
name,
|
||||
path,
|
||||
nsd_path,
|
||||
handle,
|
||||
})
|
||||
})?;
|
||||
|
||||
|
|
@ -133,24 +156,46 @@ impl NetNs {
|
|||
}
|
||||
|
||||
fn setup_interfaces(&self) -> Result<(), NamespaceError> {
|
||||
link_up_ns(&self.path, "lo")?;
|
||||
let ctl = NamespaceCtl::read()?;
|
||||
self.handle.run_in(move || {
|
||||
link_up("lo").map_err(NamespaceError::from)?;
|
||||
dbg!(ctl).apply()?;
|
||||
Ok::<_, NamespaceError>(())
|
||||
})??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(self) -> Result<(), NamespaceError> {
|
||||
let parent_namespace = NamespaceHandle::parent()?;
|
||||
|
||||
self.handle.run_in(|| move_all_links(&parent_namespace))??;
|
||||
let name = self.path.file_name().unwrap().to_str().unwrap();
|
||||
info!(name, "deleting network namespace");
|
||||
umount2(&self.path, MntFlags::MNT_DETACH).map_err(NamespaceError::UnMount)?;
|
||||
remove_file(&self.path).map_err(|error| NamespaceError::Delete {
|
||||
match umount2(&self.path, MntFlags::MNT_DETACH) {
|
||||
Err(Errno::EINVAL) => Ok(()), // not a mountpoint, namespace doesn't exist
|
||||
rest => rest,
|
||||
}
|
||||
.map_err(NamespaceError::UnMount)?;
|
||||
remove_file_if_exists(&self.path).map_err(|error| NamespaceError::Delete {
|
||||
error,
|
||||
path: self.path,
|
||||
})?;
|
||||
remove_file(&self.nsd_path).map_err(|error| NamespaceError::Delete {
|
||||
remove_file_if_exists(&self.nsd_path).map_err(|error| NamespaceError::Delete {
|
||||
error,
|
||||
path: self.nsd_path,
|
||||
})?;
|
||||
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)]
|
||||
|
|
@ -171,10 +216,16 @@ 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),
|
||||
#[error("Failed to setup sysctl for namespace: {0:#}")]
|
||||
Sysctl(#[from] CtlError),
|
||||
}
|
||||
|
||||
impl NamespaceError {
|
||||
|
|
@ -182,3 +233,27 @@ impl NamespaceError {
|
|||
NamespaceError::Create { path, error }
|
||||
}
|
||||
}
|
||||
|
||||
#[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) {
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
|
||||
rest => rest,
|
||||
}
|
||||
}
|
||||
|
||||
/// `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) {
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
|
||||
Err(err) if err.kind() == ErrorKind::ResourceBusy => Ok(()),
|
||||
rest => rest,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use nix::sys::signal::Signal;
|
|||
use nix::sys::wait::{waitpid, WaitStatus};
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NamespaceSetupError {
|
||||
|
|
|
|||
90
src/namespace/sysctl.rs
Normal file
90
src/namespace/sysctl.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use std::fmt::Display;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::io::{Error as IoError, ErrorKind, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NamespaceCtl {
|
||||
use_temp_addr: u32,
|
||||
temp_valid_lft: u32,
|
||||
temp_preferred_lft: u32,
|
||||
}
|
||||
|
||||
const PATH_TEMP_ADDR: &str = "/proc/sys/net/ipv6/conf/default/use_tempaddr";
|
||||
const PATH_VALID_LFT: &str = "/proc/sys/net/ipv6/conf/default/temp_valid_lft";
|
||||
const PATH_PREFERRED_LFT: &str = "/proc/sys/net/ipv6/conf/default/temp_prefered_lft";
|
||||
|
||||
impl NamespaceCtl {
|
||||
pub fn read() -> Result<NamespaceCtl, CtlError> {
|
||||
Ok(NamespaceCtl {
|
||||
use_temp_addr: read_sysctl(PATH_TEMP_ADDR)?,
|
||||
temp_valid_lft: read_sysctl(PATH_VALID_LFT)?,
|
||||
temp_preferred_lft: read_sysctl(PATH_PREFERRED_LFT)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self) -> Result<(), CtlError> {
|
||||
write_sysctl(PATH_TEMP_ADDR, self.use_temp_addr)?;
|
||||
write_sysctl(PATH_VALID_LFT, self.temp_valid_lft)?;
|
||||
write_sysctl(PATH_PREFERRED_LFT, self.temp_preferred_lft)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_sysctl<T: FromStr, P: AsRef<Path>>(path: P) -> Result<T, CtlError>
|
||||
where
|
||||
<T as FromStr>::Err: Display,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let mut buff = [0; 16];
|
||||
let mut file = File::open(path).map_err(|error| CtlError::Read {
|
||||
path: path.into(),
|
||||
error,
|
||||
})?;
|
||||
let read = file.read(&mut buff).map_err(|error| CtlError::Read {
|
||||
path: path.into(),
|
||||
error,
|
||||
})?;
|
||||
let data = &buff[0..read];
|
||||
let str = str::from_utf8(data)
|
||||
.map_err(|_| CtlError::Read {
|
||||
path: path.into(),
|
||||
error: IoError::new(ErrorKind::InvalidData, "stream did not contain valid UTF-8"),
|
||||
})?
|
||||
.trim();
|
||||
T::from_str(str).map_err(|error| CtlError::Parse {
|
||||
path: path.into(),
|
||||
error: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_sysctl<T: Display, P: AsRef<Path>>(path: P, value: T) -> Result<(), CtlError> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(false)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.map_err(|error| CtlError::Write {
|
||||
path: path.into(),
|
||||
error,
|
||||
})?;
|
||||
writeln!(&mut file, "{value}").map_err(|error| CtlError::Write {
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CtlError {
|
||||
#[error("failed to read sysctl option {}: {error:#}", path.display())]
|
||||
Read { path: PathBuf, error: IoError },
|
||||
#[error("failed to read sysctl option {}: {error}", path.display())]
|
||||
Parse { path: PathBuf, error: String },
|
||||
#[error("failed to write sysctl option {}: {error:#}", path.display())]
|
||||
Write { path: PathBuf, error: IoError },
|
||||
}
|
||||
|
|
@ -3,7 +3,12 @@ mod tcp;
|
|||
use crate::config::{ForwardConfig, ForwardSource, ForwardTarget, NamespaceName};
|
||||
use crate::proxy::tcp::Proxy;
|
||||
use futures::future::AbortHandle;
|
||||
use landlock::{
|
||||
ABI, Access, AccessFs, AccessNet, NetPort, Ruleset, RulesetAttr, RulesetCreatedAttr,
|
||||
RulesetError, RulesetStatus,
|
||||
};
|
||||
use main_error::MainResult;
|
||||
use nix::errno::Errno;
|
||||
use nix::sched::{CloneFlags, setns};
|
||||
use nix::sys::signal::{SIGINT, kill};
|
||||
use nix::unistd::{Gid, Pid, Uid, setgid, setuid};
|
||||
|
|
@ -13,8 +18,6 @@ use std::net::SocketAddr;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command};
|
||||
use std::thread::spawn;
|
||||
use landlock::{Access, AccessFs, AccessNet, NetPort, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus, ABI};
|
||||
use nix::errno::Errno;
|
||||
use thiserror::Error;
|
||||
use tokio::runtime::Builder;
|
||||
use tokio::signal::ctrl_c;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/// Loosely based on https://github.com/fooker/netns-proxy/blob/main/src/tcp.rs
|
||||
use crate::config::{ForwardTarget, ForwardSource};
|
||||
use crate::proxy::{ProxyError};
|
||||
use crate::config::{ForwardSource, ForwardTarget};
|
||||
use crate::proxy::ProxyError;
|
||||
use futures::TryStreamExt;
|
||||
use futures::stream::{AbortRegistration, Abortable};
|
||||
use std::fs::{remove_file, set_permissions};
|
||||
|
|
@ -56,28 +56,18 @@ impl Proxy {
|
|||
})?;
|
||||
debug!("Created TCP socket");
|
||||
|
||||
Ok(Self {
|
||||
socket,
|
||||
})
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn run(self, target: ForwardTarget, abort: AbortRegistration) {
|
||||
match self.socket {
|
||||
ProxyListener::Tcp(socket) => {
|
||||
run_tcp(socket, target.addr, abort).await
|
||||
}
|
||||
ProxyListener::Unix(socket) => {
|
||||
run_unix(socket, target.addr, abort).await
|
||||
}
|
||||
ProxyListener::Tcp(socket) => run_tcp(socket, target.addr, abort).await,
|
||||
ProxyListener::Unix(socket) => run_unix(socket, target.addr, abort).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_tcp(
|
||||
socket: TcpListener,
|
||||
target: SocketAddr,
|
||||
abort: AbortRegistration,
|
||||
) {
|
||||
async fn run_tcp(socket: TcpListener, target: SocketAddr, abort: AbortRegistration) {
|
||||
let accepts = TcpListenerStream::new(socket).map_err(|error| ProxyError::Accept { error });
|
||||
let mut accepts = pin!(Abortable::new(accepts, abort));
|
||||
while let Some(client) = accepts.next().await {
|
||||
|
|
@ -94,11 +84,7 @@ async fn run_tcp(
|
|||
}
|
||||
}
|
||||
|
||||
async fn run_unix(
|
||||
socket: UnixListener,
|
||||
target: SocketAddr,
|
||||
abort: AbortRegistration,
|
||||
) {
|
||||
async fn run_unix(socket: UnixListener, target: SocketAddr, abort: AbortRegistration) {
|
||||
let accepts = UnixListenerStream::new(socket).map_err(|error| ProxyError::Accept { error });
|
||||
let mut accepts = pin!(Abortable::new(accepts, abort));
|
||||
while let Some(client) = accepts.next().await {
|
||||
|
|
|
|||
24
src/up.rs
24
src/up.rs
|
|
@ -1,9 +1,11 @@
|
|||
use crate::config::{Config, NamespaceName};
|
||||
use crate::link::{LinkError, LinkManager};
|
||||
use crate::namespace::NetNs;
|
||||
use main_error::MainResult;
|
||||
use tracing::error;
|
||||
|
||||
pub fn up(config: Config) -> MainResult {
|
||||
let mut namespaces = NetNs::existing()?
|
||||
let mut namespaces = NetNs::existing(false)?
|
||||
.map(NetNs::new)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
|
|
@ -15,7 +17,21 @@ 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");
|
||||
}
|
||||
}
|
||||
for route in new.routes {
|
||||
namespace.handle().run_in(|| {
|
||||
let manager = LinkManager::new()?;
|
||||
let link = manager.get_link(&route.device)?;
|
||||
manager.add_route(&link, route.destination)?;
|
||||
Ok::<_, LinkError>(())
|
||||
})??;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -25,3 +41,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