2 Commits

12 changed files with 1131 additions and 25 deletions

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartvpn",
"version": "1.9.0",
"version": "1.10.0",
"private": false,
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
"type": "module",
@@ -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": {

115
rust/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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<String>,
/// For QUIC: SHA-256 hash of server certificate (base64) for cert pinning.
pub server_cert_hash: Option<String>,
/// Forwarding mode: "tun" (TUN device, requires root) or "testing" (no TUN).
/// Default: "testing".
pub forwarding_mode: Option<String>,
}
/// 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<KeepaliveSignal>,
ack_tx: mpsc::Sender<()>,
link_health: Arc<RwLock<LinkHealth>>,
mut tun_reader: Option<tokio::io::ReadHalf<tun::AsyncDevice>>,
mut tun_writer: Option<tokio::io::WriteHalf<tun::AsyncDevice>>,
tun_subnet: Option<String>,
) {
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::<std::io::Result<usize>>().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 <FrameCodec as tokio_util::codec::Encoder<Frame>>::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.

View File

@@ -21,3 +21,4 @@ pub mod wireguard;
pub mod client_registry;
pub mod acl;
pub mod proxy_protocol;
pub mod userspace_nat;

View File

@@ -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<u16>,
pub keepalive_interval_secs: Option<u64>,
pub enable_nat: Option<bool>,
/// Forwarding mode: "tun" (kernel TUN, requires root), "socket" (userspace NAT),
/// or "testing" (monitoring only, no forwarding). Default: "testing".
pub forwarding_mode: Option<String>,
/// Default rate limit for new clients (bytes/sec). None = unlimited.
pub default_rate_limit_bytes_per_sec: Option<u64>,
/// 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<tun::AsyncDevice>),
/// Userspace NAT — packets sent to smoltcp-based NAT engine via channel.
Socket(mpsc::Sender<Vec<u8>>),
/// 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<ClientRegistry>,
/// The forwarding engine for decrypted IP packets.
pub forwarding_engine: Mutex<ForwardingEngine>,
/// Routing table: assigned VPN IP → channel sender for return packets.
pub tun_routes: RwLock<HashMap<Ipv4Addr, mpsc::Sender<Vec<u8>>>>,
/// 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<tun::AsyncDevice>,
reader: tokio::io::ReadHalf<tun::AsyncDevice>,
shutdown_rx: mpsc::Receiver<()>,
},
Socket {
packet_tx: mpsc::Sender<Vec<u8>>,
packet_rx: mpsc::Receiver<Vec<u8>>,
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::<Vec<u8>>(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<ServerState>,
mut tun_reader: tokio::io::ReadHalf<tun::AsyncDevice>,
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::<Vec<u8>>(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();
<FrameCodec as tokio_util::codec::Encoder<Frame>>::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);

View File

@@ -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<IpAddr> {
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")

640
rust/src/userspace_nat.rs Normal file
View File

@@ -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<Vec<u8>>,
tx_queue: VecDeque<Vec<u8>>,
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<u8>) {
self.rx_queue.push_back(packet);
}
pub fn drain_tx(&mut self) -> impl Iterator<Item = Vec<u8>> + '_ {
self.tx_queue.drain(..)
}
}
pub struct VirtualRxToken {
buffer: Vec<u8>,
}
impl phy::RxToken for VirtualRxToken {
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&[u8]) -> R,
{
f(&self.buffer)
}
}
pub struct VirtualTxToken<'a> {
queue: &'a mut VecDeque<Vec<u8>>,
}
impl<'a> phy::TxToken for VirtualTxToken<'a> {
fn consume<R, F>(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<Self::TxToken<'_>> {
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<Vec<u8>>,
#[allow(dead_code)]
client_ip: Ipv4Addr,
}
struct UdpSession {
smoltcp_handle: SocketHandle,
bridge_data_tx: mpsc::Sender<Vec<u8>>,
#[allow(dead_code)]
client_ip: Ipv4Addr,
last_activity: tokio::time::Instant,
}
enum BridgeMessage {
TcpData { key: SessionKey, data: Vec<u8> },
TcpClosed { key: SessionKey },
UdpData { key: SessionKey, data: Vec<u8> },
}
// ============================================================================
// 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<SessionKey, TcpSession>,
udp_sessions: HashMap<SessionKey, UdpSession>,
state: Arc<ServerState>,
bridge_rx: mpsc::Receiver<BridgeMessage>,
bridge_tx: mpsc::Sender<BridgeMessage>,
start_time: std::time::Instant,
}
impl NatEngine {
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>) -> 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<u8>) {
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::<Vec<u8>>(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::<Vec<u8>>(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<SessionKey> = Vec::new();
for (key, session) in &self.tcp_sessions {
let socket = self.sockets.get_mut::<tcp::Socket>(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::<udp::Socket>(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::<tcp::Socket>(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::<tcp::Socket>(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::<udp::Socket>(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<SessionKey> = 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<Vec<u8>>,
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<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>,
) {
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<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>,
) {
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();
}

View File

@@ -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<IpAddr> {
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() {

View File

@@ -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'
}

View File

@@ -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. */