mirror of
https://codeberg.org/icewind/netnsd.git
synced 2026-06-03 09:04:07 +02:00
proxying
This commit is contained in:
parent
78e716f949
commit
ec6c3a0a8b
7 changed files with 404 additions and 40 deletions
34
Cargo.lock
generated
34
Cargo.lock
generated
|
|
@ -67,6 +67,12 @@ version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.43"
|
version = "1.2.43"
|
||||||
|
|
@ -331,6 +337,15 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humansize"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
|
|
@ -359,6 +374,12 @@ version = "0.2.177"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.28"
|
version = "0.4.28"
|
||||||
|
|
@ -417,6 +438,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-concurrency",
|
"futures-concurrency",
|
||||||
|
"humansize",
|
||||||
"main_error",
|
"main_error",
|
||||||
"nix",
|
"nix",
|
||||||
"sd-notify",
|
"sd-notify",
|
||||||
|
|
@ -641,6 +663,16 @@ version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|
@ -699,10 +731,12 @@ version = "1.48.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "signal"] }
|
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "signal", "net", "io-util"] }
|
||||||
tokio-stream = { version = "0.1.17", features = ["signal"] }
|
tokio-stream = { version = "0.1.17", features = ["signal", "net"] }
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
clap = { version = "4.5.51", features = ["derive"] }
|
clap = { version = "4.5.51", features = ["derive"] }
|
||||||
|
|
@ -17,6 +17,7 @@ nix = { version = "0.30.1", features = ["mount", "sched"] }
|
||||||
sd-notify = "0.4.5"
|
sd-notify = "0.4.5"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
futures-concurrency = "7.6.3"
|
futures-concurrency = "7.6.3"
|
||||||
|
humansize = { version = "2.1.3", features = ["no_alloc"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_test = "1.0.177"
|
serde_test = "1.0.177"
|
||||||
|
|
@ -6,7 +6,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||||
pub struct ForwardDestination {
|
pub struct ForwardDestination {
|
||||||
addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ForwardDestination> for SocketAddr {
|
impl From<ForwardDestination> for SocketAddr {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
mod namespace;
|
mod namespace;
|
||||||
|
mod proxy;
|
||||||
|
|
||||||
use crate::config::{Config, NamespaceConfig, NamespaceName};
|
use crate::config::{Config, ForwardConfig, NamespaceConfig, NamespaceName};
|
||||||
use crate::daemon::namespace::{NamespaceError, NetNs};
|
use crate::daemon::namespace::{NamespaceError, NetNs};
|
||||||
|
use futures::FutureExt;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use futures_concurrency::stream::Merge;
|
||||||
use main_error::MainResult;
|
use main_error::MainResult;
|
||||||
use sd_notify::{NotifyState, notify};
|
use sd_notify::{NotifyState, notify};
|
||||||
use std::io::Error as IoError;
|
use std::io::Error as IoError;
|
||||||
use std::pin::pin;
|
use std::pin::pin;
|
||||||
use futures::FutureExt;
|
use humansize::{SizeFormatter, BINARY};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::signal::ctrl_c;
|
use tokio::signal::ctrl_c;
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
use futures::StreamExt;
|
|
||||||
use futures_concurrency::stream::Merge;
|
|
||||||
use tokio_stream::wrappers::SignalStream;
|
use tokio_stream::wrappers::SignalStream;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
use crate::daemon::proxy::{ActiveProxy, ProxyError};
|
||||||
|
|
||||||
pub fn daemon(config: Config) -> MainResult {
|
pub fn daemon(config: Config) -> MainResult {
|
||||||
let rt = Runtime::new()?;
|
let rt = Runtime::new()?;
|
||||||
|
|
@ -22,24 +25,21 @@ pub fn daemon(config: Config) -> MainResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
for namespace in &config.namespaces {
|
|
||||||
println!("{}:", namespace.name);
|
|
||||||
for forward in &namespace.forward {
|
|
||||||
println!(" {} => {}", forward.source, forward.destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
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(true, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
notify(true, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||||
|
|
||||||
let reload_signal = signal(SignalKind::hangup()).map_err(DaemonError::ReloadSignal)?;
|
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);
|
||||||
|
|
||||||
|
let info_signal = signal(SignalKind::user_defined1()).map_err(DaemonError::Signal)?;
|
||||||
|
let info_signal = SignalStream::new(info_signal).map(|_| Event::Info);
|
||||||
|
|
||||||
let quit_signal = ctrl_c().into_stream().map(|_| Event::Quit);
|
let quit_signal = ctrl_c().into_stream().map(|_| Event::Quit);
|
||||||
|
|
||||||
let events = (reload_signal, quit_signal).merge();
|
let events = (reload_signal, info_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 {
|
||||||
|
|
@ -53,7 +53,8 @@ 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(true, &[NotifyState::Reloading, notify_time]).map_err(DaemonError::Notify)?;
|
notify(true, &[NotifyState::Reloading, notify_time])
|
||||||
|
.map_err(DaemonError::Notify)?;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!(%error, "failed to get current time, not sending reload signal");
|
error!(%error, "failed to get current time, not sending reload signal");
|
||||||
|
|
@ -64,7 +65,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
Ok(new_config) => {
|
Ok(new_config) => {
|
||||||
state.update(&new_config)?;
|
state.update(&new_config)?;
|
||||||
config = new_config;
|
config = new_config;
|
||||||
},
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!(%error, "Failed to load new config");
|
error!(%error, "Failed to load new config");
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +73,22 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
|
|
||||||
notify(true, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
notify(true, &[NotifyState::Ready]).map_err(DaemonError::Notify)?;
|
||||||
}
|
}
|
||||||
|
Event::Info => {
|
||||||
|
for namespace in &state.namespaces {
|
||||||
|
println!("{}:", namespace.name());
|
||||||
|
for proxy in &namespace.proxies {
|
||||||
|
println!(
|
||||||
|
" {} => {} {} connections ({} active), {} sent to namespace, {} received from namespace",
|
||||||
|
proxy.source,
|
||||||
|
proxy.destination,
|
||||||
|
proxy.stats.total_connections(),
|
||||||
|
proxy.stats.open_connections(),
|
||||||
|
SizeFormatter::new(proxy.stats.written(), BINARY),
|
||||||
|
SizeFormatter::new(proxy.stats.read(), BINARY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +99,7 @@ async fn daemon_async(mut config: Config) -> Result<(), DaemonError> {
|
||||||
enum Event {
|
enum Event {
|
||||||
Reload,
|
Reload,
|
||||||
Quit,
|
Quit,
|
||||||
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -91,11 +109,19 @@ struct State {
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn update(&mut self, config: &Config) -> Result<(), DaemonError> {
|
pub fn update(&mut self, config: &Config) -> Result<(), DaemonError> {
|
||||||
self.namespaces.retain(|existing| {
|
self.namespaces.retain_mut(|existing| {
|
||||||
|
if let Some(new_config) =
|
||||||
config
|
config
|
||||||
.namespaces
|
.namespaces
|
||||||
.iter()
|
.iter()
|
||||||
.any(|new| &new.name == existing.name())
|
.find(|new| &new.name == existing.name()) {
|
||||||
|
if let Err(error) = existing.update_proxies(new_config) {
|
||||||
|
error!(%error, "Failed to update proxies for namespace");
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for new in &config.namespaces {
|
for new in &config.namespaces {
|
||||||
|
|
@ -103,12 +129,12 @@ impl State {
|
||||||
self.namespaces.push(ActiveNamespace::new(new)?);
|
self.namespaces.push(ActiveNamespace::new(new)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_namespace(&self, name: &NamespaceName) -> bool {
|
fn has_namespace(&self, name: &NamespaceName) -> bool {
|
||||||
self
|
self.namespaces
|
||||||
.namespaces
|
|
||||||
.iter()
|
.iter()
|
||||||
.any(|existing| existing.name() == name)
|
.any(|existing| existing.name() == name)
|
||||||
}
|
}
|
||||||
|
|
@ -116,12 +142,36 @@ impl State {
|
||||||
|
|
||||||
struct ActiveNamespace {
|
struct ActiveNamespace {
|
||||||
ns: NetNs,
|
ns: NetNs,
|
||||||
|
proxies: Vec<ActiveProxy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveNamespace {
|
impl ActiveNamespace {
|
||||||
pub fn new(config: &NamespaceConfig) -> Result<Self, DaemonError> {
|
pub fn new(config: &NamespaceConfig) -> Result<Self, DaemonError> {
|
||||||
let ns = NetNs::new(&config.name)?;
|
let ns = NetNs::new(&config.name)?;
|
||||||
Ok(ActiveNamespace { ns })
|
|
||||||
|
let mut namespace = ActiveNamespace {
|
||||||
|
ns,
|
||||||
|
proxies: Vec::default(),
|
||||||
|
};
|
||||||
|
namespace.update_proxies(config)?;
|
||||||
|
|
||||||
|
Ok(namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_proxies(&mut self, config: &NamespaceConfig) -> Result<(), DaemonError> {
|
||||||
|
self.proxies.retain(|existing| config.forward.iter().any(|new| existing == new));
|
||||||
|
|
||||||
|
for new in &config.forward {
|
||||||
|
if !self.has_forward(new) {
|
||||||
|
self.proxies.push(ActiveProxy::new(new)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_forward(&self, config: &ForwardConfig) -> bool {
|
||||||
|
self.proxies.iter().any(|existing| existing == config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &NamespaceName {
|
pub fn name(&self) -> &NamespaceName {
|
||||||
|
|
@ -135,6 +185,8 @@ pub enum DaemonError {
|
||||||
Namespace(#[from] NamespaceError),
|
Namespace(#[from] NamespaceError),
|
||||||
#[error("Error sending notification to systemd: {0:#}")]
|
#[error("Error sending notification to systemd: {0:#}")]
|
||||||
Notify(IoError),
|
Notify(IoError),
|
||||||
#[error("Error setting up reload signal listener: {0:#}")]
|
#[error("Error setting up signal handler: {0:#}")]
|
||||||
ReloadSignal(IoError),
|
Signal(IoError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Proxy(#[from] ProxyError),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,16 @@ impl NetNs {
|
||||||
let path = parent.join(name);
|
let path = parent.join(name);
|
||||||
let mount_path = path.clone();
|
let mount_path = path.clone();
|
||||||
|
|
||||||
let _ =
|
match File::create_new(&path) {
|
||||||
File::create_new(&path).map_err(|error| NamespaceError::from_create(name, error))?;
|
Ok(_) => {}
|
||||||
|
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
|
||||||
|
return Ok(NetNs {
|
||||||
|
name: name.clone(),
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => return Err(NamespaceError::from_create(name, e)),
|
||||||
|
}
|
||||||
|
|
||||||
let handle: JoinHandle<Result<(), NamespaceError>> = spawn(move || {
|
let handle: JoinHandle<Result<(), NamespaceError>> = spawn(move || {
|
||||||
unshare(CloneFlags::CLONE_NEWNET).map_err(NamespaceError::Unshare)?;
|
unshare(CloneFlags::CLONE_NEWNET).map_err(NamespaceError::Unshare)?;
|
||||||
|
|
@ -41,7 +49,7 @@ impl NetNs {
|
||||||
handle.join().unwrap()?;
|
handle.join().unwrap()?;
|
||||||
Ok(NetNs {
|
Ok(NetNs {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
path
|
path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,8 +77,6 @@ impl Drop for NetNs {
|
||||||
pub enum NamespaceError {
|
pub enum NamespaceError {
|
||||||
#[error("Failed to create parent directory for namespaces (/var/run/netns): {0:#}")]
|
#[error("Failed to create parent directory for namespaces (/var/run/netns): {0:#}")]
|
||||||
Parent(IoError),
|
Parent(IoError),
|
||||||
#[error("Network namespace {0} already exists")]
|
|
||||||
AlreadyExists(NamespaceName),
|
|
||||||
#[error("Failed to create namespace file {}: {error:#}", path.display())]
|
#[error("Failed to create namespace file {}: {error:#}", path.display())]
|
||||||
Create { path: PathBuf, error: IoError },
|
Create { path: PathBuf, error: IoError },
|
||||||
#[error("Unexpected error while creating new network namespace: {0:}")]
|
#[error("Unexpected error while creating new network namespace: {0:}")]
|
||||||
|
|
@ -86,12 +92,9 @@ impl NamespaceError {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_create(name: &NamespaceName, error: IoError) -> Self {
|
fn from_create(name: &NamespaceName, error: IoError) -> Self {
|
||||||
match error.kind() {
|
NamespaceError::Create {
|
||||||
ErrorKind::AlreadyExists => NamespaceError::AlreadyExists(name.clone()),
|
|
||||||
_ => NamespaceError::Create {
|
|
||||||
path: Path::new("/var/run/netns").join(name),
|
path: Path::new("/var/run/netns").join(name),
|
||||||
error,
|
error,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
src/daemon/proxy/mod.rs
Normal file
106
src/daemon/proxy/mod.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
mod tcp;
|
||||||
|
|
||||||
|
use std::fs::remove_file;
|
||||||
|
use crate::config::{ForwardConfig, ForwardDestination, ForwardSource};
|
||||||
|
use crate::daemon::proxy::tcp::Proxy;
|
||||||
|
use futures::future::AbortHandle;
|
||||||
|
use std::io::Error as IoError;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ProxyError {
|
||||||
|
#[error("Failed to listen to {address}: {error:#}")]
|
||||||
|
Bind {
|
||||||
|
address: ForwardSource,
|
||||||
|
error: IoError,
|
||||||
|
},
|
||||||
|
#[error("Failed to accept proxy connection: {error:#}")]
|
||||||
|
Accept { error: IoError },
|
||||||
|
#[error("Failed to connect to proxy destination {destination}: {error:#}")]
|
||||||
|
Connect {
|
||||||
|
destination: SocketAddr,
|
||||||
|
error: IoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActiveProxy {
|
||||||
|
pub source: ForwardSource,
|
||||||
|
pub destination: ForwardDestination,
|
||||||
|
abort: AbortHandle,
|
||||||
|
pub stats: ProxyStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveProxy {
|
||||||
|
pub fn new(config: &ForwardConfig) -> Result<ActiveProxy, ProxyError> {
|
||||||
|
let proxy = Proxy::listen(config.source.clone())?;
|
||||||
|
Ok(proxy.run(config.destination.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ActiveProxy {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let ForwardSource::Unix(path) = &self.source {
|
||||||
|
if let Err(error) = remove_file(path) {
|
||||||
|
error!(%error, "failed to remove unix socket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.abort.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<ForwardConfig> for ActiveProxy {
|
||||||
|
fn eq(&self, other: &ForwardConfig) -> bool {
|
||||||
|
self.source == other.source && self.destination == other.destination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct ProxyStats {
|
||||||
|
open_connections: Arc<AtomicU64>,
|
||||||
|
total_connections: Arc<AtomicU64>,
|
||||||
|
/// Bytes proxied source -> destination
|
||||||
|
bytes_written: Arc<AtomicU64>,
|
||||||
|
/// Bytes proxied destination -> source
|
||||||
|
bytes_read: Arc<AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyStats {
|
||||||
|
pub fn open_connection(&self) {
|
||||||
|
self.open_connections.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.total_connections.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_connection(&self) {
|
||||||
|
self.open_connections.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_connections(&self) -> u64 {
|
||||||
|
self.open_connections.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_connections(&self) -> u64 {
|
||||||
|
self.total_connections.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_read(&self, bytes: u64) {
|
||||||
|
self.bytes_read.fetch_add(bytes, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_written(&self, bytes: u64) {
|
||||||
|
self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes proxied destination -> source
|
||||||
|
pub fn read(&self) -> u64 {
|
||||||
|
self.bytes_read.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes proxied source -> destination
|
||||||
|
pub fn written(&self) -> u64 {
|
||||||
|
self.bytes_written.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
168
src/daemon/proxy/tcp.rs
Normal file
168
src/daemon/proxy/tcp.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
use std::fs::remove_file;
|
||||||
|
/// Loosely based on https://github.com/fooker/netns-proxy/blob/main/src/tcp.rs
|
||||||
|
use crate::config::{ForwardDestination, ForwardSource};
|
||||||
|
use crate::daemon::proxy::{ActiveProxy, ProxyError, ProxyStats};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use futures::stream::{AbortHandle, AbortRegistration, Abortable};
|
||||||
|
use std::io::Error as IoError;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::pin::pin;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite, copy_bidirectional};
|
||||||
|
use tokio::net::{TcpListener, TcpSocket, TcpStream, UnixListener};
|
||||||
|
use tokio::spawn;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use tokio_stream::wrappers::{TcpListenerStream, UnixListenerStream};
|
||||||
|
use tracing::{Level, debug, error, instrument, span, trace, warn};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Proxy {
|
||||||
|
source: ForwardSource,
|
||||||
|
socket: ProxyListener,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ProxyListener {
|
||||||
|
Tcp(TcpListener),
|
||||||
|
Unix(UnixListener),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_tcp(addr: SocketAddr) -> Result<TcpListener, IoError> {
|
||||||
|
let socket = if addr.is_ipv4() {
|
||||||
|
TcpSocket::new_v4()
|
||||||
|
} else {
|
||||||
|
TcpSocket::new_v6()
|
||||||
|
}?;
|
||||||
|
socket.bind(addr)?;
|
||||||
|
socket.listen(1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Proxy {
|
||||||
|
pub fn listen(bind: ForwardSource) -> Result<Self, ProxyError> {
|
||||||
|
let socket = match &bind {
|
||||||
|
ForwardSource::Unix(path) => {
|
||||||
|
let _ = remove_file(path);
|
||||||
|
UnixListener::bind(path).map(ProxyListener::Unix)
|
||||||
|
},
|
||||||
|
ForwardSource::Ip(addr) => bind_tcp(*addr).map(ProxyListener::Tcp),
|
||||||
|
}
|
||||||
|
.map_err(|error| ProxyError::Bind {
|
||||||
|
address: bind.clone(),
|
||||||
|
error,
|
||||||
|
})?;
|
||||||
|
debug!("Created TCP socket");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
source: bind,
|
||||||
|
socket,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self, target: ForwardDestination) -> ActiveProxy {
|
||||||
|
let (abort_handle, abort) = AbortHandle::new_pair();
|
||||||
|
let destination = target.clone();
|
||||||
|
|
||||||
|
let stats = ProxyStats::default();
|
||||||
|
|
||||||
|
let proxy_stats = stats.clone();
|
||||||
|
spawn(async move {
|
||||||
|
match self.socket {
|
||||||
|
ProxyListener::Tcp(socket) => {
|
||||||
|
run_tcp(socket, target.addr, abort, proxy_stats).await
|
||||||
|
}
|
||||||
|
ProxyListener::Unix(socket) => {
|
||||||
|
run_unix(socket, target.addr, abort, proxy_stats).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ActiveProxy {
|
||||||
|
source: self.source,
|
||||||
|
destination,
|
||||||
|
abort: abort_handle,
|
||||||
|
stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_tcp(
|
||||||
|
socket: TcpListener,
|
||||||
|
target: SocketAddr,
|
||||||
|
abort: AbortRegistration,
|
||||||
|
stats: ProxyStats,
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
stats.open_connection();
|
||||||
|
let result: Result<(), ProxyError> = async {
|
||||||
|
let client = client?;
|
||||||
|
let remote = client.peer_addr().ok();
|
||||||
|
proxy_stream(client, target, remote, stats.clone()).await
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Error in tcp proxy: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_unix(
|
||||||
|
socket: UnixListener,
|
||||||
|
target: SocketAddr,
|
||||||
|
abort: AbortRegistration,
|
||||||
|
stats: ProxyStats,
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
stats.open_connection();
|
||||||
|
let result: Result<(), ProxyError> = async {
|
||||||
|
let client = client?;
|
||||||
|
proxy_stream(client, target, None, stats.clone()).await
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Error in tcp proxy: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(stream, proxy))]
|
||||||
|
async fn proxy_stream<Stream>(
|
||||||
|
mut stream: Stream,
|
||||||
|
target: SocketAddr,
|
||||||
|
remote: Option<SocketAddr>,
|
||||||
|
proxy: ProxyStats,
|
||||||
|
) -> Result<(), ProxyError>
|
||||||
|
where
|
||||||
|
Stream: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let _ = span!(Level::TRACE, "Client connection", connection = ?remote).entered();
|
||||||
|
|
||||||
|
trace!("connected");
|
||||||
|
|
||||||
|
let mut upstream = TcpStream::connect(target)
|
||||||
|
.await
|
||||||
|
.map_err(|error| ProxyError::Connect {
|
||||||
|
error,
|
||||||
|
destination: target,
|
||||||
|
})?;
|
||||||
|
trace!("Upstream connection established");
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
match copy_bidirectional(&mut stream, &mut upstream).await {
|
||||||
|
Ok((written, read)) => {
|
||||||
|
proxy.add_written(written);
|
||||||
|
proxy.add_read(read);
|
||||||
|
proxy.close_connection();
|
||||||
|
trace!("Upstream connection closed");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Upstream connection failed: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue