Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3afb83470 | |||
| 2d7a507cf2 | |||
| a757a4bb73 | |||
| 5bf21ab4ac |
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-30 - 1.13.0 - feat(client-registry)
|
||||
separate trusted server-defined client tags from client-reported tags with legacy tag compatibility
|
||||
|
||||
- Adds distinct serverDefinedClientTags and clientDefinedClientTags fields to client registry and TypeScript interfaces.
|
||||
- Treats legacy tags values as serverDefinedClientTags during deserialization and server-side create/update flows for backward compatibility.
|
||||
- Clarifies that only server-defined tags are trusted for access control while client-defined tags are informational only.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartvpn",
|
||||
"version": "1.11.0",
|
||||
"version": "1.13.0",
|
||||
"private": false,
|
||||
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
|
||||
"type": "module",
|
||||
|
||||
@@ -44,7 +44,12 @@ pub struct ClientEntry {
|
||||
pub priority: Option<u32>,
|
||||
/// Whether this client is enabled (default: true).
|
||||
pub enabled: Option<bool>,
|
||||
/// Tags for grouping.
|
||||
/// Tags assigned by the server admin — trusted, used for access control.
|
||||
pub server_defined_client_tags: Option<Vec<String>>,
|
||||
/// Tags reported by the connecting client — informational only.
|
||||
pub client_defined_client_tags: Option<Vec<String>>,
|
||||
/// Legacy tags field — treated as serverDefinedClientTags during deserialization.
|
||||
#[serde(default)]
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Optional description.
|
||||
pub description: Option<String>,
|
||||
@@ -90,7 +95,11 @@ impl ClientRegistry {
|
||||
/// Build a registry from a list of client entries.
|
||||
pub fn from_entries(entries: Vec<ClientEntry>) -> Result<Self> {
|
||||
let mut registry = Self::new();
|
||||
for entry in entries {
|
||||
for mut entry in entries {
|
||||
// Migrate legacy `tags` → `serverDefinedClientTags`
|
||||
if entry.server_defined_client_tags.is_none() && entry.tags.is_some() {
|
||||
entry.server_defined_client_tags = entry.tags.take();
|
||||
}
|
||||
registry.add(entry)?;
|
||||
}
|
||||
Ok(registry)
|
||||
@@ -193,6 +202,8 @@ mod tests {
|
||||
security: None,
|
||||
priority: None,
|
||||
enabled: None,
|
||||
server_defined_client_tags: None,
|
||||
client_defined_client_tags: None,
|
||||
tags: None,
|
||||
description: None,
|
||||
expires_at: None,
|
||||
|
||||
@@ -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 {
|
||||
@@ -545,9 +551,16 @@ impl VpnServer {
|
||||
).ok(),
|
||||
priority: partial.get("priority").and_then(|v| v.as_u64()).map(|v| v as u32),
|
||||
enabled: partial.get("enabled").and_then(|v| v.as_bool()).or(Some(true)),
|
||||
tags: partial.get("tags").and_then(|v| {
|
||||
server_defined_client_tags: partial.get("serverDefinedClientTags").and_then(|v| {
|
||||
v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())
|
||||
}).or_else(|| {
|
||||
// Legacy: accept "tags" as serverDefinedClientTags
|
||||
partial.get("tags").and_then(|v| {
|
||||
v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())
|
||||
})
|
||||
}),
|
||||
client_defined_client_tags: None, // Only set by connecting client
|
||||
tags: None, // Legacy field — not used for new entries
|
||||
description: partial.get("description").and_then(|v| v.as_str()).map(String::from),
|
||||
expires_at: partial.get("expiresAt").and_then(|v| v.as_str()).map(String::from),
|
||||
assigned_ip: Some(assigned_ip.to_string()),
|
||||
@@ -642,8 +655,11 @@ impl VpnServer {
|
||||
if let Some(enabled) = update.get("enabled").and_then(|v| v.as_bool()) {
|
||||
entry.enabled = Some(enabled);
|
||||
}
|
||||
if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
|
||||
entry.tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
|
||||
if let Some(tags) = update.get("serverDefinedClientTags").and_then(|v| v.as_array()) {
|
||||
entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
|
||||
} else if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
|
||||
// Legacy: accept "tags" as serverDefinedClientTags
|
||||
entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
|
||||
}
|
||||
if let Some(desc) = update.get("description").and_then(|v| v.as_str()) {
|
||||
entry.description = Some(desc.to_string());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartvpn',
|
||||
version: '1.11.0',
|
||||
version: '1.13.0',
|
||||
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ export interface IVpnClientConfig {
|
||||
wgEndpoint?: string;
|
||||
/** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */
|
||||
wgAllowedIps?: string[];
|
||||
/** Client-defined tags reported to the server after connection (informational, not for access control) */
|
||||
clientDefinedClientTags?: string[];
|
||||
}
|
||||
|
||||
export interface IVpnClientOptions {
|
||||
@@ -118,6 +120,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 {
|
||||
@@ -285,7 +292,11 @@ export interface IClientEntry {
|
||||
priority?: number;
|
||||
/** Whether this client is enabled (default: true) */
|
||||
enabled?: boolean;
|
||||
/** Tags for grouping (e.g. ["engineering", "office"]) */
|
||||
/** Tags assigned by the server admin — trusted, used for access control (e.g. ["engineering", "office"]) */
|
||||
serverDefinedClientTags?: string[];
|
||||
/** Tags reported by the connecting client — informational only, never used for access control */
|
||||
clientDefinedClientTags?: string[];
|
||||
/** @deprecated Use serverDefinedClientTags instead. Legacy field kept for backward compatibility. */
|
||||
tags?: string[];
|
||||
/** Optional description */
|
||||
description?: string;
|
||||
|
||||
Reference in New Issue
Block a user