Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 004c9ed252 | |||
| 90d7f0903b |
@@ -3,6 +3,13 @@
|
|||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-05-24 - 1.20.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add PROXY v2 real-source forwarding with authenticated VPN metadata TLVs (userspace-nat)
|
||||||
|
- allows socket forwarding to emit the client remote IP instead of the tunnel IP when configured
|
||||||
|
- adds SmartVPN client metadata to outbound PROXY v2 headers for trusted downstream authorization
|
||||||
|
|
||||||
## 2026-05-12 - 1.19.4
|
## 2026-05-12 - 1.19.4
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartvpn",
|
"name": "@push.rocks/smartvpn",
|
||||||
"version": "1.19.4",
|
"version": "1.20.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//! Spec: <https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt>
|
//! Spec: <https://www.haproxy.org/download/2.9/doc/proxy-protocol.txt>
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use serde::Serialize;
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
@@ -17,6 +18,20 @@ const PP_V2_SIGNATURE: [u8; 12] = [
|
|||||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
|
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Custom PROXY v2 TLV type for authenticated SmartVPN connection metadata.
|
||||||
|
/// 0xEA is in the PP2_TYPE_MIN_CUSTOM range (0xE0-0xEF).
|
||||||
|
pub const PP2_TYPE_SMARTVPN_METADATA: u8 = 0xEA;
|
||||||
|
|
||||||
|
/// Authenticated VPN metadata sent to trusted downstream proxies.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct VpnProxyMetadata {
|
||||||
|
pub client_id: String,
|
||||||
|
pub assigned_ip: String,
|
||||||
|
pub transport_type: String,
|
||||||
|
pub remote_addr: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parsed PROXY protocol v2 header.
|
/// Parsed PROXY protocol v2 header.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProxyHeader {
|
pub struct ProxyHeader {
|
||||||
@@ -124,6 +139,26 @@ async fn read_proxy_header_inner(stream: &mut TcpStream) -> Result<ProxyHeader>
|
|||||||
|
|
||||||
/// Build a PROXY protocol v2 header (for testing / proxy implementations).
|
/// Build a PROXY protocol v2 header (for testing / proxy implementations).
|
||||||
pub fn build_pp_v2_header(src: SocketAddr, dst: SocketAddr) -> Vec<u8> {
|
pub fn build_pp_v2_header(src: SocketAddr, dst: SocketAddr) -> Vec<u8> {
|
||||||
|
build_pp_v2_header_with_vpn_metadata(src, dst, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a PROXY protocol v2 header with optional SmartVPN metadata TLV.
|
||||||
|
pub fn build_pp_v2_header_with_vpn_metadata(
|
||||||
|
src: SocketAddr,
|
||||||
|
dst: SocketAddr,
|
||||||
|
vpn_metadata: Option<&VpnProxyMetadata>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut tlv_bytes = Vec::new();
|
||||||
|
if let Some(metadata) = vpn_metadata {
|
||||||
|
if let Ok(json) = serde_json::to_vec(metadata) {
|
||||||
|
if json.len() <= u16::MAX as usize {
|
||||||
|
tlv_bytes.push(PP2_TYPE_SMARTVPN_METADATA);
|
||||||
|
tlv_bytes.extend_from_slice(&(json.len() as u16).to_be_bytes());
|
||||||
|
tlv_bytes.extend_from_slice(&json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
buf.extend_from_slice(&PP_V2_SIGNATURE);
|
buf.extend_from_slice(&PP_V2_SIGNATURE);
|
||||||
|
|
||||||
@@ -131,22 +166,38 @@ pub fn build_pp_v2_header(src: SocketAddr, dst: SocketAddr) -> Vec<u8> {
|
|||||||
(SocketAddr::V4(s), SocketAddr::V4(d)) => {
|
(SocketAddr::V4(s), SocketAddr::V4(d)) => {
|
||||||
buf.push(0x21); // version 2 | PROXY command
|
buf.push(0x21); // version 2 | PROXY command
|
||||||
buf.push(0x11); // AF_INET | STREAM
|
buf.push(0x11); // AF_INET | STREAM
|
||||||
buf.extend_from_slice(&12u16.to_be_bytes()); // addr length
|
buf.extend_from_slice(&((12 + tlv_bytes.len()) as u16).to_be_bytes()); // addr length
|
||||||
buf.extend_from_slice(&s.ip().octets());
|
buf.extend_from_slice(&s.ip().octets());
|
||||||
buf.extend_from_slice(&d.ip().octets());
|
buf.extend_from_slice(&d.ip().octets());
|
||||||
buf.extend_from_slice(&s.port().to_be_bytes());
|
buf.extend_from_slice(&s.port().to_be_bytes());
|
||||||
buf.extend_from_slice(&d.port().to_be_bytes());
|
buf.extend_from_slice(&d.port().to_be_bytes());
|
||||||
|
buf.extend_from_slice(&tlv_bytes);
|
||||||
}
|
}
|
||||||
(SocketAddr::V6(s), SocketAddr::V6(d)) => {
|
(SocketAddr::V6(s), SocketAddr::V6(d)) => {
|
||||||
buf.push(0x21); // version 2 | PROXY command
|
buf.push(0x21); // version 2 | PROXY command
|
||||||
buf.push(0x21); // AF_INET6 | STREAM
|
buf.push(0x21); // AF_INET6 | STREAM
|
||||||
buf.extend_from_slice(&36u16.to_be_bytes()); // addr length
|
buf.extend_from_slice(&((36 + tlv_bytes.len()) as u16).to_be_bytes()); // addr length
|
||||||
buf.extend_from_slice(&s.ip().octets());
|
buf.extend_from_slice(&s.ip().octets());
|
||||||
buf.extend_from_slice(&d.ip().octets());
|
buf.extend_from_slice(&d.ip().octets());
|
||||||
buf.extend_from_slice(&s.port().to_be_bytes());
|
buf.extend_from_slice(&s.port().to_be_bytes());
|
||||||
buf.extend_from_slice(&d.port().to_be_bytes());
|
buf.extend_from_slice(&d.port().to_be_bytes());
|
||||||
|
buf.extend_from_slice(&tlv_bytes);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let src_v6 = match src.ip() {
|
||||||
|
std::net::IpAddr::V4(v4) => v4.to_ipv6_mapped(),
|
||||||
|
std::net::IpAddr::V6(v6) => v6,
|
||||||
|
};
|
||||||
|
let dst_v6 = match dst.ip() {
|
||||||
|
std::net::IpAddr::V4(v4) => v4.to_ipv6_mapped(),
|
||||||
|
std::net::IpAddr::V6(v6) => v6,
|
||||||
|
};
|
||||||
|
return build_pp_v2_header_with_vpn_metadata(
|
||||||
|
SocketAddr::V6(SocketAddrV6::new(src_v6, src.port(), 0, 0)),
|
||||||
|
SocketAddr::V6(SocketAddrV6::new(dst_v6, dst.port(), 0, 0)),
|
||||||
|
vpn_metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => panic!("Mismatched address families"),
|
|
||||||
}
|
}
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
@@ -197,6 +248,30 @@ mod tests {
|
|||||||
assert_eq!(parsed.dst_addr, dst);
|
assert_eq!(parsed.dst_addr, dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn build_ipv4_header_with_smartvpn_metadata_tlv() {
|
||||||
|
let src = "203.0.113.50:12345".parse::<SocketAddr>().unwrap();
|
||||||
|
let dst = "10.0.0.1:443".parse::<SocketAddr>().unwrap();
|
||||||
|
let metadata = VpnProxyMetadata {
|
||||||
|
client_id: "alice".to_string(),
|
||||||
|
assigned_ip: "10.8.0.2".to_string(),
|
||||||
|
transport_type: "wireguard".to_string(),
|
||||||
|
remote_addr: Some("203.0.113.50:51820".to_string()),
|
||||||
|
};
|
||||||
|
let header = build_pp_v2_header_with_vpn_metadata(src, dst, Some(&metadata));
|
||||||
|
|
||||||
|
let addr_len = u16::from_be_bytes([header[14], header[15]]) as usize;
|
||||||
|
assert!(addr_len > 12);
|
||||||
|
assert_eq!(header[28], PP2_TYPE_SMARTVPN_METADATA);
|
||||||
|
let tlv_len = u16::from_be_bytes([header[29], header[30]]) as usize;
|
||||||
|
let json = std::str::from_utf8(&header[31..31 + tlv_len]).unwrap();
|
||||||
|
assert!(json.contains("\"clientId\":\"alice\""));
|
||||||
|
|
||||||
|
let parsed = parse_header_from_bytes(&header).await.unwrap();
|
||||||
|
assert_eq!(parsed.src_addr, src);
|
||||||
|
assert_eq!(parsed.dst_addr, dst);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn parse_valid_ipv6_header() {
|
async fn parse_valid_ipv6_header() {
|
||||||
let src = "[2001:db8::1]:54321".parse::<SocketAddr>().unwrap();
|
let src = "[2001:db8::1]:54321".parse::<SocketAddr>().unwrap();
|
||||||
|
|||||||
+17
-2
@@ -73,9 +73,12 @@ pub struct ServerConfig {
|
|||||||
/// 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
|
/// 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
|
/// PROXY protocol v2 headers on outbound TCP connections.
|
||||||
/// tunnel IP as the source address.
|
|
||||||
pub socket_forward_proxy_protocol: Option<bool>,
|
pub socket_forward_proxy_protocol: Option<bool>,
|
||||||
|
/// Source address for outbound PROXY protocol headers: "tunnelIp" (legacy) or "remoteIp".
|
||||||
|
pub socket_forward_proxy_protocol_source: Option<String>,
|
||||||
|
/// Include authenticated SmartVPN metadata as a custom PROXY v2 TLV.
|
||||||
|
pub socket_forward_proxy_protocol_vpn_metadata: Option<bool>,
|
||||||
/// Destination routing policy for VPN client traffic (socket mode).
|
/// Destination routing policy for VPN client traffic (socket mode).
|
||||||
pub destination_policy: Option<DestinationPolicyConfig>,
|
pub destination_policy: Option<DestinationPolicyConfig>,
|
||||||
/// WireGuard: server X25519 private key (base64). Required when transport includes WG.
|
/// WireGuard: server X25519 private key (base64). Required when transport includes WG.
|
||||||
@@ -431,11 +434,17 @@ 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 proxy_protocol = config.socket_forward_proxy_protocol.unwrap_or(false);
|
||||||
|
let proxy_protocol_source = crate::userspace_nat::ProxyProtocolSource::from_config(
|
||||||
|
config.socket_forward_proxy_protocol_source.as_deref(),
|
||||||
|
);
|
||||||
|
let proxy_protocol_vpn_metadata = config.socket_forward_proxy_protocol_vpn_metadata.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,
|
proxy_protocol,
|
||||||
|
proxy_protocol_source,
|
||||||
|
proxy_protocol_vpn_metadata,
|
||||||
config.destination_policy.clone(),
|
config.destination_policy.clone(),
|
||||||
);
|
);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -473,11 +482,17 @@ impl VpnServer {
|
|||||||
|
|
||||||
// Start socket (NAT) engine
|
// Start socket (NAT) engine
|
||||||
let proxy_protocol = config.socket_forward_proxy_protocol.unwrap_or(false);
|
let proxy_protocol = config.socket_forward_proxy_protocol.unwrap_or(false);
|
||||||
|
let proxy_protocol_source = crate::userspace_nat::ProxyProtocolSource::from_config(
|
||||||
|
config.socket_forward_proxy_protocol_source.as_deref(),
|
||||||
|
);
|
||||||
|
let proxy_protocol_vpn_metadata = config.socket_forward_proxy_protocol_vpn_metadata.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,
|
proxy_protocol,
|
||||||
|
proxy_protocol_source,
|
||||||
|
proxy_protocol_vpn_metadata,
|
||||||
config.destination_policy.clone(),
|
config.destination_policy.clone(),
|
||||||
);
|
);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
|||||||
@@ -21,6 +21,21 @@ use crate::tunnel;
|
|||||||
/// Sessions exceeding this are aborted — the client cannot keep up.
|
/// Sessions exceeding this are aborted — the client cannot keep up.
|
||||||
const TCP_PENDING_SEND_MAX: usize = 512 * 1024;
|
const TCP_PENDING_SEND_MAX: usize = 512 * 1024;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProxyProtocolSource {
|
||||||
|
TunnelIp,
|
||||||
|
RemoteIp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyProtocolSource {
|
||||||
|
pub fn from_config(value: Option<&str>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some("remoteIp") => ProxyProtocolSource::RemoteIp,
|
||||||
|
_ => ProxyProtocolSource::TunnelIp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Virtual IP device for smoltcp
|
// Virtual IP device for smoltcp
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -208,8 +223,10 @@ pub struct NatEngine {
|
|||||||
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
|
/// When true, outbound TCP connections prepend PROXY protocol v2 headers
|
||||||
/// with the VPN client's tunnel IP as source address.
|
/// with VPN source identity.
|
||||||
proxy_protocol: bool,
|
proxy_protocol: bool,
|
||||||
|
proxy_protocol_source: ProxyProtocolSource,
|
||||||
|
proxy_protocol_vpn_metadata: bool,
|
||||||
/// Destination routing policy: forceTarget, block, or allow.
|
/// Destination routing policy: forceTarget, block, or allow.
|
||||||
destination_policy: Option<DestinationPolicyConfig>,
|
destination_policy: Option<DestinationPolicyConfig>,
|
||||||
}
|
}
|
||||||
@@ -225,7 +242,15 @@ enum DestinationAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NatEngine {
|
impl NatEngine {
|
||||||
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>, proxy_protocol: bool, destination_policy: Option<DestinationPolicyConfig>) -> Self {
|
pub fn new(
|
||||||
|
gateway_ip: Ipv4Addr,
|
||||||
|
mtu: usize,
|
||||||
|
state: Arc<ServerState>,
|
||||||
|
proxy_protocol: bool,
|
||||||
|
proxy_protocol_source: ProxyProtocolSource,
|
||||||
|
proxy_protocol_vpn_metadata: bool,
|
||||||
|
destination_policy: Option<DestinationPolicyConfig>,
|
||||||
|
) -> 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);
|
||||||
@@ -258,6 +283,8 @@ impl NatEngine {
|
|||||||
bridge_tx,
|
bridge_tx,
|
||||||
start_time: std::time::Instant::now(),
|
start_time: std::time::Instant::now(),
|
||||||
proxy_protocol,
|
proxy_protocol,
|
||||||
|
proxy_protocol_source,
|
||||||
|
proxy_protocol_vpn_metadata,
|
||||||
destination_policy,
|
destination_policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,6 +508,9 @@ impl NatEngine {
|
|||||||
// Start bridge tasks for sessions whose handshake just completed
|
// Start bridge tasks for sessions whose handshake just completed
|
||||||
let bridge_tx_clone = self.bridge_tx.clone();
|
let bridge_tx_clone = self.bridge_tx.clone();
|
||||||
let proxy_protocol = self.proxy_protocol;
|
let proxy_protocol = self.proxy_protocol;
|
||||||
|
let proxy_protocol_source = self.proxy_protocol_source;
|
||||||
|
let proxy_protocol_vpn_metadata = self.proxy_protocol_vpn_metadata;
|
||||||
|
let state = Arc::clone(&self.state);
|
||||||
for (key, session) in self.tcp_sessions.iter_mut() {
|
for (key, session) in self.tcp_sessions.iter_mut() {
|
||||||
if !session.bridge_started && !session.closing {
|
if !session.bridge_started && !session.closing {
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(session.smoltcp_handle);
|
let socket = self.sockets.get_mut::<tcp::Socket>(session.smoltcp_handle);
|
||||||
@@ -492,8 +522,11 @@ impl NatEngine {
|
|||||||
let k = key.clone();
|
let k = key.clone();
|
||||||
let addr = session.connect_addr;
|
let addr = session.connect_addr;
|
||||||
let pp = proxy_protocol;
|
let pp = proxy_protocol;
|
||||||
|
let pp_source = proxy_protocol_source;
|
||||||
|
let pp_metadata = proxy_protocol_vpn_metadata;
|
||||||
|
let state = Arc::clone(&state);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
tcp_bridge_task(k, data_rx, btx, pp, addr).await;
|
tcp_bridge_task(k, data_rx, btx, pp, pp_source, pp_metadata, state, addr).await;
|
||||||
});
|
});
|
||||||
debug!("NAT: TCP handshake complete, starting bridge for {}:{} -> {}:{}",
|
debug!("NAT: TCP handshake complete, starting bridge for {}:{} -> {}:{}",
|
||||||
key.src_ip, key.src_port, key.dst_ip, key.dst_port);
|
key.src_ip, key.src_port, key.dst_ip, key.dst_port);
|
||||||
@@ -748,6 +781,9 @@ async fn tcp_bridge_task(
|
|||||||
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,
|
proxy_protocol: bool,
|
||||||
|
proxy_protocol_source: ProxyProtocolSource,
|
||||||
|
proxy_protocol_vpn_metadata: bool,
|
||||||
|
state: Arc<ServerState>,
|
||||||
connect_addr: SocketAddr,
|
connect_addr: SocketAddr,
|
||||||
) {
|
) {
|
||||||
// Connect to resolved destination (may differ from key.dst_ip if policy rewrote it)
|
// Connect to resolved destination (may differ from key.dst_ip if policy rewrote it)
|
||||||
@@ -768,11 +804,21 @@ 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
|
// Send PROXY protocol v2 header with configured client source identity.
|
||||||
if proxy_protocol {
|
if proxy_protocol {
|
||||||
let src = SocketAddr::new(key.src_ip.into(), key.src_port);
|
let (src, metadata) = build_proxy_protocol_identity(
|
||||||
|
&state,
|
||||||
|
key.src_ip,
|
||||||
|
key.src_port,
|
||||||
|
proxy_protocol_source,
|
||||||
|
proxy_protocol_vpn_metadata,
|
||||||
|
).await;
|
||||||
let dst = SocketAddr::new(key.dst_ip.into(), key.dst_port);
|
let dst = SocketAddr::new(key.dst_ip.into(), key.dst_port);
|
||||||
let pp_header = crate::proxy_protocol::build_pp_v2_header(src, dst);
|
let pp_header = crate::proxy_protocol::build_pp_v2_header_with_vpn_metadata(
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
metadata.as_ref(),
|
||||||
|
);
|
||||||
if let Err(e) = writer.write_all(&pp_header).await {
|
if let Err(e) = writer.write_all(&pp_header).await {
|
||||||
debug!("NAT: failed to send PP v2 header to {}: {}", connect_addr, e);
|
debug!("NAT: failed to send PP v2 header to {}: {}", connect_addr, e);
|
||||||
let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await;
|
let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await;
|
||||||
@@ -818,6 +864,51 @@ async fn tcp_bridge_task(
|
|||||||
read_task.abort();
|
read_task.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn build_proxy_protocol_identity(
|
||||||
|
state: &Arc<ServerState>,
|
||||||
|
tunnel_ip: Ipv4Addr,
|
||||||
|
tunnel_port: u16,
|
||||||
|
proxy_protocol_source: ProxyProtocolSource,
|
||||||
|
include_metadata: bool,
|
||||||
|
) -> (SocketAddr, Option<crate::proxy_protocol::VpnProxyMetadata>) {
|
||||||
|
let tunnel_addr = SocketAddr::new(tunnel_ip.into(), tunnel_port);
|
||||||
|
|
||||||
|
let client_id = state
|
||||||
|
.client_registry
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get_by_assigned_ip(&tunnel_ip.to_string())
|
||||||
|
.map(|entry| entry.client_id.clone());
|
||||||
|
|
||||||
|
let client_info = if let Some(ref client_id) = client_id {
|
||||||
|
state.clients.read().await.get(client_id).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let remote_addr = client_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|info| info.remote_addr.as_ref())
|
||||||
|
.and_then(|addr| addr.parse::<SocketAddr>().ok());
|
||||||
|
let source_addr = match proxy_protocol_source {
|
||||||
|
ProxyProtocolSource::RemoteIp => remote_addr.unwrap_or(tunnel_addr),
|
||||||
|
ProxyProtocolSource::TunnelIp => tunnel_addr,
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadata = if include_metadata {
|
||||||
|
client_info.map(|info| crate::proxy_protocol::VpnProxyMetadata {
|
||||||
|
client_id: info.registered_client_id,
|
||||||
|
assigned_ip: info.assigned_ip,
|
||||||
|
transport_type: info.transport_type,
|
||||||
|
remote_addr: info.remote_addr,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
(source_addr, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
async fn udp_bridge_task(
|
async fn udp_bridge_task(
|
||||||
key: SessionKey,
|
key: SessionKey,
|
||||||
mut data_rx: mpsc::Receiver<Vec<u8>>,
|
mut data_rx: mpsc::Receiver<Vec<u8>>,
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartvpn',
|
name: '@push.rocks/smartvpn',
|
||||||
version: '1.19.4',
|
version: '1.20.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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,10 +122,15 @@ export interface IVpnServerConfig {
|
|||||||
* 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
|
/** When true and forwardingMode is 'socket', the userspace NAT engine prepends
|
||||||
* PROXY protocol v2 headers on outbound TCP connections, conveying the VPN client's
|
* PROXY protocol v2 headers on outbound TCP connections. */
|
||||||
* 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;
|
socketForwardProxyProtocol?: boolean;
|
||||||
|
/** Source address to place into outbound PROXY v2 headers.
|
||||||
|
* 'tunnelIp' preserves legacy behavior. 'remoteIp' exposes the VPN client's
|
||||||
|
* real connecting IP when known, with tunnel IP fallback. */
|
||||||
|
socketForwardProxyProtocolSource?: 'tunnelIp' | 'remoteIp';
|
||||||
|
/** When true, outbound PROXY v2 headers include authenticated SmartVPN metadata
|
||||||
|
* in a vendor TLV: clientId, assignedIp, transportType, and remoteAddr. */
|
||||||
|
socketForwardProxyProtocolVpnMetadata?: boolean;
|
||||||
/** Destination routing policy for VPN client traffic (socket mode).
|
/** Destination routing policy for VPN client traffic (socket mode).
|
||||||
* Controls where decrypted traffic goes: allow through, block, or redirect to a target.
|
* Controls where decrypted traffic goes: allow through, block, or redirect to a target.
|
||||||
* Default: all traffic passes through (backward compatible). */
|
* Default: all traffic passes through (backward compatible). */
|
||||||
|
|||||||
Reference in New Issue
Block a user