mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-03 09:04:07 +02:00
reload destination -> target and module fixes
This commit is contained in:
parent
645a6e9978
commit
5e5ee227fc
10 changed files with 69 additions and 42 deletions
9
README.md
Normal file
9
README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# netnsd
|
||||||
|
|
||||||
|
A declarative manager for Linux network namespaces.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fully declarative configuration
|
||||||
|
- Hot reloading of configuration
|
||||||
|
- Port forwarding into the namespace
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
...
|
...
|
||||||
}: {
|
}: {
|
||||||
imports = [./nix/module.nix];
|
imports = [./nix/module.nix];
|
||||||
config = lib.mkIf config.networking.netnsd.enable {
|
config = lib.mkIf (config.networking.netnsd.namespaces != {}) {
|
||||||
|
nixpkgs.overlays = [ outputs.overlays.default ];
|
||||||
networking.netnsd.package = lib.mkDefault pkgs.netnsd;
|
networking.netnsd.package = lib.mkDefault pkgs.netnsd;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ with lib; let
|
||||||
hasNamespaces = cfg.namespaces != {};
|
hasNamespaces = cfg.namespaces != {};
|
||||||
format = pkgs.formats.toml {};
|
format = pkgs.formats.toml {};
|
||||||
configFile = format.generate "netnsd.toml" {
|
configFile = format.generate "netnsd.toml" {
|
||||||
inherit (cfg) namespaces;
|
namespace = mapAttrsToList (_: value: value) cfg.namespaces;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
options.networking.netnsd = {
|
options.networking.netnsd = {
|
||||||
|
|
@ -18,6 +18,12 @@ in {
|
||||||
description = "package to use";
|
description = "package to use";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logLevel = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "info";
|
||||||
|
description = "Log level";
|
||||||
|
};
|
||||||
|
|
||||||
namespaces = mkOption {
|
namespaces = mkOption {
|
||||||
type = types.attrsOf (types.submodule ({name, ...}: {
|
type = types.attrsOf (types.submodule ({name, ...}: {
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -31,11 +37,11 @@ in {
|
||||||
options = {
|
options = {
|
||||||
source = mkOption {
|
source = mkOption {
|
||||||
type = types.oneOf [types.port types.str];
|
type = types.oneOf [types.port types.str];
|
||||||
default = config.destination;
|
default = config.target;
|
||||||
defaultText = "<destination>";
|
defaultText = "<target>";
|
||||||
description = "source port, address or socket outside the namespace";
|
description = "source port, address or socket outside the namespace";
|
||||||
};
|
};
|
||||||
destination = mkOption {
|
target = mkOption {
|
||||||
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";
|
||||||
};
|
};
|
||||||
|
|
@ -46,6 +52,7 @@ in {
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
description = "namespaces to setup";
|
description = "namespaces to setup";
|
||||||
|
default = {};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -53,17 +60,20 @@ 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;
|
||||||
|
|
||||||
systemd.services.netcsctl = {
|
systemd.services.netnsd = {
|
||||||
reloadTriggers = [configFile];
|
reloadTriggers = [configFile];
|
||||||
|
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
|
before = ["network.target"];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RUST_LOG = cfg.logLevel;
|
||||||
|
};
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
Type = "notify-reload";
|
Type = "notify-reload";
|
||||||
ExecStart = "${getExec cfg.pkg} daemon -c /etc/netnsd/netnsd.toml";
|
ExecStart = "${getExe cfg.package} daemon -c /etc/netnsd/netnsd.toml";
|
||||||
PrivateTmp = true;
|
|
||||||
ProtectSystem = "full";
|
|
||||||
ProtectHome = true;
|
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,6 @@ in
|
||||||
cargoLock = {
|
cargoLock = {
|
||||||
lockFile = ../Cargo.lock;
|
lockFile = ../Cargo.lock;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
meta.mainProgram = "netnsd";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
mod destination;
|
mod target;
|
||||||
mod name;
|
mod name;
|
||||||
mod source;
|
mod source;
|
||||||
|
|
||||||
pub use crate::config::destination::ForwardDestination;
|
pub use crate::config::target::ForwardTarget;
|
||||||
pub use crate::config::name::NamespaceName;
|
pub use crate::config::name::NamespaceName;
|
||||||
pub use crate::config::source::ForwardSource;
|
pub use crate::config::source::ForwardSource;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -80,7 +80,7 @@ pub struct NamespaceConfig {
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct ForwardConfig {
|
pub struct ForwardConfig {
|
||||||
pub source: ForwardSource,
|
pub source: ForwardSource,
|
||||||
pub destination: ForwardDestination,
|
pub target: ForwardTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,31 @@ use std::net::{IpAddr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||||
pub struct ForwardDestination {
|
pub struct ForwardTarget {
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ForwardDestination> for SocketAddr {
|
impl From<ForwardTarget> for SocketAddr {
|
||||||
fn from(value: ForwardDestination) -> Self {
|
fn from(value: ForwardTarget) -> Self {
|
||||||
value.addr
|
value.addr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ForwardDestination {
|
impl Display for ForwardTarget {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.addr)
|
write!(f, "{}", self.addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for ForwardDestination {
|
impl<'de> Deserialize<'de> for ForwardTarget {
|
||||||
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>,
|
||||||
{
|
{
|
||||||
struct ForwardDestinationVisitor;
|
struct ForwardTargetVisitor;
|
||||||
|
|
||||||
impl Visitor<'_> for ForwardDestinationVisitor {
|
impl Visitor<'_> for ForwardTargetVisitor {
|
||||||
type Value = ForwardDestination;
|
type Value = ForwardTarget;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||||
formatter
|
formatter
|
||||||
|
|
@ -51,7 +51,7 @@ impl<'de> Deserialize<'de> for ForwardDestination {
|
||||||
E: Error,
|
E: Error,
|
||||||
{
|
{
|
||||||
let ip = IpAddr::from([127, 0, 0, 1]);
|
let ip = IpAddr::from([127, 0, 0, 1]);
|
||||||
Ok(ForwardDestination {
|
Ok(ForwardTarget {
|
||||||
addr: SocketAddr::from((ip, v)),
|
addr: SocketAddr::from((ip, v)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -76,11 +76,11 @@ impl<'de> Deserialize<'de> for ForwardDestination {
|
||||||
let addr = v
|
let addr = v
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?;
|
.map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?;
|
||||||
Ok(ForwardDestination { addr })
|
Ok(ForwardTarget { addr })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializer.deserialize_any(ForwardDestinationVisitor)
|
deserializer.deserialize_any(ForwardTargetVisitor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,14 +90,14 @@ fn test_de() {
|
||||||
|
|
||||||
let addr_str = "127.0.0.1:80";
|
let addr_str = "127.0.0.1:80";
|
||||||
let addr = SocketAddr::from_str("127.0.0.1:80").unwrap();
|
let addr = SocketAddr::from_str("127.0.0.1:80").unwrap();
|
||||||
fn port_addr(port: u16) -> ForwardDestination {
|
fn port_addr(port: u16) -> ForwardTarget {
|
||||||
ForwardDestination {
|
ForwardTarget {
|
||||||
addr: SocketAddr::new(IpAddr::from([127, 0, 0, 1]), port),
|
addr: SocketAddr::new(IpAddr::from([127, 0, 0, 1]), port),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_de_tokens(&ForwardDestination { addr }, &[Token::String(addr_str)]);
|
assert_de_tokens(&ForwardTarget { addr }, &[Token::String(addr_str)]);
|
||||||
assert_de_tokens(&ForwardDestination { addr }, &[Token::Str(addr_str)]);
|
assert_de_tokens(&ForwardTarget { addr }, &[Token::Str(addr_str)]);
|
||||||
assert_de_tokens(&port_addr(80), &[Token::Str("80")]);
|
assert_de_tokens(&port_addr(80), &[Token::Str("80")]);
|
||||||
|
|
||||||
assert_de_tokens(&port_addr(80), &[Token::U8(80)]);
|
assert_de_tokens(&port_addr(80), &[Token::U8(80)]);
|
||||||
|
|
@ -107,19 +107,19 @@ fn test_de() {
|
||||||
assert_de_tokens(&port_addr(80), &[Token::I16(80)]);
|
assert_de_tokens(&port_addr(80), &[Token::I16(80)]);
|
||||||
assert_de_tokens(&port_addr(80), &[Token::I64(80)]);
|
assert_de_tokens(&port_addr(80), &[Token::I64(80)]);
|
||||||
|
|
||||||
assert_de_tokens_error::<ForwardDestination>(
|
assert_de_tokens_error::<ForwardTarget>(
|
||||||
&[Token::I64(-80)],
|
&[Token::I64(-80)],
|
||||||
"invalid value: integer `-80`, expected Either a port as integer, or a string containing a socket address",
|
"invalid value: integer `-80`, expected Either a port as integer, or a string containing a socket address",
|
||||||
);
|
);
|
||||||
assert_de_tokens_error::<ForwardDestination>(
|
assert_de_tokens_error::<ForwardTarget>(
|
||||||
&[Token::U64(12345678)],
|
&[Token::U64(12345678)],
|
||||||
"invalid value: integer `12345678`, expected Either a port as integer, or a string containing a socket address",
|
"invalid value: integer `12345678`, expected Either a port as integer, or a string containing a socket address",
|
||||||
);
|
);
|
||||||
assert_de_tokens_error::<ForwardDestination>(
|
assert_de_tokens_error::<ForwardTarget>(
|
||||||
&[Token::Str("hello world")],
|
&[Token::Str("hello world")],
|
||||||
"invalid value: string \"hello world\", expected Either a port as integer, or a string containing a socket address",
|
"invalid value: string \"hello world\", expected Either a port as integer, or a string containing a socket address",
|
||||||
);
|
);
|
||||||
assert_de_tokens_error::<ForwardDestination>(
|
assert_de_tokens_error::<ForwardTarget>(
|
||||||
&[Token::Str("localhost:80")],
|
&[Token::Str("localhost:80")],
|
||||||
"invalid value: string \"localhost:80\", expected Either a port as integer, or a string containing a socket address",
|
"invalid value: string \"localhost:80\", expected Either a port as integer, or a string containing a socket address",
|
||||||
);
|
);
|
||||||
|
|
@ -38,9 +38,12 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
let info_signal = signal(SignalKind::user_defined1()).map_err(DaemonError::Signal)?;
|
let info_signal = signal(SignalKind::user_defined1()).map_err(DaemonError::Signal)?;
|
||||||
let info_signal = SignalStream::new(info_signal).map(|_| Event::Info);
|
let info_signal = SignalStream::new(info_signal).map(|_| Event::Info);
|
||||||
|
|
||||||
|
let stop_signal = signal(SignalKind::terminate()).map_err(DaemonError::Signal)?;
|
||||||
|
let stop_signal = SignalStream::new(stop_signal).map(|_| Event::Quit);
|
||||||
|
|
||||||
let quit_signal = ctrl_c().into_stream().map(|_| Event::Quit);
|
let quit_signal = ctrl_c().into_stream().map(|_| Event::Quit);
|
||||||
|
|
||||||
let events = (reload_signal, info_signal, quit_signal).merge();
|
let events = (reload_signal, info_signal, stop_signal, quit_signal).merge();
|
||||||
|
|
||||||
let mut events = pin!(events);
|
let mut events = pin!(events);
|
||||||
while let Some(event) = events.next().await {
|
while let Some(event) = events.next().await {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use std::fs::{File, create_dir_all, remove_file};
|
||||||
use std::io::{Error as IoError, ErrorKind};
|
use std::io::{Error as IoError, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
pub struct NetNs {
|
pub struct NetNs {
|
||||||
name: NamespaceName,
|
name: NamespaceName,
|
||||||
|
|
@ -21,7 +21,6 @@ pub struct NetNs {
|
||||||
impl NetNs {
|
impl NetNs {
|
||||||
/// Create a new named network namespace that will be removed when dropped
|
/// 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> {
|
||||||
info!(%name, "creating network namespace");
|
|
||||||
let parent = Path::new("/var/run/netns");
|
let parent = Path::new("/var/run/netns");
|
||||||
create_dir_all(parent).map_err(NamespaceError::Parent)?;
|
create_dir_all(parent).map_err(NamespaceError::Parent)?;
|
||||||
let path = parent.join(&name);
|
let path = parent.join(&name);
|
||||||
|
|
@ -29,6 +28,7 @@ impl NetNs {
|
||||||
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");
|
||||||
return Ok(NetNs {
|
return Ok(NetNs {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
path,
|
path,
|
||||||
|
|
@ -36,6 +36,7 @@ impl NetNs {
|
||||||
}
|
}
|
||||||
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
|
Err(e) => return Err(NamespaceError::from_create(path.clone(), e)),
|
||||||
}
|
}
|
||||||
|
info!(%name, "creating network namespace");
|
||||||
|
|
||||||
let ns = create_network_namespace(move |ns| {
|
let ns = create_network_namespace(move |ns| {
|
||||||
bind_namespace(&ns, &path)?;
|
bind_namespace(&ns, &path)?;
|
||||||
|
|
@ -52,6 +53,7 @@ impl NetNs {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_namespace(namespace: &Path, path: &Path) -> Result<(), NamespaceError> {
|
fn bind_namespace(namespace: &Path, path: &Path) -> Result<(), NamespaceError> {
|
||||||
|
debug!(namespace = %namespace.display(), path = %path.display(), "mounting namespace");
|
||||||
mount(
|
mount(
|
||||||
Some(namespace.as_os_str()),
|
Some(namespace.as_os_str()),
|
||||||
path.as_os_str(),
|
path.as_os_str(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
mod tcp;
|
mod tcp;
|
||||||
|
|
||||||
use crate::config::{ForwardConfig, ForwardDestination, ForwardSource, NamespaceName};
|
use crate::config::{ForwardConfig, ForwardTarget, ForwardSource, NamespaceName};
|
||||||
use crate::daemon::proxy::tcp::Proxy;
|
use crate::daemon::proxy::tcp::Proxy;
|
||||||
use futures::future::AbortHandle;
|
use futures::future::AbortHandle;
|
||||||
use nix::sched::{CloneFlags, setns};
|
use nix::sched::{CloneFlags, setns};
|
||||||
|
|
@ -35,7 +35,7 @@ pub enum ProxyError {
|
||||||
|
|
||||||
pub struct ActiveProxy {
|
pub struct ActiveProxy {
|
||||||
pub source: ForwardSource,
|
pub source: ForwardSource,
|
||||||
pub destination: ForwardDestination,
|
pub destination: ForwardTarget,
|
||||||
abort: AbortHandle,
|
abort: AbortHandle,
|
||||||
pub stats: ProxyStats,
|
pub stats: ProxyStats,
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ impl ActiveProxy {
|
||||||
|
|
||||||
let (abort, abort_reg) = AbortHandle::new_pair();
|
let (abort, abort_reg) = AbortHandle::new_pair();
|
||||||
|
|
||||||
let destination = config.destination.clone();
|
let destination = config.target.clone();
|
||||||
let run_stats = stats.clone();
|
let run_stats = stats.clone();
|
||||||
let ns_path = PathBuf::from(format!("/var/run/netns/{namespace}"));
|
let ns_path = PathBuf::from(format!("/var/run/netns/{namespace}"));
|
||||||
let ns_handle = File::open(&ns_path).map_err(|error| ProxyError::OpenNamespace {
|
let ns_handle = File::open(&ns_path).map_err(|error| ProxyError::OpenNamespace {
|
||||||
|
|
@ -75,7 +75,7 @@ impl ActiveProxy {
|
||||||
|
|
||||||
Ok(ActiveProxy {
|
Ok(ActiveProxy {
|
||||||
source: config.source.clone(),
|
source: config.source.clone(),
|
||||||
destination: config.destination.clone(),
|
destination: config.target.clone(),
|
||||||
abort,
|
abort,
|
||||||
stats,
|
stats,
|
||||||
})
|
})
|
||||||
|
|
@ -95,7 +95,7 @@ impl Drop for ActiveProxy {
|
||||||
|
|
||||||
impl PartialEq<ForwardConfig> for ActiveProxy {
|
impl PartialEq<ForwardConfig> for ActiveProxy {
|
||||||
fn eq(&self, other: &ForwardConfig) -> bool {
|
fn eq(&self, other: &ForwardConfig) -> bool {
|
||||||
self.source == other.source && self.destination == other.destination
|
self.source == other.source && self.destination == other.target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/// 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::{ForwardDestination, ForwardSource};
|
use crate::config::{ForwardTarget, ForwardSource};
|
||||||
use crate::daemon::proxy::{ProxyError, ProxyStats};
|
use crate::daemon::proxy::{ProxyError, ProxyStats};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use futures::stream::{AbortRegistration, Abortable};
|
use futures::stream::{AbortRegistration, Abortable};
|
||||||
|
|
@ -61,7 +61,7 @@ impl Proxy {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self, target: ForwardDestination, abort: AbortRegistration, stats: ProxyStats) {
|
pub async fn run(self, target: ForwardTarget, abort: AbortRegistration, stats: ProxyStats) {
|
||||||
let proxy_stats = stats.clone();
|
let proxy_stats = stats.clone();
|
||||||
match self.socket {
|
match self.socket {
|
||||||
ProxyListener::Tcp(socket) => {
|
ProxyListener::Tcp(socket) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue