From 9d105e8034f2e6838c5b9da5d9a9c5b91a9764b8 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 29 Mar 2026 23:33:44 +0000 Subject: [PATCH] feat(rust-server, rust-client, ts-interfaces): add configurable packet forwarding with TUN and userspace NAT modes --- changelog.md | 10 + package.json | 2 +- rust/Cargo.lock | 115 +++++++ rust/Cargo.toml | 1 + rust/src/client.rs | 98 ++++++ rust/src/lib.rs | 1 + rust/src/server.rs | 233 ++++++++++++++ rust/src/tunnel.rs | 22 +- rust/src/userspace_nat.rs | 640 ++++++++++++++++++++++++++++++++++++++ rust/src/wireguard.rs | 24 +- ts/00_commitinfo_data.ts | 2 +- ts/smartvpn.interfaces.ts | 6 + 12 files changed, 1130 insertions(+), 24 deletions(-) create mode 100644 rust/src/userspace_nat.rs diff --git a/changelog.md b/changelog.md index 7ead27e..2315e93 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-03-29 - 1.10.0 - feat(rust-server, rust-client, ts-interfaces) +add configurable packet forwarding with TUN and userspace NAT modes + +- introduce forwardingMode options for client and server configuration interfaces +- add server-side forwarding engines for kernel TUN, userspace socket NAT, and testing mode +- add a smoltcp-based userspace NAT implementation for packet forwarding without root-only TUN routing +- enable client-side TUN forwarding support with route setup, packet I/O, and cleanup +- centralize raw packet destination IP extraction in tunnel utilities for shared routing logic +- update test command timeout and logging flags + ## 2026-03-29 - 1.9.0 - feat(server) add PROXY protocol v2 support for real client IP handling and connection ACLs diff --git a/package.json b/package.json index 76212c7..5ba7132 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "(tsbuild tsfolders) && (tsrust)", "test:before": "(tsrust)", - "test": "tstest test/ --verbose", + "test": "tstest test/ --verbose--verbose --logfile --timeout 60", "buildDocs": "tsdoc" }, "repository": { diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 141a1e6..02d205f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -237,6 +237,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -488,6 +494,47 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "deranged" version = "0.5.8" @@ -714,6 +761,25 @@ dependencies = [ "polyval", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -915,6 +981,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "matchers" version = "0.2.0" @@ -1116,6 +1188,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1598,6 +1692,7 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", + "smoltcp", "snow", "thiserror 2.0.18", "tokio", @@ -1609,6 +1704,20 @@ dependencies = [ "webpki-roots 1.0.6", ] +[[package]] +name = "smoltcp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac729b0a77bd092a3f06ddaddc59fe0d67f48ba0de45a9abe707c2842c7f8767" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + [[package]] name = "snow" version = "0.9.6" @@ -1635,6 +1744,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e11f1c1..3405544 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,6 +35,7 @@ rustls-pemfile = "2" webpki-roots = "1" mimalloc = "0.1" boringtun = "0.7" +smoltcp = { version = "0.13", default-features = false, features = ["medium-ip", "proto-ipv4", "socket-tcp", "socket-udp", "alloc"] } chrono = { version = "0.4", features = ["serde"] } ipnet = "2" diff --git a/rust/src/client.rs b/rust/src/client.rs index 687ed4d..96da678 100644 --- a/rust/src/client.rs +++ b/rust/src/client.rs @@ -1,6 +1,7 @@ use anyhow::Result; use bytes::BytesMut; use serde::Deserialize; +use std::net::Ipv4Addr; use std::sync::Arc; use tokio::sync::{mpsc, watch, RwLock}; use tracing::{info, error, warn, debug}; @@ -12,6 +13,7 @@ use crate::telemetry::ConnectionQuality; use crate::transport; use crate::transport_trait::{self, TransportSink, TransportStream}; use crate::quic_transport; +use crate::tunnel::{self, TunConfig}; /// Client configuration (matches TS IVpnClientConfig). #[derive(Debug, Clone, Deserialize)] @@ -30,6 +32,9 @@ pub struct ClientConfig { pub transport: Option, /// For QUIC: SHA-256 hash of server certificate (base64) for cert pinning. pub server_cert_hash: Option, + /// Forwarding mode: "tun" (TUN device, requires root) or "testing" (no TUN). + /// Default: "testing". + pub forwarding_mode: Option, } /// Client statistics. @@ -234,6 +239,31 @@ impl VpnClient { info!("Connected to VPN, assigned IP: {}", assigned_ip); + // Optionally create TUN device for IP packet forwarding (requires root) + let tun_enabled = config.forwarding_mode.as_deref() == Some("tun"); + let (tun_reader, tun_writer, tun_subnet) = if tun_enabled { + let client_tun_ip: Ipv4Addr = assigned_ip.parse()?; + let mtu = ip_info["mtu"].as_u64().unwrap_or(1420) as u16; + let tun_config = TunConfig { + name: "svpn-client0".to_string(), + address: client_tun_ip, + netmask: Ipv4Addr::new(255, 255, 255, 0), + mtu, + }; + let tun_device = tunnel::create_tun(&tun_config)?; + + // Add route for VPN subnet through the TUN device + let gateway_str = ip_info["gateway"].as_str().unwrap_or("10.8.0.1"); + let gateway: Ipv4Addr = gateway_str.parse().unwrap_or(Ipv4Addr::new(10, 8, 0, 1)); + let subnet = format!("{}/24", Ipv4Addr::from(u32::from(gateway) & 0xFFFFFF00)); + tunnel::add_route(&subnet, &tun_config.name).await?; + + let (reader, writer) = tokio::io::split(tun_device); + (Some(reader), Some(writer), Some(subnet)) + } else { + (None, None, None) + }; + // Create adaptive keepalive monitor (use custom interval if configured) let ka_config = config.keepalive_interval_secs.map(|secs| { let mut cfg = keepalive::AdaptiveKeepaliveConfig::default(); @@ -260,6 +290,9 @@ impl VpnClient { handle.signal_rx, handle.ack_tx, link_health, + tun_reader, + tun_writer, + tun_subnet, )); Ok(assigned_ip_clone) @@ -356,8 +389,14 @@ async fn client_loop( mut signal_rx: mpsc::Receiver, ack_tx: mpsc::Sender<()>, link_health: Arc>, + mut tun_reader: Option>, + mut tun_writer: Option>, + tun_subnet: Option, ) { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let mut buf = vec![0u8; 65535]; + let mut tun_buf = vec![0u8; 65536]; loop { tokio::select! { @@ -373,6 +412,14 @@ async fn client_loop( let mut s = stats.write().await; s.bytes_received += len as u64; s.packets_received += 1; + drop(s); + + // Write decrypted packet to TUN device (if enabled) + if let Some(ref mut writer) = tun_writer { + if let Err(e) = writer.write_all(&buf[..len]).await { + warn!("TUN write error: {}", e); + } + } } Err(e) => { warn!("Decrypt error: {}", e); @@ -407,6 +454,50 @@ async fn client_loop( } } } + // Read outbound packets from TUN and send to server (only when TUN enabled) + result = async { + match tun_reader { + Some(ref mut reader) => reader.read(&mut tun_buf).await, + None => std::future::pending::>().await, + } + } => { + match result { + Ok(0) => { + info!("TUN device closed"); + break; + } + Ok(n) => { + match noise_transport.write_message(&tun_buf[..n], &mut buf) { + Ok(len) => { + let frame = Frame { + packet_type: PacketType::IpPacket, + payload: buf[..len].to_vec(), + }; + let mut frame_bytes = BytesMut::new(); + if >::encode( + &mut FrameCodec, frame, &mut frame_bytes + ).is_ok() { + if sink.send_reliable(frame_bytes.to_vec()).await.is_err() { + warn!("Failed to send TUN packet to server"); + break; + } + let mut s = stats.write().await; + s.bytes_sent += n as u64; + s.packets_sent += 1; + } + } + Err(e) => { + warn!("Noise encrypt error: {}", e); + break; + } + } + } + Err(e) => { + warn!("TUN read error: {}", e); + break; + } + } + } signal = signal_rx.recv() => { match signal { Some(KeepaliveSignal::SendPing(timestamp_ms)) => { @@ -456,6 +547,13 @@ async fn client_loop( } } } + + // Cleanup: remove TUN route if enabled + if let Some(ref subnet) = tun_subnet { + if let Err(e) = tunnel::remove_route(subnet, "svpn-client0").await { + warn!("Failed to remove client TUN route: {}", e); + } + } } /// Try to connect via QUIC. Returns transport halves on success. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 85acbad..da8932b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -21,3 +21,4 @@ pub mod wireguard; pub mod client_registry; pub mod acl; pub mod proxy_protocol; +pub mod userspace_nat; diff --git a/rust/src/server.rs b/rust/src/server.rs index 14e97ed..06625fc 100644 --- a/rust/src/server.rs +++ b/rust/src/server.rs @@ -19,6 +19,7 @@ use crate::ratelimit::TokenBucket; use crate::transport; use crate::transport_trait::{self, TransportSink, TransportStream}; use crate::quic_transport; +use crate::tunnel::{self, TunConfig}; /// Dead-peer timeout: 3x max keepalive interval (Healthy=60s). const DEAD_PEER_TIMEOUT: Duration = Duration::from_secs(180); @@ -37,6 +38,9 @@ pub struct ServerConfig { pub mtu: Option, pub keepalive_interval_secs: Option, pub enable_nat: Option, + /// Forwarding mode: "tun" (kernel TUN, requires root), "socket" (userspace NAT), + /// or "testing" (monitoring only, no forwarding). Default: "testing". + pub forwarding_mode: Option, /// Default rate limit for new clients (bytes/sec). None = unlimited. pub default_rate_limit_bytes_per_sec: Option, /// Default burst size for new clients (bytes). None = unlimited. @@ -94,6 +98,16 @@ pub struct ServerStatistics { pub total_connections: u64, } +/// The forwarding engine determines how decrypted IP packets are routed. +pub enum ForwardingEngine { + /// Kernel TUN device — packets written to the TUN, kernel handles routing. + Tun(tokio::io::WriteHalf), + /// Userspace NAT — packets sent to smoltcp-based NAT engine via channel. + Socket(mpsc::Sender>), + /// Testing/monitoring — packets are counted but not forwarded. + Testing, +} + /// Shared server state. pub struct ServerState { pub config: ServerConfig, @@ -104,6 +118,12 @@ pub struct ServerState { pub mtu_config: MtuConfig, pub started_at: std::time::Instant, pub client_registry: RwLock, + /// The forwarding engine for decrypted IP packets. + pub forwarding_engine: Mutex, + /// Routing table: assigned VPN IP → channel sender for return packets. + pub tun_routes: RwLock>>>, + /// Shutdown signal for the forwarding background task (TUN reader or NAT engine). + pub tun_shutdown: mpsc::Sender<()>, } /// The VPN server. @@ -139,6 +159,51 @@ impl VpnServer { } let link_mtu = config.mtu.unwrap_or(1420); + let mode = config.forwarding_mode.as_deref().unwrap_or("testing"); + let gateway_ip = ip_pool.gateway_addr(); + + // Create forwarding engine based on mode + enum ForwardingSetup { + Tun { + writer: tokio::io::WriteHalf, + reader: tokio::io::ReadHalf, + shutdown_rx: mpsc::Receiver<()>, + }, + Socket { + packet_tx: mpsc::Sender>, + packet_rx: mpsc::Receiver>, + shutdown_rx: mpsc::Receiver<()>, + }, + Testing, + } + + let (setup, fwd_shutdown_tx) = match mode { + "tun" => { + let tun_config = TunConfig { + name: "svpn0".to_string(), + address: gateway_ip, + netmask: Ipv4Addr::new(255, 255, 255, 0), + mtu: link_mtu, + }; + let tun_device = tunnel::create_tun(&tun_config)?; + tunnel::add_route(&config.subnet, &tun_config.name).await?; + let (reader, writer) = tokio::io::split(tun_device); + let (tx, rx) = mpsc::channel::<()>(1); + (ForwardingSetup::Tun { writer, reader, shutdown_rx: rx }, tx) + } + "socket" => { + info!("Starting userspace NAT forwarding (no root required)"); + let (packet_tx, packet_rx) = mpsc::channel::>(4096); + let (tx, rx) = mpsc::channel::<()>(1); + (ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx: rx }, tx) + } + _ => { + info!("Forwarding disabled (testing/monitoring mode)"); + let (tx, _rx) = mpsc::channel::<()>(1); + (ForwardingSetup::Testing, tx) + } + }; + // Compute effective MTU from overhead let overhead = TunnelOverhead::default_overhead(); let mtu_config = MtuConfig::new(overhead.effective_tun_mtu(1500).max(link_mtu)); @@ -158,8 +223,38 @@ impl VpnServer { mtu_config, started_at: std::time::Instant::now(), client_registry: RwLock::new(registry), + forwarding_engine: Mutex::new(ForwardingEngine::Testing), + tun_routes: RwLock::new(HashMap::new()), + tun_shutdown: fwd_shutdown_tx, }); + // Spawn the forwarding background task and set the engine + match setup { + ForwardingSetup::Tun { writer, reader, shutdown_rx } => { + *state.forwarding_engine.lock().await = ForwardingEngine::Tun(writer); + let tun_state = state.clone(); + tokio::spawn(async move { + if let Err(e) = run_tun_reader(tun_state, reader, shutdown_rx).await { + error!("TUN reader error: {}", e); + } + }); + } + ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx } => { + *state.forwarding_engine.lock().await = ForwardingEngine::Socket(packet_tx); + let nat_engine = crate::userspace_nat::NatEngine::new( + gateway_ip, + link_mtu as usize, + state.clone(), + ); + tokio::spawn(async move { + if let Err(e) = nat_engine.run(packet_rx, shutdown_rx).await { + error!("NAT engine error: {}", e); + } + }); + } + ForwardingSetup::Testing => {} + } + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); self.state = Some(state.clone()); self.shutdown_tx = Some(shutdown_tx); @@ -220,6 +315,34 @@ impl VpnServer { } pub async fn stop(&mut self) -> Result<()> { + if let Some(ref state) = self.state { + let mode = state.config.forwarding_mode.as_deref().unwrap_or("testing"); + + match mode { + "tun" => { + let _ = state.tun_shutdown.send(()).await; + *state.forwarding_engine.lock().await = ForwardingEngine::Testing; + if let Err(e) = tunnel::remove_route(&state.config.subnet, "svpn0").await { + warn!("Failed to remove TUN route: {}", e); + } + } + "socket" => { + let _ = state.tun_shutdown.send(()).await; + *state.forwarding_engine.lock().await = ForwardingEngine::Testing; + } + _ => {} + } + + // Clean up NAT rules + if state.config.enable_nat.unwrap_or(false) { + if let Ok(iface) = crate::network::get_default_interface() { + if let Err(e) = crate::network::remove_nat(&state.config.subnet, &iface).await { + warn!("Failed to remove NAT rules: {}", e); + } + } + } + } + if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(()).await; } @@ -736,6 +859,56 @@ async fn run_quic_listener( Ok(()) } +/// TUN reader task: reads IP packets from the TUN device and dispatches them +/// to the correct client via the routing table. +async fn run_tun_reader( + state: Arc, + mut tun_reader: tokio::io::ReadHalf, + mut shutdown_rx: mpsc::Receiver<()>, +) -> Result<()> { + use tokio::io::AsyncReadExt; + + let mut buf = vec![0u8; 65536]; + + loop { + tokio::select! { + result = tun_reader.read(&mut buf) => { + let n = match result { + Ok(0) => { + info!("TUN reader: device closed"); + break; + } + Ok(n) => n, + Err(e) => { + error!("TUN reader error: {}", e); + break; + } + }; + + // Extract destination IP from the raw IP packet + let dst_ip = match tunnel::extract_dst_ip(&buf[..n]) { + Some(std::net::IpAddr::V4(v4)) => v4, + _ => continue, // IPv6 or malformed — skip + }; + + // Look up client by destination IP + let routes = state.tun_routes.read().await; + if let Some(sender) = routes.get(&dst_ip) { + if sender.try_send(buf[..n].to_vec()).is_err() { + // Channel full or closed — drop packet (correct for IP best-effort) + } + } + } + _ = shutdown_rx.recv() => { + info!("TUN reader shutting down"); + break; + } + } + } + + Ok(()) +} + /// Transport-agnostic client handler. Performs the Noise IK handshake, authenticates /// the client against the registry, and runs the main packet forwarding loop. async fn handle_client_connection( @@ -846,6 +1019,14 @@ async fn handle_client_connection( // Allocate IP let assigned_ip = state.ip_pool.lock().await.allocate(&client_id)?; + // Create return-packet channel for forwarding engine -> client + let (tun_return_tx, mut tun_return_rx) = mpsc::channel::>(256); + let fwd_mode = state.config.forwarding_mode.as_deref().unwrap_or("testing"); + let forwarding_active = fwd_mode == "tun" || fwd_mode == "socket"; + if forwarding_active { + state.tun_routes.write().await.insert(assigned_ip, tun_return_tx); + } + // Determine rate limits: per-client security overrides server defaults let (rate_limit, burst) = if let Some(ref sec) = client_security { if let Some(ref rl) = sec.rate_limit { @@ -973,6 +1154,24 @@ async fn handle_client_connection( if let Some(info) = clients.get_mut(&client_id) { info.bytes_received += len as u64; } + drop(clients); + + // Forward decrypted packet via the active engine + { + let mut engine = state.forwarding_engine.lock().await; + match &mut *engine { + ForwardingEngine::Tun(writer) => { + use tokio::io::AsyncWriteExt; + if let Err(e) = writer.write_all(&buf[..len]).await { + warn!("TUN write error for client {}: {}", client_id, e); + } + } + ForwardingEngine::Socket(sender) => { + let _ = sender.try_send(buf[..len].to_vec()); + } + ForwardingEngine::Testing => {} + } + } } Err(e) => { warn!("Decrypt error from {}: {}", client_id, e); @@ -1029,6 +1228,37 @@ async fn handle_client_connection( } } } + // Return packets from TUN device destined for this client + Some(packet) = tun_return_rx.recv() => { + let pkt_len = packet.len(); + match noise_transport.write_message(&packet, &mut buf) { + Ok(len) => { + let frame = Frame { + packet_type: PacketType::IpPacket, + payload: buf[..len].to_vec(), + }; + let mut frame_bytes = BytesMut::new(); + >::encode( + &mut FrameCodec, frame, &mut frame_bytes + )?; + sink.send_reliable(frame_bytes.to_vec()).await?; + + // Update stats + let mut stats = state.stats.write().await; + stats.bytes_sent += pkt_len as u64; + stats.packets_sent += 1; + drop(stats); + let mut clients = state.clients.write().await; + if let Some(info) = clients.get_mut(&client_id) { + info.bytes_sent += pkt_len as u64; + } + } + Err(e) => { + warn!("Noise encrypt error for return packet to {}: {}", client_id, e); + break; + } + } + } _ = tokio::time::sleep_until(last_activity + DEAD_PEER_TIMEOUT) => { warn!("Client {} dead-peer timeout ({}s inactivity)", client_id, DEAD_PEER_TIMEOUT.as_secs()); break; @@ -1037,6 +1267,9 @@ async fn handle_client_connection( } // Cleanup + if forwarding_active { + state.tun_routes.write().await.remove(&assigned_ip); + } state.clients.write().await.remove(&client_id); state.ip_pool.lock().await.release(&assigned_ip); state.rate_limiters.lock().await.remove(&client_id); diff --git a/rust/src/tunnel.rs b/rust/src/tunnel.rs index faa221a..c4a560f 100644 --- a/rust/src/tunnel.rs +++ b/rust/src/tunnel.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use tracing::info; /// Configuration for creating a TUN device. @@ -80,6 +80,26 @@ pub fn check_tun_mtu(packet: &[u8], mtu_config: &crate::mtu::MtuConfig) -> TunMt } } +/// Extract destination IP from a raw IP packet header. +pub fn extract_dst_ip(packet: &[u8]) -> Option { + if packet.is_empty() { + return None; + } + let version = packet[0] >> 4; + match version { + 4 if packet.len() >= 20 => { + let dst = Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]); + Some(IpAddr::V4(dst)) + } + 6 if packet.len() >= 40 => { + let mut octets = [0u8; 16]; + octets.copy_from_slice(&packet[24..40]); + Some(IpAddr::V6(Ipv6Addr::from(octets))) + } + _ => None, + } +} + /// Remove a route. pub async fn remove_route(subnet: &str, device_name: &str) -> Result<()> { let output = tokio::process::Command::new("ip") diff --git a/rust/src/userspace_nat.rs b/rust/src/userspace_nat.rs new file mode 100644 index 0000000..6a1cd72 --- /dev/null +++ b/rust/src/userspace_nat.rs @@ -0,0 +1,640 @@ +use std::collections::{HashMap, VecDeque}; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet}; +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; +use smoltcp::socket::{tcp, udp}; +use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr, IpEndpoint}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpStream, UdpSocket}; +use tokio::sync::mpsc; +use tracing::{debug, info, warn}; + +use crate::server::ServerState; +use crate::tunnel; + +// ============================================================================ +// Virtual IP device for smoltcp +// ============================================================================ + +pub struct VirtualIpDevice { + rx_queue: VecDeque>, + tx_queue: VecDeque>, + mtu: usize, +} + +impl VirtualIpDevice { + pub fn new(mtu: usize) -> Self { + Self { + rx_queue: VecDeque::new(), + tx_queue: VecDeque::new(), + mtu, + } + } + + pub fn inject_packet(&mut self, packet: Vec) { + self.rx_queue.push_back(packet); + } + + pub fn drain_tx(&mut self) -> impl Iterator> + '_ { + self.tx_queue.drain(..) + } +} + +pub struct VirtualRxToken { + buffer: Vec, +} + +impl phy::RxToken for VirtualRxToken { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + f(&self.buffer) + } +} + +pub struct VirtualTxToken<'a> { + queue: &'a mut VecDeque>, +} + +impl<'a> phy::TxToken for VirtualTxToken<'a> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + let mut buffer = vec![0u8; len]; + let result = f(&mut buffer); + self.queue.push_back(buffer); + result + } +} + +impl Device for VirtualIpDevice { + type RxToken<'a> = VirtualRxToken; + type TxToken<'a> = VirtualTxToken<'a>; + + fn receive( + &mut self, + _timestamp: smoltcp::time::Instant, + ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + self.rx_queue.pop_front().map(|buffer| { + let rx = VirtualRxToken { buffer }; + let tx = VirtualTxToken { + queue: &mut self.tx_queue, + }; + (rx, tx) + }) + } + + fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option> { + Some(VirtualTxToken { + queue: &mut self.tx_queue, + }) + } + + fn capabilities(&self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + caps.medium = Medium::Ip; + caps.max_transmission_unit = self.mtu; + caps.max_burst_size = Some(1); + caps + } +} + +// ============================================================================ +// Session tracking +// ============================================================================ + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +struct SessionKey { + src_ip: Ipv4Addr, + src_port: u16, + dst_ip: Ipv4Addr, + dst_port: u16, + protocol: u8, +} + +struct TcpSession { + smoltcp_handle: SocketHandle, + bridge_data_tx: mpsc::Sender>, + #[allow(dead_code)] + client_ip: Ipv4Addr, +} + +struct UdpSession { + smoltcp_handle: SocketHandle, + bridge_data_tx: mpsc::Sender>, + #[allow(dead_code)] + client_ip: Ipv4Addr, + last_activity: tokio::time::Instant, +} + +enum BridgeMessage { + TcpData { key: SessionKey, data: Vec }, + TcpClosed { key: SessionKey }, + UdpData { key: SessionKey, data: Vec }, +} + +// ============================================================================ +// IP packet parsing helpers +// ============================================================================ + +fn parse_ipv4_header(packet: &[u8]) -> Option<(u8, Ipv4Addr, Ipv4Addr, u8)> { + if packet.len() < 20 { + return None; + } + let version = packet[0] >> 4; + if version != 4 { + return None; + } + let ihl = (packet[0] & 0x0F) as usize * 4; + let protocol = packet[9]; + let src = Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]); + let dst = Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]); + Some((ihl as u8, src, dst, protocol)) +} + +fn parse_tcp_ports(packet: &[u8], ihl: usize) -> Option<(u16, u16, u8)> { + if packet.len() < ihl + 14 { + return None; + } + let src_port = u16::from_be_bytes([packet[ihl], packet[ihl + 1]]); + let dst_port = u16::from_be_bytes([packet[ihl + 2], packet[ihl + 3]]); + let flags = packet[ihl + 13]; + Some((src_port, dst_port, flags)) +} + +fn parse_udp_ports(packet: &[u8], ihl: usize) -> Option<(u16, u16)> { + if packet.len() < ihl + 4 { + return None; + } + let src_port = u16::from_be_bytes([packet[ihl], packet[ihl + 1]]); + let dst_port = u16::from_be_bytes([packet[ihl + 2], packet[ihl + 3]]); + Some((src_port, dst_port)) +} + +// ============================================================================ +// NAT Engine +// ============================================================================ + +pub struct NatEngine { + device: VirtualIpDevice, + iface: Interface, + sockets: SocketSet<'static>, + tcp_sessions: HashMap, + udp_sessions: HashMap, + state: Arc, + bridge_rx: mpsc::Receiver, + bridge_tx: mpsc::Sender, + start_time: std::time::Instant, +} + +impl NatEngine { + pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc) -> Self { + let mut device = VirtualIpDevice::new(mtu); + let config = Config::new(HardwareAddress::Ip); + let now = smoltcp::time::Instant::from_millis(0); + let mut iface = Interface::new(config, &mut device, now); + + // Accept packets to ANY destination IP (essential for NAT) + iface.set_any_ip(true); + + // Assign the gateway IP as the interface address + iface.update_ip_addrs(|addrs| { + addrs + .push(IpCidr::new(IpAddress::Ipv4(gateway_ip.into()), 24)) + .unwrap(); + }); + + // Add a default route so smoltcp knows where to send packets + iface.routes_mut().add_default_ipv4_route(gateway_ip.into()).unwrap(); + + let sockets = SocketSet::new(Vec::with_capacity(256)); + let (bridge_tx, bridge_rx) = mpsc::channel(4096); + + Self { + device, + iface, + sockets, + tcp_sessions: HashMap::new(), + udp_sessions: HashMap::new(), + state, + bridge_rx, + bridge_tx, + start_time: std::time::Instant::now(), + } + } + + fn smoltcp_now(&self) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_millis(self.start_time.elapsed().as_millis() as i64) + } + + /// Inject a raw IP packet from a VPN client and handle new session creation. + fn inject_packet(&mut self, packet: Vec) { + let Some((ihl, src_ip, dst_ip, protocol)) = parse_ipv4_header(&packet) else { + return; + }; + let ihl = ihl as usize; + + match protocol { + 6 => { + // TCP + let Some((src_port, dst_port, flags)) = parse_tcp_ports(&packet, ihl) else { + return; + }; + let key = SessionKey { + src_ip, + src_port, + dst_ip, + dst_port, + protocol: 6, + }; + + // SYN without ACK = new connection + let is_syn = (flags & 0x02) != 0 && (flags & 0x10) == 0; + if is_syn && !self.tcp_sessions.contains_key(&key) { + self.create_tcp_session(&key); + } + } + 17 => { + // UDP + let Some((src_port, dst_port)) = parse_udp_ports(&packet, ihl) else { + return; + }; + let key = SessionKey { + src_ip, + src_port, + dst_ip, + dst_port, + protocol: 17, + }; + + if !self.udp_sessions.contains_key(&key) { + self.create_udp_session(&key); + } + + // Update last_activity for existing sessions + if let Some(session) = self.udp_sessions.get_mut(&key) { + session.last_activity = tokio::time::Instant::now(); + } + } + _ => { + // ICMP and other protocols — not forwarded in socket mode + return; + } + } + + self.device.inject_packet(packet); + } + + fn create_tcp_session(&mut self, key: &SessionKey) { + // Create smoltcp TCP socket + let tcp_rx_buf = tcp::SocketBuffer::new(vec![0u8; 65535]); + let tcp_tx_buf = tcp::SocketBuffer::new(vec![0u8; 65535]); + let mut socket = tcp::Socket::new(tcp_rx_buf, tcp_tx_buf); + + // Listen on the destination address so smoltcp accepts the SYN + let endpoint = IpEndpoint::new( + IpAddress::Ipv4(key.dst_ip.into()), + key.dst_port, + ); + if socket.listen(endpoint).is_err() { + warn!("NAT: failed to listen on {:?}", endpoint); + return; + } + + let handle = self.sockets.add(socket); + + // Channel for sending data from NAT engine to bridge task + let (data_tx, data_rx) = mpsc::channel::>(256); + + let session = TcpSession { + smoltcp_handle: handle, + bridge_data_tx: data_tx, + client_ip: key.src_ip, + }; + self.tcp_sessions.insert(key.clone(), session); + + // Spawn bridge task that connects to the real destination + let bridge_tx = self.bridge_tx.clone(); + let key_clone = key.clone(); + tokio::spawn(async move { + tcp_bridge_task(key_clone, data_rx, bridge_tx).await; + }); + + debug!( + "NAT: new TCP session {}:{} -> {}:{}", + key.src_ip, key.src_port, key.dst_ip, key.dst_port + ); + } + + fn create_udp_session(&mut self, key: &SessionKey) { + // Create smoltcp UDP socket + let udp_rx_buf = udp::PacketBuffer::new( + vec![udp::PacketMetadata::EMPTY; 32], + vec![0u8; 65535], + ); + let udp_tx_buf = udp::PacketBuffer::new( + vec![udp::PacketMetadata::EMPTY; 32], + vec![0u8; 65535], + ); + let mut socket = udp::Socket::new(udp_rx_buf, udp_tx_buf); + + let endpoint = IpEndpoint::new( + IpAddress::Ipv4(key.dst_ip.into()), + key.dst_port, + ); + if socket.bind(endpoint).is_err() { + warn!("NAT: failed to bind UDP on {:?}", endpoint); + return; + } + + let handle = self.sockets.add(socket); + + let (data_tx, data_rx) = mpsc::channel::>(256); + + let session = UdpSession { + smoltcp_handle: handle, + bridge_data_tx: data_tx, + client_ip: key.src_ip, + last_activity: tokio::time::Instant::now(), + }; + self.udp_sessions.insert(key.clone(), session); + + let bridge_tx = self.bridge_tx.clone(); + let key_clone = key.clone(); + tokio::spawn(async move { + udp_bridge_task(key_clone, data_rx, bridge_tx).await; + }); + + debug!( + "NAT: new UDP session {}:{} -> {}:{}", + key.src_ip, key.src_port, key.dst_ip, key.dst_port + ); + } + + /// Poll smoltcp, bridge data between smoltcp sockets and bridge tasks, + /// and dispatch outgoing packets to VPN clients. + async fn process(&mut self) { + let now = self.smoltcp_now(); + self.iface + .poll(now, &mut self.device, &mut self.sockets); + + // Bridge: read data from smoltcp TCP sockets → send to bridge tasks + let mut closed_tcp: Vec = Vec::new(); + for (key, session) in &self.tcp_sessions { + let socket = self.sockets.get_mut::(session.smoltcp_handle); + if socket.can_recv() { + let _ = socket.recv(|data| { + let _ = session.bridge_data_tx.try_send(data.to_vec()); + (data.len(), ()) + }); + } + // Detect closed connections + if !socket.is_open() && !socket.is_listening() { + closed_tcp.push(key.clone()); + } + } + + // Clean up closed TCP sessions + for key in closed_tcp { + if let Some(session) = self.tcp_sessions.remove(&key) { + self.sockets.remove(session.smoltcp_handle); + debug!("NAT: TCP session closed {}:{} -> {}:{}", key.src_ip, key.src_port, key.dst_ip, key.dst_port); + } + } + + // Bridge: read data from smoltcp UDP sockets → send to bridge tasks + for (_key, session) in &self.udp_sessions { + let socket = self.sockets.get_mut::(session.smoltcp_handle); + while let Ok((data, _meta)) = socket.recv() { + let _ = session.bridge_data_tx.try_send(data.to_vec()); + } + } + + // Dispatch outgoing packets from smoltcp to VPN clients + let routes = self.state.tun_routes.read().await; + for packet in self.device.drain_tx() { + if let Some(std::net::IpAddr::V4(dst_ip)) = tunnel::extract_dst_ip(&packet) { + if let Some(sender) = routes.get(&dst_ip) { + let _ = sender.try_send(packet); + } + } + } + } + + fn handle_bridge_message(&mut self, msg: BridgeMessage) { + match msg { + BridgeMessage::TcpData { key, data } => { + if let Some(session) = self.tcp_sessions.get(&key) { + let socket = + self.sockets.get_mut::(session.smoltcp_handle); + if socket.can_send() { + let _ = socket.send_slice(&data); + } + } + } + BridgeMessage::TcpClosed { key } => { + if let Some(session) = self.tcp_sessions.remove(&key) { + let socket = + self.sockets.get_mut::(session.smoltcp_handle); + socket.close(); + // Don't remove from SocketSet yet — let smoltcp send FIN + // It will be cleaned up in process() when is_open() returns false + self.tcp_sessions.insert(key, session); + } + } + BridgeMessage::UdpData { key, data } => { + if let Some(session) = self.udp_sessions.get_mut(&key) { + session.last_activity = tokio::time::Instant::now(); + let socket = + self.sockets.get_mut::(session.smoltcp_handle); + let dst_endpoint = IpEndpoint::new( + IpAddress::Ipv4(key.src_ip.into()), + key.src_port, + ); + // Send response: from the "server" (dst) back to the "client" (src) + let _ = socket.send_slice(&data, dst_endpoint); + } + } + } + } + + fn cleanup_idle_udp_sessions(&mut self) { + let timeout = Duration::from_secs(60); + let now = tokio::time::Instant::now(); + let expired: Vec = self + .udp_sessions + .iter() + .filter(|(_, s)| now.duration_since(s.last_activity) > timeout) + .map(|(k, _)| k.clone()) + .collect(); + + for key in expired { + if let Some(session) = self.udp_sessions.remove(&key) { + self.sockets.remove(session.smoltcp_handle); + debug!( + "NAT: UDP session timed out {}:{} -> {}:{}", + key.src_ip, key.src_port, key.dst_ip, key.dst_port + ); + } + } + } + + /// Main async event loop for the NAT engine. + pub async fn run( + mut self, + mut packet_rx: mpsc::Receiver>, + mut shutdown_rx: mpsc::Receiver<()>, + ) -> Result<()> { + info!("Userspace NAT engine started"); + let mut timer = tokio::time::interval(Duration::from_millis(50)); + let mut cleanup_timer = tokio::time::interval(Duration::from_secs(10)); + + loop { + tokio::select! { + Some(packet) = packet_rx.recv() => { + self.inject_packet(packet); + self.process().await; + } + Some(msg) = self.bridge_rx.recv() => { + self.handle_bridge_message(msg); + self.process().await; + } + _ = timer.tick() => { + // Periodic poll for smoltcp maintenance (TCP retransmit, etc.) + self.process().await; + } + _ = cleanup_timer.tick() => { + self.cleanup_idle_udp_sessions(); + } + _ = shutdown_rx.recv() => { + info!("Userspace NAT engine shutting down"); + break; + } + } + } + + Ok(()) + } +} + +// ============================================================================ +// Bridge tasks +// ============================================================================ + +async fn tcp_bridge_task( + key: SessionKey, + mut data_rx: mpsc::Receiver>, + bridge_tx: mpsc::Sender, +) { + let addr = SocketAddr::new(key.dst_ip.into(), key.dst_port); + + // Connect to real destination with timeout + let stream = match tokio::time::timeout(Duration::from_secs(30), TcpStream::connect(addr)).await + { + Ok(Ok(s)) => s, + Ok(Err(e)) => { + debug!("NAT TCP connect to {} failed: {}", addr, e); + let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await; + return; + } + Err(_) => { + debug!("NAT TCP connect to {} timed out", addr); + let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await; + return; + } + }; + + let (mut reader, mut writer) = stream.into_split(); + + // Read from real socket → send to NAT engine + let bridge_tx2 = bridge_tx.clone(); + let key2 = key.clone(); + let read_task = tokio::spawn(async move { + let mut buf = vec![0u8; 65536]; + loop { + match reader.read(&mut buf).await { + Ok(0) => break, + Ok(n) => { + if bridge_tx2 + .send(BridgeMessage::TcpData { + key: key2.clone(), + data: buf[..n].to_vec(), + }) + .await + .is_err() + { + break; + } + } + Err(_) => break, + } + } + let _ = bridge_tx2 + .send(BridgeMessage::TcpClosed { key: key2 }) + .await; + }); + + // Receive from NAT engine → write to real socket + while let Some(data) = data_rx.recv().await { + if writer.write_all(&data).await.is_err() { + break; + } + } + + read_task.abort(); +} + +async fn udp_bridge_task( + key: SessionKey, + mut data_rx: mpsc::Receiver>, + bridge_tx: mpsc::Sender, +) { + let socket = match UdpSocket::bind("0.0.0.0:0").await { + Ok(s) => s, + Err(e) => { + warn!("NAT UDP bind failed: {}", e); + return; + } + }; + let dest = SocketAddr::new(key.dst_ip.into(), key.dst_port); + + let socket = Arc::new(socket); + let socket2 = socket.clone(); + let bridge_tx2 = bridge_tx.clone(); + let key2 = key.clone(); + + // Read responses from real socket + let read_task = tokio::spawn(async move { + let mut buf = vec![0u8; 65536]; + loop { + match socket2.recv_from(&mut buf).await { + Ok((n, _src)) => { + if bridge_tx2 + .send(BridgeMessage::UdpData { + key: key2.clone(), + data: buf[..n].to_vec(), + }) + .await + .is_err() + { + break; + } + } + Err(_) => break, + } + } + }); + + // Forward data from NAT engine to real socket + while let Some(data) = data_rx.recv().await { + let _ = socket.send_to(&data, dest).await; + } + + read_task.abort(); +} diff --git a/rust/src/wireguard.rs b/rust/src/wireguard.rs index a96987c..2a9a06c 100644 --- a/rust/src/wireguard.rs +++ b/rust/src/wireguard.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; use std::time::Instant; @@ -18,6 +18,7 @@ use tokio::sync::{mpsc, oneshot, RwLock}; use tracing::{debug, error, info, warn}; use crate::network; +use crate::tunnel::extract_dst_ip; use crate::tunnel::{self, TunConfig}; // ============================================================================ @@ -228,26 +229,6 @@ impl AllowedIp { } } -/// Extract destination IP from an IP packet header. -fn extract_dst_ip(packet: &[u8]) -> Option { - if packet.is_empty() { - return None; - } - let version = packet[0] >> 4; - match version { - 4 if packet.len() >= 20 => { - let dst = Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]); - Some(IpAddr::V4(dst)) - } - 6 if packet.len() >= 40 => { - let mut octets = [0u8; 16]; - octets.copy_from_slice(&packet[24..40]); - Some(IpAddr::V6(Ipv6Addr::from(octets))) - } - _ => None, - } -} - // ============================================================================ // Dynamic peer management commands // ============================================================================ @@ -1096,6 +1077,7 @@ fn chrono_now() -> String { #[cfg(test)] mod tests { use super::*; + use std::net::Ipv6Addr; #[test] fn test_generate_wg_keypair() { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 6de5ed9..af35db2 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartvpn', - version: '1.9.0', + version: '1.10.0', description: 'A VPN solution with TypeScript control plane and Rust data plane daemon' } diff --git a/ts/smartvpn.interfaces.ts b/ts/smartvpn.interfaces.ts index 3d87ae5..a3d759c 100644 --- a/ts/smartvpn.interfaces.ts +++ b/ts/smartvpn.interfaces.ts @@ -40,6 +40,9 @@ export interface IVpnClientConfig { transport?: 'auto' | 'websocket' | 'quic' | 'wireguard'; /** For QUIC: SHA-256 hash of server certificate (base64) for cert pinning */ serverCertHash?: string; + /** Forwarding mode: 'tun' (TUN device, requires root) or 'testing' (no TUN). + * Default: 'testing'. */ + forwardingMode?: 'tun' | 'testing'; /** WireGuard: client private key (base64, X25519) */ wgPrivateKey?: string; /** WireGuard: client TUN address (e.g. 10.8.0.2) */ @@ -86,6 +89,9 @@ export interface IVpnServerConfig { keepaliveIntervalSecs?: number; /** Enable NAT/masquerade for client traffic */ enableNat?: boolean; + /** Forwarding mode: 'tun' (kernel TUN, requires root), 'socket' (userspace NAT), + * or 'testing' (monitoring only). Default: 'testing'. */ + forwardingMode?: 'tun' | 'socket' | 'testing'; /** Default rate limit for new clients (bytes/sec). Omit for unlimited. */ defaultRateLimitBytesPerSec?: number; /** Default burst size for new clients (bytes). Omit for unlimited. */