diff --git a/changelog.md b/changelog.md index 15bc8ac..f41c583 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 1.12.0 - feat(server) +add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding + +- introduce a socketForwardProxyProtocol server option in Rust and TypeScript interfaces +- pass the new setting into the userspace NAT engine and TCP bridge tasks +- prepend PROXY protocol v2 headers on outbound TCP connections when socket forwarding is enabled + ## 2026-03-30 - 1.11.0 - feat(server) unify WireGuard into the shared server transport pipeline diff --git a/rust/src/server.rs b/rust/src/server.rs index c91948c..77e347b 100644 --- a/rust/src/server.rs +++ b/rust/src/server.rs @@ -58,6 +58,10 @@ pub struct ServerConfig { pub proxy_protocol: Option, /// Server-level IP block list — applied at TCP accept, before Noise handshake. pub connection_ip_block_list: Option>, + /// When true and forwarding_mode is "socket", the userspace NAT engine prepends + /// PROXY protocol v2 headers on outbound TCP connections, conveying the VPN client's + /// tunnel IP as the source address. + pub socket_forward_proxy_protocol: Option, /// WireGuard: server X25519 private key (base64). Required when transport includes WG. pub wg_private_key: Option, /// WireGuard: UDP listen port (default: 51820). @@ -251,10 +255,12 @@ impl VpnServer { } ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx } => { *state.forwarding_engine.lock().await = ForwardingEngine::Socket(packet_tx); + let proxy_protocol = config.socket_forward_proxy_protocol.unwrap_or(false); let nat_engine = crate::userspace_nat::NatEngine::new( gateway_ip, link_mtu as usize, state.clone(), + proxy_protocol, ); tokio::spawn(async move { if let Err(e) = nat_engine.run(packet_rx, shutdown_rx).await { diff --git a/rust/src/userspace_nat.rs b/rust/src/userspace_nat.rs index 6a1cd72..16fee48 100644 --- a/rust/src/userspace_nat.rs +++ b/rust/src/userspace_nat.rs @@ -191,10 +191,13 @@ pub struct NatEngine { bridge_rx: mpsc::Receiver, bridge_tx: mpsc::Sender, start_time: std::time::Instant, + /// When true, outbound TCP connections prepend PROXY protocol v2 headers + /// with the VPN client's tunnel IP as source address. + proxy_protocol: bool, } impl NatEngine { - pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc) -> Self { + pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc, proxy_protocol: bool) -> Self { let mut device = VirtualIpDevice::new(mtu); let config = Config::new(HardwareAddress::Ip); let now = smoltcp::time::Instant::from_millis(0); @@ -226,6 +229,7 @@ impl NatEngine { bridge_rx, bridge_tx, start_time: std::time::Instant::now(), + proxy_protocol, } } @@ -322,8 +326,9 @@ impl NatEngine { // Spawn bridge task that connects to the real destination let bridge_tx = self.bridge_tx.clone(); let key_clone = key.clone(); + let proxy_protocol = self.proxy_protocol; tokio::spawn(async move { - tcp_bridge_task(key_clone, data_rx, bridge_tx).await; + tcp_bridge_task(key_clone, data_rx, bridge_tx, proxy_protocol).await; }); debug!( @@ -531,6 +536,7 @@ async fn tcp_bridge_task( key: SessionKey, mut data_rx: mpsc::Receiver>, bridge_tx: mpsc::Sender, + proxy_protocol: bool, ) { let addr = SocketAddr::new(key.dst_ip.into(), key.dst_port); @@ -552,6 +558,18 @@ async fn tcp_bridge_task( let (mut reader, mut writer) = stream.into_split(); + // Send PROXY protocol v2 header with VPN client's tunnel IP as source + if proxy_protocol { + let src = SocketAddr::new(key.src_ip.into(), key.src_port); + let dst = SocketAddr::new(key.dst_ip.into(), key.dst_port); + let pp_header = crate::proxy_protocol::build_pp_v2_header(src, dst); + if let Err(e) = writer.write_all(&pp_header).await { + debug!("NAT: failed to send PP v2 header to {}: {}", addr, e); + let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await; + return; + } + } + // Read from real socket → send to NAT engine let bridge_tx2 = bridge_tx.clone(); let key2 = key.clone(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c8a7290..6742895 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.11.0', + version: '1.12.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 093b4ad..6600fa5 100644 --- a/ts/smartvpn.interfaces.ts +++ b/ts/smartvpn.interfaces.ts @@ -118,6 +118,11 @@ export interface IVpnServerConfig { /** Server-level IP block list — applied at TCP accept, before Noise handshake. * Supports exact IPs, CIDR, wildcards, ranges. */ connectionIpBlockList?: string[]; + /** When true and forwardingMode is 'socket', the userspace NAT engine prepends + * PROXY protocol v2 headers on outbound TCP connections, conveying the VPN client's + * tunnel IP as the source address. This allows downstream services (e.g. SmartProxy) + * to see the real VPN client identity instead of 127.0.0.1. */ + socketForwardProxyProtocol?: boolean; } export interface IVpnServerOptions {