mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-04 01:24:07 +02:00
Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
20 changed files with 548 additions and 1035 deletions
562
Cargo.lock
generated
562
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
29
Cargo.toml
29
Cargo.toml
|
|
@ -1,29 +1,28 @@
|
||||||
[package]
|
[package]
|
||||||
name = "netnsd"
|
name = "netnsd"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88.0"
|
rust-version = "1.88.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.49.0", features = ["macros", "rt", "signal", "net", "io-util"] }
|
tokio = { version = "1.48.0", features = ["macros", "rt", "signal", "net", "io-util"] }
|
||||||
tokio-stream = { version = "0.1.18", features = ["signal", "net"] }
|
tokio-stream = { version = "0.1.17", features = ["signal", "net"] }
|
||||||
toml = "1.0.1"
|
toml = "0.9.8"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
clap = { version = "4.5.58", features = ["derive"] }
|
clap = { version = "4.5.51", features = ["derive"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.17"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.22"
|
tracing-subscriber = "0.3.20"
|
||||||
main_error = "0.1.2"
|
main_error = "0.1.2"
|
||||||
nix = { version = "0.31.1", features = ["mount", "sched", "user", "signal"] }
|
nix = { version = "0.30.1", features = ["mount", "sched", "user", "signal"] }
|
||||||
sd-notify = "0.5.0"
|
sd-notify = "0.4.5"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
futures-concurrency = "7.7.1"
|
futures-concurrency = "7.6.3"
|
||||||
neli = "0.7.4"
|
neli = "0.7.1"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
uzers = "0.12.2"
|
uzers = "0.12.1"
|
||||||
sysinfo = "0.38.1"
|
sysinfo = "0.37.2"
|
||||||
landlock = "0.4.4"
|
landlock = "0.4.4"
|
||||||
cidr = { version = "0.3.2", features = ["serde"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_test = "1.0.177"
|
serde_test = "1.0.177"
|
||||||
12
README.md
12
README.md
|
|
@ -5,11 +5,8 @@ A declarative manager for Linux network namespaces.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Fully declarative configuration
|
- Fully declarative configuration
|
||||||
- Standalone binary with no runtime dependencies
|
|
||||||
- Hot reloading of configuration
|
- Hot reloading of configuration
|
||||||
- Port forwarding into or out of the namespace
|
- Port forwarding into the namespace
|
||||||
- Moving network devices to the namespace
|
|
||||||
- Setting up routing inside the namespace
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
@ -56,13 +53,6 @@ You can specify a different configuration path with the `--config` option.
|
||||||
[[namespace]]
|
[[namespace]]
|
||||||
# name of the namespace to create
|
# name of the namespace to create
|
||||||
name = "test"
|
name = "test"
|
||||||
# move existing devices into the namespace
|
|
||||||
devices = ["somelink"]
|
|
||||||
|
|
||||||
# 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
|
# You can define any number of port forwards to setup into the namespace
|
||||||
[[namespace.forward]]
|
[[namespace.forward]]
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,6 @@
|
||||||
[[namespace]]
|
[[namespace]]
|
||||||
# name of the namespace to create
|
# name of the namespace to create
|
||||||
name = "test"
|
name = "test"
|
||||||
# move existing devices into the namespace
|
|
||||||
devices = ["somelink"]
|
|
||||||
|
|
||||||
# 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
|
# You can define any number of port forwards to setup into the namespace
|
||||||
[[namespace.forward]]
|
[[namespace.forward]]
|
||||||
|
|
|
||||||
32
flake.lock
generated
32
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774313767,
|
"lastModified": 1763938834,
|
||||||
"narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=",
|
"narHash": "sha256-j8iB0Yr4zAvQLueCZ5abxfk6fnG/SJ5JnGUziETjwfg=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "3d9df76e29656c679c744968b17fbaf28f0e923d",
|
"rev": "d9e753122e51cee64eb8d2dddfe11148f339f5a2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -22,11 +22,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777298791,
|
"lastModified": 1764593611,
|
||||||
"narHash": "sha256-MEQeYwRQcV7RvlKMVrFy07dmoY8t2s/SIK7EpCNLOu8=",
|
"narHash": "sha256-6SdexcO69Dlu14YN2xuB1A6JHWSrcqMj7Na9oK7IT2M=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "flakelight",
|
"repo": "flakelight",
|
||||||
"rev": "c4b125c5453559de6a228be70f71abeb017f3265",
|
"rev": "0d63256401341f528dd628f1a8e96d3afecade7a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -44,11 +44,11 @@
|
||||||
"rust-overlay": "rust-overlay"
|
"rust-overlay": "rust-overlay"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776692281,
|
"lastModified": 1764619631,
|
||||||
"narHash": "sha256-DCCSbpTUDqRiHsTS/sIgCTwa1r4gsJlWSFE6difdWxk=",
|
"narHash": "sha256-WojMP5S9qLmOLecEQ+7+yc33Ly1ydoRsODNG6hlLqiQ=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"rev": "d1d0e0bb5b0acebed06ccced9cc27f82aafab058",
|
"rev": "0fae557bf52d8493840aca52d433c473ecc305ef",
|
||||||
"revCount": 71,
|
"revCount": 67,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://codeberg.org/icewind/mill-scale"
|
"url": "https://codeberg.org/icewind/mill-scale"
|
||||||
},
|
},
|
||||||
|
|
@ -59,11 +59,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777428379,
|
"lastModified": 1764522689,
|
||||||
"narHash": "sha256-ypxFOeDz+CqADEQNL72haqGjvZQdBR5Vc7pyx2JDttI=",
|
"narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "755f5aa91337890c432639c60b6064bb7fe67769",
|
"rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -88,11 +88,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1774535687,
|
"lastModified": 1764557621,
|
||||||
"narHash": "sha256-dpKS/8+uB0EoI4mCrpio+xs8Xxry6ZhLLwV8VIbbfrs=",
|
"narHash": "sha256-kX5PoY8hQZ80+amMQgOO9t8Tc1JZ70gYRnzaVD4AA+o=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "75900435aa883f84b038316864b3f60956681523",
|
"rev": "93316876c2229460a5d6f5f052766cc4cef538ce",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -45,37 +45,11 @@ in {
|
||||||
type = types.oneOf [types.port types.str];
|
type = types.oneOf [types.port types.str];
|
||||||
description = "target port or address inside the namespace";
|
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";
|
description = "ports to forward into the namespace";
|
||||||
default = [];
|
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";
|
description = "namespaces to setup";
|
||||||
|
|
@ -87,7 +61,7 @@ in {
|
||||||
# symlink instead of passing `configFile` directly to netnsd to allow changing the config without changing the path
|
# 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.etc."netnsd/netnsd.toml".source = configFile;
|
||||||
|
|
||||||
environment.systemPackages = [cfg.package];
|
environment.systemPackages = with pkgs; [cfg.package];
|
||||||
|
|
||||||
systemd.services.netnsd = {
|
systemd.services.netnsd = {
|
||||||
reloadTriggers = [configFile];
|
reloadTriggers = [configFile];
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
rustc = rust-bin.stable.latest.minimal;
|
rustc = rust-bin.stable.latest.minimal;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
rustPlatform.buildRustPackage {
|
rustPlatform.buildRustPackage rec {
|
||||||
pname = cargoPackage.name;
|
pname = cargoPackage.name;
|
||||||
inherit (cargoPackage) version;
|
inherit (cargoPackage) version;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,13 @@ mod name;
|
||||||
mod source;
|
mod source;
|
||||||
mod target;
|
mod target;
|
||||||
|
|
||||||
pub use crate::config::name::{DeviceName, NamespaceName};
|
pub use crate::config::name::NamespaceName;
|
||||||
pub use crate::config::source::ForwardSource;
|
pub use crate::config::source::ForwardSource;
|
||||||
pub use crate::config::target::ForwardTarget;
|
pub use crate::config::target::ForwardTarget;
|
||||||
use cidr::AnyIpCidr;
|
use serde::Deserialize;
|
||||||
use serde::de::Error;
|
|
||||||
use serde::{Deserialize, Deserializer};
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use toml::from_str;
|
use toml::from_str;
|
||||||
|
|
||||||
|
|
@ -85,12 +80,7 @@ impl RawConfig {
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct NamespaceConfig {
|
pub struct NamespaceConfig {
|
||||||
pub name: NamespaceName,
|
pub name: NamespaceName,
|
||||||
#[serde(default)]
|
|
||||||
pub forward: Vec<ForwardConfig>,
|
pub forward: Vec<ForwardConfig>,
|
||||||
#[serde(default)]
|
|
||||||
pub devices: Vec<DeviceName>,
|
|
||||||
#[serde(default, rename = "route")]
|
|
||||||
pub routes: Vec<RouteConfig>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
@ -101,27 +91,6 @@ pub struct ForwardConfig {
|
||||||
pub reverse: bool,
|
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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
#[error("Error while reading config from {}: {error:#}", path.display())]
|
#[error("Error while reading config from {}: {error:#}", path.display())]
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,8 @@ use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
struct ValidatedName(String);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
|
|
||||||
#[serde(from = "ValidatedName")]
|
|
||||||
pub struct NamespaceName(String);
|
pub struct NamespaceName(String);
|
||||||
|
|
||||||
impl From<ValidatedName> for NamespaceName {
|
|
||||||
fn from(value: ValidatedName) -> Self {
|
|
||||||
NamespaceName(value.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<OsString> for NamespaceName {
|
impl TryFrom<OsString> for NamespaceName {
|
||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
|
|
@ -62,60 +52,7 @@ impl From<NamespaceName> for String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
|
impl<'de> Deserialize<'de> for NamespaceName {
|
||||||
#[serde(from = "ValidatedName")]
|
|
||||||
pub struct DeviceName(String);
|
|
||||||
|
|
||||||
impl From<ValidatedName> for DeviceName {
|
|
||||||
fn from(value: ValidatedName) -> Self {
|
|
||||||
DeviceName(value.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<OsString> for DeviceName {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: OsString) -> Result<Self, Self::Error> {
|
|
||||||
let str = value.into_string().map_err(|_| ())?;
|
|
||||||
if validate_name(&str) {
|
|
||||||
Ok(DeviceName(str))
|
|
||||||
} else {
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for DeviceName {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for DeviceName {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
self.0.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<Path> for DeviceName {
|
|
||||||
fn as_ref(&self) -> &Path {
|
|
||||||
self.0.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq<&str> for DeviceName {
|
|
||||||
fn eq(&self, other: &&str) -> bool {
|
|
||||||
self.0 == *other
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DeviceName> for String {
|
|
||||||
fn from(value: DeviceName) -> Self {
|
|
||||||
value.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for ValidatedName {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
|
|
@ -123,7 +60,7 @@ impl<'de> Deserialize<'de> for ValidatedName {
|
||||||
struct NamespaceNameVisitor;
|
struct NamespaceNameVisitor;
|
||||||
|
|
||||||
impl Visitor<'_> for NamespaceNameVisitor {
|
impl Visitor<'_> for NamespaceNameVisitor {
|
||||||
type Value = ValidatedName;
|
type Value = NamespaceName;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||||
formatter.write_str("A valid namespace name")
|
formatter.write_str("A valid namespace name")
|
||||||
|
|
@ -136,7 +73,7 @@ impl<'de> Deserialize<'de> for ValidatedName {
|
||||||
if !validate_name(v) {
|
if !validate_name(v) {
|
||||||
return Err(E::invalid_value(Unexpected::Str(v), &self));
|
return Err(E::invalid_value(Unexpected::Str(v), &self));
|
||||||
}
|
}
|
||||||
Ok(ValidatedName(v.into()))
|
Ok(NamespaceName(v.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||||
|
|
@ -146,7 +83,7 @@ impl<'de> Deserialize<'de> for ValidatedName {
|
||||||
if !validate_name(&v) {
|
if !validate_name(&v) {
|
||||||
return Err(E::invalid_value(Unexpected::Str(&v), &self));
|
return Err(E::invalid_value(Unexpected::Str(&v), &self));
|
||||||
}
|
}
|
||||||
Ok(ValidatedName(v))
|
Ok(NamespaceName(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,24 +92,20 @@ impl<'de> Deserialize<'de> for ValidatedName {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for NamespaceName {
|
impl FromStr for NamespaceName {
|
||||||
type Err = InvalidNameError;
|
type Err = InvalidNamespaceNameError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if !validate_name(s) {
|
if !validate_name(s) {
|
||||||
return Err(InvalidNameError {
|
return Err(InvalidNamespaceNameError { name: s.into() });
|
||||||
name: s.into(),
|
|
||||||
kind: "namespace",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Ok(NamespaceName(s.into()))
|
Ok(NamespaceName(s.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("invalid name for {kind}: '{name}'")]
|
#[error("invalid name for namespace: '{name}'")]
|
||||||
pub struct InvalidNameError {
|
pub struct InvalidNamespaceNameError {
|
||||||
name: String,
|
name: String,
|
||||||
kind: &'static str,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a name follows the portable filename character set
|
/// Check if a name follows the portable filename character set
|
||||||
|
|
|
||||||
143
src/daemon.rs
143
src/daemon.rs
|
|
@ -1,10 +1,5 @@
|
||||||
use crate::config::{
|
use crate::config::{Config, ForwardConfig, NamespaceConfig, NamespaceName};
|
||||||
Config, DeviceName, ForwardConfig, NamespaceConfig, NamespaceName, RouteConfig,
|
use crate::namespace::{NamespaceError, NetNs};
|
||||||
};
|
|
||||||
use crate::link::{LinkError, LinkManager};
|
|
||||||
use crate::namespace::{
|
|
||||||
NamespaceEnterError, NamespaceError, NamespaceHandle, NamespaceHandleError, NetNs,
|
|
||||||
};
|
|
||||||
use crate::proxy::{ActiveProxy, ProxyError};
|
use crate::proxy::{ActiveProxy, ProxyError};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
|
@ -30,15 +25,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
state.update(&config)?;
|
state.update(&config)?;
|
||||||
|
|
||||||
// now the namespaces are setup, we can tell systemd to start any service depending on them
|
// now the namespaces are setup, we can tell systemd to start any service depending on them
|
||||||
notify(&[
|
notify(false, &[NotifyState::Ready]).map_err(DaemonError::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 = signal(SignalKind::hangup()).map_err(DaemonError::Signal)?;
|
||||||
let reload_signal = SignalStream::new(reload_signal).map(|_| Event::Reload);
|
let reload_signal = SignalStream::new(reload_signal).map(|_| Event::Reload);
|
||||||
|
|
@ -65,7 +52,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
|
|
||||||
match NotifyState::monotonic_usec_now() {
|
match NotifyState::monotonic_usec_now() {
|
||||||
Ok(notify_time) => {
|
Ok(notify_time) => {
|
||||||
notify(&[NotifyState::Reloading, notify_time])
|
notify(false, &[NotifyState::Reloading, notify_time])
|
||||||
.map_err(DaemonError::Notify)?;
|
.map_err(DaemonError::Notify)?;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -83,15 +70,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(&[
|
notify(false, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||||
NotifyState::Ready,
|
|
||||||
NotifyState::Status(&format!(
|
|
||||||
"Reloaded with {} namespaces",
|
|
||||||
state.namespaces.len()
|
|
||||||
)),
|
|
||||||
])
|
|
||||||
.map_err(DaemonError::Notify)?;
|
|
||||||
info!("reloaded");
|
|
||||||
}
|
}
|
||||||
Event::Info => {
|
Event::Info => {
|
||||||
for namespace in &state.namespaces {
|
for namespace in &state.namespaces {
|
||||||
|
|
@ -104,7 +83,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = notify(&[NotifyState::Stopping]);
|
let _ = notify(false, &[NotifyState::Stopping]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +101,7 @@ struct State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn new() -> Result<Self, DaemonError> {
|
pub fn new() -> Result<Self, DaemonError> {
|
||||||
let namespaces = NetNs::existing(false)?
|
let namespaces = NetNs::existing()?
|
||||||
.map(ActiveNamespace::new)
|
.map(ActiveNamespace::new)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
Ok(State { namespaces })
|
Ok(State { namespaces })
|
||||||
|
|
@ -145,8 +124,6 @@ impl State {
|
||||||
for namespace in &mut self.namespaces {
|
for namespace in &mut self.namespaces {
|
||||||
let config = config.get_namespace(namespace.name()).unwrap();
|
let config = config.get_namespace(namespace.name()).unwrap();
|
||||||
namespace.update_proxies(config)?;
|
namespace.update_proxies(config)?;
|
||||||
namespace.update_devices(config)?;
|
|
||||||
namespace.update_links(config)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -162,8 +139,6 @@ impl State {
|
||||||
struct ActiveNamespace {
|
struct ActiveNamespace {
|
||||||
ns: NetNs,
|
ns: NetNs,
|
||||||
proxies: Vec<ActiveProxy>,
|
proxies: Vec<ActiveProxy>,
|
||||||
devices: Vec<DeviceName>,
|
|
||||||
routes: Vec<RouteConfig>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveNamespace {
|
impl ActiveNamespace {
|
||||||
|
|
@ -173,8 +148,6 @@ impl ActiveNamespace {
|
||||||
Ok(ActiveNamespace {
|
Ok(ActiveNamespace {
|
||||||
ns,
|
ns,
|
||||||
proxies: Vec::default(),
|
proxies: Vec::default(),
|
||||||
devices: Vec::default(),
|
|
||||||
routes: Vec::default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,106 +164,10 @@ impl ActiveNamespace {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_devices(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> {
|
|
||||||
let parent_namespace = NamespaceHandle::parent()?;
|
|
||||||
|
|
||||||
let removed: Vec<_> = self
|
|
||||||
.devices
|
|
||||||
.extract_if(.., |existing| {
|
|
||||||
!config.devices.iter().any(|new| existing == new)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
self.ns.handle().run_in(move || {
|
|
||||||
let link_manager = LinkManager::new()?;
|
|
||||||
for link in link_manager.get_links()?.flatten() {
|
|
||||||
if removed.iter().any(|name| *name == link.name.as_str()) {
|
|
||||||
info!(namespace = %config.name, link = link.name , "moving link out of namespace");
|
|
||||||
link_manager.move_link(&link, &parent_namespace)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok::<_, LinkError>(())
|
|
||||||
})??;
|
|
||||||
|
|
||||||
let mut added = Vec::new();
|
|
||||||
for new in &config.devices {
|
|
||||||
if !self.has_device(new) {
|
|
||||||
added.push(new.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let link_manager = LinkManager::new()?;
|
|
||||||
for link in link_manager.get_links()?.flatten() {
|
|
||||||
if added.iter().any(|name| *name == link.name.as_str()) {
|
|
||||||
info!(namespace = %config.name, link = link.name , "moving link into namespace");
|
|
||||||
link_manager.move_link(&link, self.ns.handle())?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for new in added {
|
|
||||||
self.devices.push(new);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
fn has_forward(&self, config: &ForwardConfig) -> bool {
|
||||||
self.proxies.iter().any(|existing| existing == config)
|
self.proxies.iter().any(|existing| existing == config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_device(&self, name: &DeviceName) -> bool {
|
|
||||||
self.devices.iter().any(|existing| existing == name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_route(&self, route: &RouteConfig) -> bool {
|
|
||||||
self.routes.iter().any(|existing| existing == route)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &NamespaceName {
|
pub fn name(&self) -> &NamespaceName {
|
||||||
self.ns.name()
|
self.ns.name()
|
||||||
}
|
}
|
||||||
|
|
@ -306,10 +183,4 @@ pub enum DaemonError {
|
||||||
Signal(IoError),
|
Signal(IoError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Proxy(#[from] ProxyError),
|
Proxy(#[from] ProxyError),
|
||||||
#[error(transparent)]
|
|
||||||
Handle(#[from] NamespaceHandleError),
|
|
||||||
#[error(transparent)]
|
|
||||||
Enter(#[from] NamespaceEnterError),
|
|
||||||
#[error(transparent)]
|
|
||||||
Link(#[from] LinkError),
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::namespace::NetNs;
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
|
|
||||||
pub fn down() -> MainResult {
|
pub fn down() -> MainResult {
|
||||||
for name in NetNs::existing(true)? {
|
for name in NetNs::existing()? {
|
||||||
let ns = NetNs::new(name)?;
|
let ns = NetNs::new(name)?;
|
||||||
ns.delete()?
|
ns.delete()?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
277
src/link.rs
277
src/link.rs
|
|
@ -1,265 +1,96 @@
|
||||||
use cidr::{AnyIpCidr, Family};
|
|
||||||
use neli::consts::nl::NlmF;
|
use neli::consts::nl::NlmF;
|
||||||
|
use neli::consts::rtnl::Ifla;
|
||||||
|
use neli::consts::rtnl::RtAddrFamily;
|
||||||
use neli::consts::rtnl::Rtm;
|
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::consts::socket::NlFamily;
|
||||||
use neli::err::RouterError;
|
use neli::err::RouterError;
|
||||||
use neli::nl::NlPayload;
|
use neli::nl::NlPayload;
|
||||||
use neli::router::synchronous::NlRouter;
|
use neli::router::synchronous::NlRouter;
|
||||||
use neli::rtnl::{Ifinfomsg, RtattrBuilder, Rtmsg};
|
use neli::rtnl::Ifinfomsg;
|
||||||
use neli::rtnl::{IfinfomsgBuilder, RtmsgBuilder};
|
use neli::rtnl::IfinfomsgBuilder;
|
||||||
use neli::types::{Buffer, RtBuffer};
|
|
||||||
use neli::utils::Groups;
|
use neli::utils::Groups;
|
||||||
use nix::libc::c_int;
|
use nix::errno::Errno;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use nix::sched::{setns, CloneFlags};
|
||||||
use std::os::fd::AsRawFd;
|
use std::fs::File;
|
||||||
|
use std::io::Error as IoError;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::thread::spawn;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{info, instrument};
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum LinkError {
|
pub enum LinkError {
|
||||||
#[error("Failed to communicate with netlink: {0}")]
|
#[error("failed to communicate with netlink")]
|
||||||
Netlink(String),
|
Netlink,
|
||||||
#[error("Failed to parse netlink response: {0}")]
|
#[error("failed to code netlink response")]
|
||||||
Parse(String),
|
Parse,
|
||||||
#[error("Link not found: {0}")]
|
#[error("unexpected panic in link setup")]
|
||||||
NotFound(String),
|
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 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, P> From<RouterError<T, P>> for LinkError
|
impl<T, P> From<RouterError<T, P>> for LinkError {
|
||||||
where
|
fn from(_value: RouterError<T, P>) -> Self {
|
||||||
T: Debug,
|
LinkError::Netlink
|
||||||
P: Debug,
|
|
||||||
{
|
|
||||||
fn from(value: RouterError<T, P>) -> Self {
|
|
||||||
LinkError::Netlink(value.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LinkManager {
|
/// Set a link to UP inside a namespace
|
||||||
router: NlRouter,
|
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(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
spawn(move || {
|
||||||
|
setns(ns_handle, CloneFlags::CLONE_NEWNET).map_err(LinkError::Namespace)?;
|
||||||
|
link_up(link_name)
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.map_err(|_| LinkError::Panic)?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Link {
|
/// Set a link to UP
|
||||||
family: RtAddrFamily,
|
pub fn link_up(link_name: &str) -> Result<(), LinkError> {
|
||||||
index: c_int,
|
// I honestly don't really know how this code works
|
||||||
pub name: String,
|
// 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)?;
|
||||||
impl Display for Link {
|
rtnl.enable_strict_checking(true)?;
|
||||||
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()
|
let ifinfomsg = IfinfomsgBuilder::default()
|
||||||
.ifi_family(RtAddrFamily::Inet)
|
.ifi_family(RtAddrFamily::Inet)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let recv = self.router.send::<_, _, Rtm, Ifinfomsg>(
|
let recv = rtnl.send::<_, _, Rtm, Ifinfomsg>(
|
||||||
Rtm::Getlink,
|
Rtm::Getlink,
|
||||||
NlmF::DUMP | NlmF::ACK,
|
NlmF::DUMP | NlmF::ACK,
|
||||||
NlPayload::Payload(ifinfomsg),
|
NlPayload::Payload(ifinfomsg),
|
||||||
)?;
|
)?;
|
||||||
Ok(recv
|
for response in recv {
|
||||||
.map(|response| {
|
|
||||||
if let Some(payload) = response?.get_payload() {
|
if let Some(payload) = response?.get_payload() {
|
||||||
let name = payload
|
let name = payload
|
||||||
.rtattrs()
|
.rtattrs()
|
||||||
.get_attr_handle()
|
.get_attr_handle()
|
||||||
.get_attr_payload_as_with_len::<String>(Ifla::Ifname)
|
.get_attr_payload_as_with_len::<String>(Ifla::Ifname)
|
||||||
.map_err(|e| LinkError::Parse(e.to_string()))?;
|
.map_err(|_| LinkError::Parse)?;
|
||||||
Ok(Some(Link {
|
if name == link_name {
|
||||||
family: *payload.ifi_family(),
|
let up_msg = IfinfomsgBuilder::default()
|
||||||
index: *payload.ifi_index(),
|
.ifi_family(RtAddrFamily::Inet)
|
||||||
name,
|
.ifi_index(*payload.ifi_index())
|
||||||
}))
|
.up()
|
||||||
} 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()
|
.build()
|
||||||
.expect("invalid rtattr"),
|
.unwrap();
|
||||||
);
|
rtnl.send::<_, _, Rtm, Ifinfomsg>(
|
||||||
|
|
||||||
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,
|
Rtm::Setlink,
|
||||||
NlmF::ACK,
|
NlmF::ACK,
|
||||||
NlPayload::Payload(up_msg),
|
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(())
|
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> {
|
|
||||||
let manager = LinkManager::new()?;
|
|
||||||
let link = manager.get_link(link_name)?;
|
|
||||||
manager.up(&link)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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::<_, 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")
|
|
||||||
}
|
|
||||||
|
|
|
||||||
14
src/main.rs
14
src/main.rs
|
|
@ -5,10 +5,10 @@ use crate::proxy::proxy;
|
||||||
use crate::up::up;
|
use crate::up::up;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
use nix::errno::Errno;
|
|
||||||
use nix::sys::signal::{Signal, kill};
|
|
||||||
use nix::unistd::Pid;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use nix::sys::signal::{kill, Signal};
|
||||||
|
use nix::unistd::Pid;
|
||||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
|
@ -94,12 +94,10 @@ fn reload() -> MainResult {
|
||||||
match kill(Pid::from_raw(proc.pid().as_u32() as i32), Signal::SIGHUP) {
|
match kill(Pid::from_raw(proc.pid().as_u32() as i32), Signal::SIGHUP) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Sent reload command to daemon")
|
info!("Sent reload command to daemon")
|
||||||
}
|
},
|
||||||
Err(Errno::EPERM) => {
|
Err(Errno::EPERM) => {
|
||||||
error!(
|
error!("Sending signal not permitted, try are you running the command as root?");
|
||||||
"Sending signal not permitted, try are you running the command as root?"
|
},
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!(%error, "Unexpected error");
|
error!(%error, "Unexpected error");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
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,12 +1,8 @@
|
||||||
mod handle;
|
|
||||||
mod raw;
|
mod raw;
|
||||||
mod sysctl;
|
|
||||||
|
|
||||||
use crate::config::{DeviceName, NamespaceName};
|
use crate::config::NamespaceName;
|
||||||
use crate::link::{link_up, move_all_links, move_link_into, LinkError};
|
use crate::link::{link_up_ns, LinkError};
|
||||||
pub use crate::namespace::handle::{NamespaceHandle, NamespaceHandleError};
|
|
||||||
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError};
|
use crate::namespace::raw::{create_network_namespace, NamespaceSetupError};
|
||||||
use crate::namespace::sysctl::{CtlError, NamespaceCtl};
|
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use nix::errno::Errno;
|
use nix::errno::Errno;
|
||||||
use nix::mount::{mount, umount2, MntFlags, MsFlags};
|
use nix::mount::{mount, umount2, MntFlags, MsFlags};
|
||||||
|
|
@ -16,19 +12,16 @@ use std::iter::empty;
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
pub struct NetNs {
|
pub struct NetNs {
|
||||||
name: NamespaceName,
|
name: NamespaceName,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
nsd_path: PathBuf,
|
nsd_path: PathBuf,
|
||||||
handle: NamespaceHandle,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetNs {
|
impl NetNs {
|
||||||
pub fn existing(
|
pub fn existing() -> Result<impl Iterator<Item = NamespaceName>, NamespaceError> {
|
||||||
include_broken: bool,
|
|
||||||
) -> Result<impl Iterator<Item = NamespaceName>, NamespaceError> {
|
|
||||||
let dir = match read_dir("/var/run/netnsd") {
|
let dir = match read_dir("/var/run/netnsd") {
|
||||||
Ok(dir) => Ok(dir),
|
Ok(dir) => Ok(dir),
|
||||||
Err(error) if error.kind() == ErrorKind::NotFound => {
|
Err(error) if error.kind() == ErrorKind::NotFound => {
|
||||||
|
|
@ -39,14 +32,12 @@ impl NetNs {
|
||||||
error,
|
error,
|
||||||
}),
|
}),
|
||||||
}?;
|
}?;
|
||||||
Ok(Either::Right(
|
Ok(Either::Right(dir.flatten().flat_map(|entry| {
|
||||||
dir.flatten()
|
NamespaceName::try_from(entry.file_name()).ok()
|
||||||
.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
|
/// Create a new named network namespace that will be removed when dropped
|
||||||
pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> {
|
pub fn new(name: NamespaceName) -> Result<Self, NamespaceError> {
|
||||||
let parent = Path::new("/var/run/netns");
|
let parent = Path::new("/var/run/netns");
|
||||||
let nsd_parent = Path::new("/var/run/netnsd");
|
let nsd_parent = Path::new("/var/run/netnsd");
|
||||||
|
|
@ -56,34 +47,22 @@ impl NetNs {
|
||||||
let path = parent.join(&name);
|
let path = parent.join(&name);
|
||||||
let nsd_path = nsd_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) {
|
match File::create_new(&path) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
|
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
|
||||||
info!(%name, "using existing network namespace");
|
info!(%name, "using existing network namespace");
|
||||||
|
|
||||||
if !nsd_path.is_symlink() {
|
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 {
|
symlink(&path, &nsd_path).map_err(|error| NamespaceError::Symlink {
|
||||||
error,
|
error,
|
||||||
path: nsd_path.clone(),
|
path: nsd_path.clone(),
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let handle = NamespaceHandle::open(&path)?;
|
|
||||||
return Ok(NetNs {
|
return Ok(NetNs {
|
||||||
name,
|
name,
|
||||||
nsd_path,
|
nsd_path,
|
||||||
path,
|
path,
|
||||||
handle,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
|
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
|
||||||
|
|
@ -98,12 +77,10 @@ impl NetNs {
|
||||||
path: nsd_path.clone(),
|
path: nsd_path.clone(),
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let handle = NamespaceHandle::open(&path)?;
|
|
||||||
Result::<_, NamespaceError>::Ok(NetNs {
|
Result::<_, NamespaceError>::Ok(NetNs {
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
nsd_path,
|
nsd_path,
|
||||||
handle,
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -156,46 +133,24 @@ impl NetNs {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_interfaces(&self) -> Result<(), NamespaceError> {
|
fn setup_interfaces(&self) -> Result<(), NamespaceError> {
|
||||||
let ctl = NamespaceCtl::read()?;
|
link_up_ns(&self.path, "lo")?;
|
||||||
self.handle.run_in(move || {
|
|
||||||
link_up("lo").map_err(NamespaceError::from)?;
|
|
||||||
dbg!(ctl).apply()?;
|
|
||||||
Ok::<_, NamespaceError>(())
|
|
||||||
})??;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(self) -> Result<(), NamespaceError> {
|
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();
|
let name = self.path.file_name().unwrap().to_str().unwrap();
|
||||||
info!(name, "deleting network namespace");
|
info!(name, "deleting network namespace");
|
||||||
match umount2(&self.path, MntFlags::MNT_DETACH) {
|
umount2(&self.path, MntFlags::MNT_DETACH).map_err(NamespaceError::UnMount)?;
|
||||||
Err(Errno::EINVAL) => Ok(()), // not a mountpoint, namespace doesn't exist
|
remove_file(&self.path).map_err(|error| NamespaceError::Delete {
|
||||||
rest => rest,
|
|
||||||
}
|
|
||||||
.map_err(NamespaceError::UnMount)?;
|
|
||||||
remove_file_if_exists(&self.path).map_err(|error| NamespaceError::Delete {
|
|
||||||
error,
|
error,
|
||||||
path: self.path,
|
path: self.path,
|
||||||
})?;
|
})?;
|
||||||
remove_file_if_exists(&self.nsd_path).map_err(|error| NamespaceError::Delete {
|
remove_file(&self.nsd_path).map_err(|error| NamespaceError::Delete {
|
||||||
error,
|
error,
|
||||||
path: self.nsd_path,
|
path: self.nsd_path,
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move a device into this namespace
|
|
||||||
pub fn move_device(&self, device: &DeviceName) -> Result<(), LinkError> {
|
|
||||||
move_link_into(device.as_ref(), self.handle())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the namespace handle
|
|
||||||
pub fn handle(&self) -> &NamespaceHandle {
|
|
||||||
&self.handle
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -216,16 +171,10 @@ pub enum NamespaceError {
|
||||||
Mount(Errno),
|
Mount(Errno),
|
||||||
#[error("Failed to unmount netns handle: {0:?}")]
|
#[error("Failed to unmount netns handle: {0:?}")]
|
||||||
UnMount(Errno),
|
UnMount(Errno),
|
||||||
|
#[error("Failed to setup loopback inside namespace: {0:#}")]
|
||||||
|
Link(#[from] LinkError),
|
||||||
#[error("Failed to scan {} for namespaces: {error:#}", path.display())]
|
#[error("Failed to scan {} for namespaces: {error:#}", path.display())]
|
||||||
Scan { path: PathBuf, error: IoError },
|
Scan { path: PathBuf, error: IoError },
|
||||||
#[error(transparent)]
|
|
||||||
Handle(#[from] NamespaceHandleError),
|
|
||||||
#[error(transparent)]
|
|
||||||
Enter(#[from] NamespaceEnterError),
|
|
||||||
#[error(transparent)]
|
|
||||||
Link(#[from] LinkError),
|
|
||||||
#[error("Failed to setup sysctl for namespace: {0:#}")]
|
|
||||||
Sysctl(#[from] CtlError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NamespaceError {
|
impl NamespaceError {
|
||||||
|
|
@ -233,27 +182,3 @@ impl NamespaceError {
|
||||||
NamespaceError::Create { path, error }
|
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,6 +4,7 @@ use nix::sys::signal::Signal;
|
||||||
use nix::sys::wait::{waitpid, WaitStatus};
|
use nix::sys::wait::{waitpid, WaitStatus};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum NamespaceSetupError {
|
pub enum NamespaceSetupError {
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
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,12 +3,7 @@ mod tcp;
|
||||||
use crate::config::{ForwardConfig, ForwardSource, ForwardTarget, NamespaceName};
|
use crate::config::{ForwardConfig, ForwardSource, ForwardTarget, NamespaceName};
|
||||||
use crate::proxy::tcp::Proxy;
|
use crate::proxy::tcp::Proxy;
|
||||||
use futures::future::AbortHandle;
|
use futures::future::AbortHandle;
|
||||||
use landlock::{
|
|
||||||
ABI, Access, AccessFs, AccessNet, NetPort, Ruleset, RulesetAttr, RulesetCreatedAttr,
|
|
||||||
RulesetError, RulesetStatus,
|
|
||||||
};
|
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
use nix::errno::Errno;
|
|
||||||
use nix::sched::{CloneFlags, setns};
|
use nix::sched::{CloneFlags, setns};
|
||||||
use nix::sys::signal::{SIGINT, kill};
|
use nix::sys::signal::{SIGINT, kill};
|
||||||
use nix::unistd::{Gid, Pid, Uid, setgid, setuid};
|
use nix::unistd::{Gid, Pid, Uid, setgid, setuid};
|
||||||
|
|
@ -18,6 +13,8 @@ use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command};
|
use std::process::{Child, Command};
|
||||||
use std::thread::spawn;
|
use std::thread::spawn;
|
||||||
|
use landlock::{Access, AccessFs, AccessNet, NetPort, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetError, RulesetStatus, ABI};
|
||||||
|
use nix::errno::Errno;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::runtime::Builder;
|
use tokio::runtime::Builder;
|
||||||
use tokio::signal::ctrl_c;
|
use tokio::signal::ctrl_c;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/// Loosely based on https://github.com/fooker/netns-proxy/blob/main/src/tcp.rs
|
/// Loosely based on https://github.com/fooker/netns-proxy/blob/main/src/tcp.rs
|
||||||
use crate::config::{ForwardSource, ForwardTarget};
|
use crate::config::{ForwardTarget, ForwardSource};
|
||||||
use crate::proxy::ProxyError;
|
use crate::proxy::{ProxyError};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use futures::stream::{AbortRegistration, Abortable};
|
use futures::stream::{AbortRegistration, Abortable};
|
||||||
use std::fs::{remove_file, set_permissions};
|
use std::fs::{remove_file, set_permissions};
|
||||||
|
|
@ -56,18 +56,28 @@ impl Proxy {
|
||||||
})?;
|
})?;
|
||||||
debug!("Created TCP socket");
|
debug!("Created TCP socket");
|
||||||
|
|
||||||
Ok(Self { socket })
|
Ok(Self {
|
||||||
|
socket,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self, target: ForwardTarget, abort: AbortRegistration) {
|
pub async fn run(self, target: ForwardTarget, abort: AbortRegistration) {
|
||||||
match self.socket {
|
match self.socket {
|
||||||
ProxyListener::Tcp(socket) => run_tcp(socket, target.addr, abort).await,
|
ProxyListener::Tcp(socket) => {
|
||||||
ProxyListener::Unix(socket) => run_unix(socket, target.addr, abort).await,
|
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 accepts = TcpListenerStream::new(socket).map_err(|error| ProxyError::Accept { error });
|
||||||
let mut accepts = pin!(Abortable::new(accepts, abort));
|
let mut accepts = pin!(Abortable::new(accepts, abort));
|
||||||
while let Some(client) = accepts.next().await {
|
while let Some(client) = accepts.next().await {
|
||||||
|
|
@ -84,7 +94,11 @@ async fn run_tcp(socket: TcpListener, target: SocketAddr, abort: AbortRegistrati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 accepts = UnixListenerStream::new(socket).map_err(|error| ProxyError::Accept { error });
|
||||||
let mut accepts = pin!(Abortable::new(accepts, abort));
|
let mut accepts = pin!(Abortable::new(accepts, abort));
|
||||||
while let Some(client) = accepts.next().await {
|
while let Some(client) = accepts.next().await {
|
||||||
|
|
|
||||||
24
src/up.rs
24
src/up.rs
|
|
@ -1,11 +1,9 @@
|
||||||
use crate::config::{Config, NamespaceName};
|
use crate::config::{Config, NamespaceName};
|
||||||
use crate::link::{LinkError, LinkManager};
|
|
||||||
use crate::namespace::NetNs;
|
use crate::namespace::NetNs;
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
pub fn up(config: Config) -> MainResult {
|
pub fn up(config: Config) -> MainResult {
|
||||||
let mut namespaces = NetNs::existing(false)?
|
let mut namespaces = NetNs::existing()?
|
||||||
.map(NetNs::new)
|
.map(NetNs::new)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
|
@ -17,21 +15,7 @@ pub fn up(config: Config) -> MainResult {
|
||||||
|
|
||||||
for new in config.namespaces {
|
for new in config.namespaces {
|
||||||
if !has_namespace(&namespaces, &new.name) {
|
if !has_namespace(&namespaces, &new.name) {
|
||||||
namespaces.push(NetNs::new(new.name.clone())?);
|
namespaces.push(NetNs::new(new.name)?);
|
||||||
}
|
|
||||||
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>(())
|
|
||||||
})??;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +25,3 @@ pub fn up(config: Config) -> MainResult {
|
||||||
fn has_namespace(namespaces: &[NetNs], name: &NamespaceName) -> bool {
|
fn has_namespace(namespaces: &[NetNs], name: &NamespaceName) -> bool {
|
||||||
namespaces.iter().any(|existing| existing.name() == name)
|
namespaces.iter().any(|existing| existing.name() == name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_namespace<'a>(namespaces: &'a [NetNs], name: &NamespaceName) -> Option<&'a NetNs> {
|
|
||||||
namespaces.iter().find(|existing| existing.name() == name)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue