2 Commits

6 changed files with 40 additions and 4 deletions

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartvpn",
"version": "1.11.0",
"version": "1.12.0",
"private": false,
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
"type": "module",

View File

@@ -58,6 +58,10 @@ pub struct ServerConfig {
pub proxy_protocol: Option<bool>,
/// Server-level IP block list — applied at TCP accept, before Noise handshake.
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.
pub wg_private_key: Option<String>,
/// 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 {

View File

@@ -191,10 +191,13 @@ pub struct NatEngine {
bridge_rx: mpsc::Receiver<BridgeMessage>,
bridge_tx: mpsc::Sender<BridgeMessage>,
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<ServerState>) -> Self {
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>, 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<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>,
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();

View File

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

View File

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