support reverse forwarding

This commit is contained in:
Robin Appelman 2025-11-10 22:19:13 +01:00
commit d8d1bc17df
6 changed files with 65 additions and 21 deletions

View file

@ -77,4 +77,10 @@ name = "test2"
# listening on a specific address instead of 0.0.0.0 # listening on a specific address instead of 0.0.0.0
source = "127.0.0.1:9091" source = "127.0.0.1:9091"
target = 80 target = 80
[[namespace.forward]]
# forward from inside the namespace to outside instead
reverse = true
source = 80
target = 80
``` ```

View file

@ -26,3 +26,9 @@ name = "test2"
# listening on a specific address instead of 0.0.0.0 # listening on a specific address instead of 0.0.0.0
source = "127.0.0.1:9091" source = "127.0.0.1:9091"
target = 80 target = 80
[[namespace.forward]]
# forward from inside the namespace to outside instead
reverse = true
source = 80
target = 80

View file

@ -48,6 +48,7 @@ in {
}; };
})); }));
description = "ports to forward into the namespace"; description = "ports to forward into the namespace";
default = [];
}; };
}; };
})); }));

View file

@ -87,6 +87,8 @@ pub struct NamespaceConfig {
pub struct ForwardConfig { pub struct ForwardConfig {
pub source: ForwardSource, pub source: ForwardSource,
pub target: ForwardTarget, pub target: ForwardTarget,
#[serde(default)]
pub reverse: bool,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -7,7 +7,7 @@ use nix::sched::{CloneFlags, setns};
use std::fs::{File, remove_file}; use std::fs::{File, remove_file};
use std::io::Error as IoError; use std::io::Error as IoError;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::spawn; use std::thread::spawn;
@ -45,20 +45,23 @@ impl ActiveProxy {
config: &ForwardConfig, config: &ForwardConfig,
namespace: &NamespaceName, namespace: &NamespaceName,
) -> Result<ActiveProxy, ProxyError> { ) -> Result<ActiveProxy, ProxyError> {
let proxy = Proxy::listen(config.source.clone())?;
let stats = ProxyStats::default(); let stats = ProxyStats::default();
let (abort, abort_reg) = AbortHandle::new_pair(); let (abort, abort_reg) = AbortHandle::new_pair();
let destination = config.target.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_handle = File::open(&ns_path).map_err(|error| ProxyError::OpenNamespace { let ns_handle = open_namespace(format!("/var/run/netns/{namespace}"))?;
error, let self_ns_handle = open_namespace("/proc/self/ns/net")?;
path: ns_path,
})?; let (listen_namespace, target_namespace) = if config.reverse {
spawn(move || match setns(ns_handle, CloneFlags::CLONE_NEWNET) { (Some(ns_handle), self_ns_handle)
Ok(_) => { } else {
(None, ns_handle)
};
let source = config.source.clone();
spawn(move || {
let rt = match Builder::new_current_thread().enable_io().build() { let rt = match Builder::new_current_thread().enable_io().build() {
Ok(rt) => rt, Ok(rt) => rt,
Err(error) => { Err(error) => {
@ -66,11 +69,29 @@ impl ActiveProxy {
return; return;
} }
}; };
rt.block_on(proxy.run(destination, abort_reg, run_stats));
rt.block_on(async {
if let Some(listen_namespace) = listen_namespace {
if let Err(error) = setns(listen_namespace, CloneFlags::CLONE_NEWNET) {
error!(%error, "Failed to join listen network namespace for proxy");
return;
} }
}
let proxy = match Proxy::listen(&source) {
Ok(proxy) => proxy,
Err(error) => { Err(error) => {
error!(%error, "Failed to join network namespace for proxy"); error!(%error, "Failed to listen to {source}");
return;
} }
};
if let Err(error) = setns(target_namespace, CloneFlags::CLONE_NEWNET) {
error!(%error, "Failed to join target network namespace for proxy");
return;
}
proxy.run(destination, abort_reg, run_stats).await;
});
}); });
Ok(ActiveProxy { Ok(ActiveProxy {
@ -145,3 +166,11 @@ impl ProxyStats {
self.bytes_written.load(Ordering::Relaxed) self.bytes_written.load(Ordering::Relaxed)
} }
} }
fn open_namespace(path: impl AsRef<Path>) -> Result<File, ProxyError> {
let path = path.as_ref();
File::open(path).map_err(|error| ProxyError::OpenNamespace {
error,
path: path.into(),
})
}

View file

@ -37,8 +37,8 @@ fn bind_tcp(addr: SocketAddr) -> Result<TcpListener, IoError> {
} }
impl Proxy { impl Proxy {
pub fn listen(bind: ForwardSource) -> Result<Self, ProxyError> { pub fn listen(bind: &ForwardSource) -> Result<Self, ProxyError> {
let socket = match &bind { let socket = match bind {
ForwardSource::Unix(path) => { ForwardSource::Unix(path) => {
let _ = remove_file(path); let _ = remove_file(path);
UnixListener::bind(path).map(|listener| { UnixListener::bind(path).map(|listener| {
@ -51,7 +51,7 @@ impl Proxy {
ForwardSource::Ip(addr) => bind_tcp(*addr).map(ProxyListener::Tcp), ForwardSource::Ip(addr) => bind_tcp(*addr).map(ProxyListener::Tcp),
} }
.map_err(|error| ProxyError::Bind { .map_err(|error| ProxyError::Bind {
address: bind, address: bind.clone(),
error, error,
})?; })?;
debug!("Created TCP socket"); debug!("Created TCP socket");