feat(server): add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding

This commit is contained in:
2026-03-30 07:13:49 +00:00
parent af46dc9b39
commit 5bf21ab4ac
5 changed files with 39 additions and 3 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # 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) ## 2026-03-30 - 1.11.0 - feat(server)
unify WireGuard into the shared server transport pipeline unify WireGuard into the shared server transport pipeline

View File

@@ -58,6 +58,10 @@ pub struct ServerConfig {
pub proxy_protocol: Option<bool>, pub proxy_protocol: Option<bool>,
/// Server-level IP block list — applied at TCP accept, before Noise handshake. /// Server-level IP block list — applied at TCP accept, before Noise handshake.
pub connection_ip_block_list: Option<Vec<String>>, pub connection_ip_block_list: Option<Vec<String>>,
/// 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<bool>,
/// WireGuard: server X25519 private key (base64). Required when transport includes WG. /// WireGuard: server X25519 private key (base64). Required when transport includes WG.
pub wg_private_key: Option<String>, pub wg_private_key: Option<String>,
/// WireGuard: UDP listen port (default: 51820). /// WireGuard: UDP listen port (default: 51820).
@@ -251,10 +255,12 @@ impl VpnServer {
} }
ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx } => { ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx } => {
*state.forwarding_engine.lock().await = ForwardingEngine::Socket(packet_tx); *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( let nat_engine = crate::userspace_nat::NatEngine::new(
gateway_ip, gateway_ip,
link_mtu as usize, link_mtu as usize,
state.clone(), state.clone(),
proxy_protocol,
); );
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = nat_engine.run(packet_rx, shutdown_rx).await { if let Err(e) = nat_engine.run(packet_rx, shutdown_rx).await {

View File

@@ -191,10 +191,13 @@ pub struct NatEngine {
bridge_rx: mpsc::Receiver<BridgeMessage>, bridge_rx: mpsc::Receiver<BridgeMessage>,
bridge_tx: mpsc::Sender<BridgeMessage>, bridge_tx: mpsc::Sender<BridgeMessage>,
start_time: std::time::Instant, 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 { impl NatEngine {
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>) -> Self { pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>, proxy_protocol: bool) -> Self {
let mut device = VirtualIpDevice::new(mtu); let mut device = VirtualIpDevice::new(mtu);
let config = Config::new(HardwareAddress::Ip); let config = Config::new(HardwareAddress::Ip);
let now = smoltcp::time::Instant::from_millis(0); let now = smoltcp::time::Instant::from_millis(0);
@@ -226,6 +229,7 @@ impl NatEngine {
bridge_rx, bridge_rx,
bridge_tx, bridge_tx,
start_time: std::time::Instant::now(), start_time: std::time::Instant::now(),
proxy_protocol,
} }
} }
@@ -322,8 +326,9 @@ impl NatEngine {
// Spawn bridge task that connects to the real destination // Spawn bridge task that connects to the real destination
let bridge_tx = self.bridge_tx.clone(); let bridge_tx = self.bridge_tx.clone();
let key_clone = key.clone(); let key_clone = key.clone();
let proxy_protocol = self.proxy_protocol;
tokio::spawn(async move { 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!( debug!(
@@ -531,6 +536,7 @@ async fn tcp_bridge_task(
key: SessionKey, key: SessionKey,
mut data_rx: mpsc::Receiver<Vec<u8>>, mut data_rx: mpsc::Receiver<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>, bridge_tx: mpsc::Sender<BridgeMessage>,
proxy_protocol: bool,
) { ) {
let addr = SocketAddr::new(key.dst_ip.into(), key.dst_port); 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(); 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 // Read from real socket → send to NAT engine
let bridge_tx2 = bridge_tx.clone(); let bridge_tx2 = bridge_tx.clone();
let key2 = key.clone(); let key2 = key.clone();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartvpn', 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' description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
} }

View File

@@ -118,6 +118,11 @@ export interface IVpnServerConfig {
/** Server-level IP block list — applied at TCP accept, before Noise handshake. /** Server-level IP block list — applied at TCP accept, before Noise handshake.
* Supports exact IPs, CIDR, wildcards, ranges. */ * Supports exact IPs, CIDR, wildcards, ranges. */
connectionIpBlockList?: string[]; 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 { export interface IVpnServerOptions {