feat(server): add optional PROXY protocol v2 headers for socket-based userspace NAT forwarding
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user