@push.rocks/smartvpn 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartvpn',
6
- version: '1.8.0',
6
+ version: '1.9.0',
7
7
  description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlFQUF5RTtDQUN2RixDQUFBIn0=
@@ -85,6 +85,13 @@ export interface IVpnServerConfig {
85
85
  wgPeers?: IWgPeerConfig[];
86
86
  /** Pre-registered clients for Noise IK authentication */
87
87
  clients?: IClientEntry[];
88
+ /** Enable PROXY protocol v2 on incoming WebSocket connections.
89
+ * Required when behind a reverse proxy that sends PP v2 headers (HAProxy, SmartProxy).
90
+ * SECURITY: Must be false when accepting direct client connections. */
91
+ proxyProtocol?: boolean;
92
+ /** Server-level IP block list — applied at TCP accept, before Noise handshake.
93
+ * Supports exact IPs, CIDR, wildcards, ranges. */
94
+ connectionIpBlockList?: string[];
88
95
  }
89
96
  export interface IVpnServerOptions {
90
97
  transport: TVpnTransportOptions;
@@ -124,6 +131,8 @@ export interface IVpnClientInfo {
124
131
  authenticatedKey: string;
125
132
  /** Registered client ID from the client registry */
126
133
  registeredClientId: string;
134
+ /** Real client IP:port (from PROXY protocol or direct TCP connection) */
135
+ remoteAddr?: string;
127
136
  }
128
137
  export interface IVpnServerStatistics extends IVpnStatistics {
129
138
  activeClients: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartvpn",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "private": false,
5
5
  "description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
6
6
  "type": "module",
package/readme.md CHANGED
@@ -5,6 +5,7 @@ A high-performance VPN solution with a **TypeScript control plane** and a **Rust
5
5
  🔐 **Noise IK** mutual authentication — per-client X25519 keypairs, server-side registry
6
6
  🚀 **Triple transport**: WebSocket (Cloudflare-friendly), raw **QUIC** (datagrams), and **WireGuard** (standard protocol)
7
7
  🛡️ **ACL engine** — deny-overrides-allow IP filtering, aligned with SmartProxy conventions
8
+ 🔀 **PROXY protocol v2** — real client IPs behind reverse proxies (HAProxy, SmartProxy, Cloudflare Spectrum)
8
9
  📊 **Adaptive QoS**: per-client rate limiting, priority queues, connection quality tracking
9
10
  🔄 **Hub API**: one `createClient()` call generates keys, assigns IP, returns both SmartVPN + WireGuard configs
10
11
  📡 **Real-time telemetry**: RTT, jitter, loss ratio, link health — all via typed APIs
@@ -125,6 +126,32 @@ security: {
125
126
 
126
127
  Supports exact IPs, CIDR, wildcards (`192.168.1.*`), and ranges (`1.1.1.1-1.1.1.100`).
127
128
 
129
+ ### 🔀 PROXY Protocol v2
130
+
131
+ When the VPN server sits behind a reverse proxy, enable PROXY protocol v2 to receive the **real client IP** instead of the proxy's address. This makes `ipAllowList` / `ipBlockList` ACLs work correctly through load balancers.
132
+
133
+ ```typescript
134
+ await server.start({
135
+ // ... other config ...
136
+ proxyProtocol: true, // parse PP v2 headers on WS connections
137
+ connectionIpBlockList: ['198.51.100.0/24'], // server-wide block list (pre-handshake)
138
+ });
139
+ ```
140
+
141
+ **Two-phase ACL with real IPs:**
142
+
143
+ | Phase | When | What Happens |
144
+ |-------|------|-------------|
145
+ | **Pre-handshake** | After TCP accept | Server-level `connectionIpBlockList` rejects known-bad IPs — zero crypto cost |
146
+ | **Post-handshake** | After Noise IK identifies client | Per-client `ipAllowList` / `ipBlockList` checked against real source IP |
147
+
148
+ - Parses the PP v2 binary header from raw TCP before WebSocket upgrade
149
+ - 5-second timeout protects against stalling attacks
150
+ - LOCAL command (proxy health checks) handled gracefully
151
+ - IPv4 and IPv6 addresses supported
152
+ - `remoteAddr` field on `IVpnClientInfo` exposes the real client IP for monitoring
153
+ - **Security**: must be `false` (default) when accepting direct connections — only enable behind a trusted proxy
154
+
128
155
  ### 📊 Telemetry & QoS
129
156
 
130
157
  - **Connection quality**: Smoothed RTT, jitter, min/max RTT, loss ratio, link health (`healthy` / `degraded` / `critical`)
@@ -217,13 +244,13 @@ const unit = VpnInstaller.generateServiceUnit({
217
244
 
218
245
  | Interface | Purpose |
219
246
  |-----------|---------|
220
- | `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, clients) |
247
+ | `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, clients, proxy protocol) |
221
248
  | `IVpnClientConfig` | Client configuration (server URL, keys, transport, WG options) |
222
249
  | `IClientEntry` | Server-side client definition (ID, keys, security, priority, tags, expiry) |
223
250
  | `IClientSecurity` | Per-client ACLs and rate limits (SmartProxy-aligned naming) |
224
251
  | `IClientRateLimit` | Rate limiting config (bytesPerSec, burstBytes) |
225
252
  | `IClientConfigBundle` | Full config bundle returned by `createClient()` |
226
- | `IVpnClientInfo` | Connected client info (IP, stats, authenticated key) |
253
+ | `IVpnClientInfo` | Connected client info (IP, stats, authenticated key, remote addr) |
227
254
  | `IVpnConnectionQuality` | RTT, jitter, loss ratio, link health |
228
255
  | `IVpnKeypair` | Base64-encoded public/private key pair |
229
256
 
@@ -314,7 +341,7 @@ pnpm install
314
341
  # Build (TypeScript + Rust cross-compile)
315
342
  pnpm build
316
343
 
317
- # Run all tests (79 TS + 121 Rust = 200 tests)
344
+ # Run all tests (79 TS + 129 Rust = 208 tests)
318
345
  pnpm test
319
346
 
320
347
  # Run Rust tests directly
@@ -345,6 +372,7 @@ smartvpn/
345
372
  │ ├── crypto.rs # Noise IK + XChaCha20
346
373
  │ ├── client_registry.rs # Client database
347
374
  │ ├── acl.rs # ACL engine
375
+ │ ├── proxy_protocol.rs # PROXY protocol v2 parser
348
376
  │ ├── management.rs # JSON-lines IPC
349
377
  │ ├── transport.rs # WebSocket transport
350
378
  │ ├── quic_transport.rs # QUIC transport
package/readme.plan.md CHANGED
@@ -1,459 +1,253 @@
1
- # Enterprise Auth & Client Management for SmartVPN
1
+ # PROXY Protocol v2 Support for SmartVPN WebSocket Transport
2
2
 
3
3
  ## Context
4
4
 
5
- SmartVPN's Noise NK mode currently allows **any client that knows the server's public key** to connect no per-client identity or access control. The goal is to make SmartVPN enterprise-ready with:
5
+ SmartVPN's WebSocket transport is designed to sit behind reverse proxies (Cloudflare, HAProxy, SmartProxy). The recently added ACL engine has `ipAllowList`/`ipBlockList` per client, but without PROXY protocol support the server only sees the proxy's IP — not the real client's. This makes source-IP ACLs useless behind a proxy.
6
6
 
7
- 1. **Per-client cryptographic authentication** (Noise IK handshake)
8
- 2. **Rich client definitions** with ACLs, rate limits, and priority
9
- 3. **Hub-generated configs** — server generates typed SmartVPN client configs AND WireGuard .conf files from the same client definition
10
- 4. **Top-notch DX** — one `createClient()` call gives you everything
11
-
12
- **This is a breaking change.** No backward compatibility with the old NK anonymous mode.
7
+ PROXY protocol v2 solves this by letting the proxy prepend a binary header with the real client IP/port before the WebSocket upgrade.
13
8
 
14
9
  ---
15
10
 
16
- ## Design Overview
17
-
18
- ### The Hub Model
11
+ ## Design
19
12
 
20
- The server acts as a **hub** that manages client definitions. Each client definition is the **single source of truth** from which both SmartVPN native configs and WireGuard configs are generated.
13
+ ### Two-Phase ACL with Real Client IP
21
14
 
22
15
  ```
23
- Hub (Server)
24
- └── Client Registry
25
- ├── "alice-laptop" SmartVPN config OR WireGuard .conf
26
- ├── "bob-phone" → SmartVPN config OR WireGuard .conf
27
- └── "office-gw" SmartVPN config OR WireGuard .conf
16
+ TCP accept → Read PP v2 header → Extract real IP
17
+
18
+ ├─ Phase 1 (pre-handshake): Check server-level connectionIpBlockList reject early
19
+
20
+ ├─ WebSocket upgrade Noise IK handshake → Client identity known
21
+
22
+ └─ Phase 2 (post-handshake): Check per-client ipAllowList/ipBlockList → reject if denied
28
23
  ```
29
24
 
30
- ### Authentication: NK IK (Breaking Change)
31
-
32
- **Old (removed):** `Noise_NK_25519_ChaChaPoly_BLAKE2s` — client is anonymous
33
- **New (always):** `Noise_IK_25519_ChaChaPoly_BLAKE2s` — client presents its static key during handshake
34
-
35
- IK is a 2-message handshake (same count as NK), so **the frame protocol stays identical**. Changes:
36
- - `create_initiator()` now requires `(client_private_key, server_public_key)` — always
37
- - `create_responder()` remains `(server_private_key)` — but now uses IK pattern
38
- - After handshake, server extracts client's public key via `get_remote_static()` and verifies against registry
39
- - Old NK functions are replaced, not kept alongside
40
-
41
- **Every client must have a keypair. Every server must have a client registry.**
42
-
43
- ---
44
-
45
- ## Core Interface: `IClientEntry`
46
-
47
- This is the server-side client definition — the central config object.
48
- Naming and structure are aligned with SmartProxy's `IRouteConfig` / `IRouteSecurity` patterns.
49
-
50
- ```typescript
51
- export interface IClientEntry {
52
- /** Human-readable client ID (e.g. "alice-laptop") */
53
- clientId: string;
54
-
55
- /** Client's Noise IK public key (base64) — for SmartVPN native transport */
56
- publicKey: string;
57
- /** Client's WireGuard public key (base64) — for WireGuard transport */
58
- wgPublicKey?: string;
25
+ - **Phase 1**: Server-wide block list (`connectionIpBlockList` on `IVpnServerConfig`). Rejects before any crypto work. Protects server resources.
26
+ - **Phase 2**: Per-client ACL from `IClientSecurity.ipAllowList`/`ipBlockList`. Applied after the Noise IK handshake identifies the client.
59
27
 
60
- // ── Security (aligned with SmartProxy IRouteSecurity pattern) ─────────
28
+ ### No New Dependencies
61
29
 
62
- security?: IClientSecurity;
30
+ PROXY protocol v2 is a fixed-format binary header (16-byte signature + variable address block). Manual parsing (~80 lines) follows the same pattern as `codec.rs`. No crate needed.
63
31
 
64
- // ── QoS ────────────────────────────────────────────────────────────────
32
+ ### Scope: WebSocket Only
65
33
 
66
- /** Traffic priority (lower = higher priority, default: 100) */
67
- priority?: number;
34
+ - **WebSocket**: Needs PP v2 (sits behind reverse proxies)
35
+ - **QUIC**: Direct UDP, just use `conn.remote_address()`
36
+ - **WireGuard**: Direct UDP, uses boringtun peer tracking
68
37
 
69
- // ── Metadata (aligned with SmartProxy IRouteConfig pattern) ────────────
70
-
71
- /** Whether this client is enabled (default: true) */
72
- enabled?: boolean;
73
- /** Tags for grouping (e.g. ["engineering", "office"]) */
74
- tags?: string[];
75
- /** Optional description */
76
- description?: string;
77
- /** Optional expiry (ISO 8601 timestamp, omit = never expires) */
78
- expiresAt?: string;
79
- }
80
-
81
- /**
82
- * Security settings per client — mirrors SmartProxy's IRouteSecurity structure.
83
- * Uses the same ipAllowList/ipBlockList naming convention.
84
- * Adds VPN-specific destination filtering (destinationAllowList/destinationBlockList).
85
- */
86
- export interface IClientSecurity {
87
- /** Source IPs/CIDRs the client may connect FROM (empty = any).
88
- * Supports: exact IP, CIDR, wildcard (192.168.1.*), ranges (1.1.1.1-1.1.1.5).
89
- * Same format as SmartProxy's ipAllowList. */
90
- ipAllowList?: string[];
91
- /** Source IPs blocked — overrides ipAllowList (deny wins).
92
- * Same format as SmartProxy's ipBlockList. */
93
- ipBlockList?: string[];
94
- /** Destination IPs/CIDRs the client may reach through the VPN (empty = all) */
95
- destinationAllowList?: string[];
96
- /** Destination IPs blocked — overrides destinationAllowList (deny wins) */
97
- destinationBlockList?: string[];
98
- /** Max concurrent connections from this client */
99
- maxConnections?: number;
100
- /** Per-client rate limiting */
101
- rateLimit?: IClientRateLimit;
102
- }
103
-
104
- export interface IClientRateLimit {
105
- /** Max throughput in bytes/sec */
106
- bytesPerSec: number;
107
- /** Burst allowance in bytes */
108
- burstBytes: number;
109
- }
110
- ```
38
+ ---
111
39
 
112
- ### SmartProxy Alignment Notes
40
+ ## Implementation
113
41
 
114
- | Pattern | SmartProxy | SmartVPN |
115
- |---------|-----------|---------|
116
- | ACL naming | `ipAllowList` / `ipBlockList` | Same — `ipAllowList` / `ipBlockList` |
117
- | Security grouping | `security: IRouteSecurity` sub-object | Same — `security: IClientSecurity` sub-object |
118
- | Rate limit structure | `rateLimit: IRouteRateLimit` object | Same pattern — `rateLimit: IClientRateLimit` object |
119
- | IP format support | Exact, CIDR, wildcard, ranges | Same formats |
120
- | Metadata fields | `priority`, `tags`, `enabled`, `description` | Same fields |
121
- | ACL evaluation | Block-first, then allow-list | Same — deny overrides allow |
42
+ ### Phase 1: New Rust module `proxy_protocol.rs`
122
43
 
123
- ### ACL Evaluation Order
44
+ **New file: `rust/src/proxy_protocol.rs`**
124
45
 
46
+ PP v2 binary format:
125
47
  ```
126
- 1. Check ipBlockList / destinationBlockList first (explicit deny wins)
127
- 2. If denied, DROP
128
- 3. Check ipAllowList / destinationAllowList (explicit allow)
129
- 4. If ipAllowList is empty → allow any source
130
- 5. If destinationAllowList is empty allow all destinations
48
+ Bytes 0-11: Signature \x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A
49
+ Byte 12: Version (high nibble = 0x2) | Command (low nibble: 0x0=LOCAL, 0x1=PROXY)
50
+ Byte 13: Address family | Protocol (0x11 = IPv4/TCP, 0x21 = IPv6/TCP)
51
+ Bytes 14-15: Address data length (big-endian u16)
52
+ Bytes 16+: IPv4: 4 src_ip + 4 dst_ip + 2 src_port + 2 dst_port (12 bytes)
53
+ IPv6: 16 src_ip + 16 dst_ip + 2 src_port + 2 dst_port (36 bytes)
131
54
  ```
132
55
 
133
- ---
134
-
135
- ## Hub Config Generation
56
+ ```rust
57
+ pub struct ProxyHeader {
58
+ pub src_addr: SocketAddr,
59
+ pub dst_addr: SocketAddr,
60
+ pub is_local: bool, // LOCAL command = health check probe
61
+ }
136
62
 
137
- ### `createClient()` The One-Call DX
63
+ /// Read and parse a PROXY protocol v2 header from a TCP stream.
64
+ /// Reads exactly the header bytes — the stream is clean for WS upgrade after.
65
+ pub async fn read_proxy_header(stream: &mut TcpStream) -> Result<ProxyHeader>
66
+ ```
138
67
 
139
- When the hub creates a client, it:
140
- 1. Generates a Noise IK keypair for the client
141
- 2. Generates a WireGuard keypair for the client
142
- 3. Allocates a VPN IP address
143
- 4. Stores the `IClientEntry` in the registry
144
- 5. Returns a **complete config bundle** with everything the client needs
68
+ - 5-second timeout on header read (constant `PROXY_HEADER_TIMEOUT`)
69
+ - Validates 12-byte signature, version nibble, command type
70
+ - Parses IPv4 and IPv6 address blocks
71
+ - LOCAL command returns `is_local: true` (caller closes connection gracefully)
72
+ - Unit tests: valid IPv4/IPv6 headers, LOCAL command, invalid signature, truncated data
145
73
 
146
- ```typescript
147
- export interface IClientConfigBundle {
148
- /** The server-side client entry */
149
- entry: IClientEntry;
150
- /** Ready-to-use SmartVPN client config (typed object) */
151
- smartvpnConfig: IVpnClientConfig;
152
- /** Ready-to-use WireGuard .conf file content (string) */
153
- wireguardConfig: string;
154
- /** Client's private keys (ONLY returned at creation time, not stored server-side) */
155
- secrets: {
156
- noisePrivateKey: string;
157
- wgPrivateKey: string;
158
- };
159
- }
160
- ```
74
+ **Modify: `rust/src/lib.rs`** — add `pub mod proxy_protocol;`
161
75
 
162
- The `secrets` are returned **only at creation time** — the server stores only public keys.
76
+ ### Phase 2: Server config + client info fields
163
77
 
164
- ### `exportClientConfig()` — Re-export (without secrets)
78
+ **File: `rust/src/server.rs` — `ServerConfig`**
165
79
 
166
- ```typescript
167
- exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): IVpnClientConfig | string
80
+ Add:
81
+ ```rust
82
+ /// Enable PROXY protocol v2 parsing on WebSocket connections.
83
+ /// SECURITY: Must be false when accepting direct client connections.
84
+ pub proxy_protocol: Option<bool>,
85
+ /// Server-level IP block list — applied at TCP accept time, before Noise handshake.
86
+ pub connection_ip_block_list: Option<Vec<String>>,
168
87
  ```
169
88
 
170
- ---
171
-
172
- ## Updated `IVpnServerConfig`
89
+ **File: `rust/src/server.rs` — `ClientInfo`**
173
90
 
174
- ```typescript
175
- export interface IVpnServerConfig {
176
- listenAddr: string;
177
- tlsCert?: string;
178
- tlsKey?: string;
179
- privateKey: string; // Server's Noise static private key (base64)
180
- publicKey: string; // Server's Noise static public key (base64)
181
- subnet: string;
182
- dns?: string[];
183
- mtu?: number;
184
- keepaliveIntervalSecs?: number;
185
- enableNat?: boolean;
186
- defaultRateLimitBytesPerSec?: number;
187
- defaultBurstBytes?: number;
188
- transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard';
189
- quicListenAddr?: string;
190
- quicIdleTimeoutSecs?: number;
191
- wgListenPort?: number;
192
- wgPeers?: IWgPeerConfig[]; // Keep for raw WG mode
193
-
194
- /** Pre-registered clients — REQUIRED for SmartVPN native transport */
195
- clients: IClientEntry[];
196
- }
91
+ Add:
92
+ ```rust
93
+ /// Real client IP:port (from PROXY protocol header or direct TCP connection).
94
+ pub remote_addr: Option<String>,
197
95
  ```
198
96
 
199
- Note: `clients` is now **required** (not optional), and there is no `authMode` field — IK is always used.
200
-
201
- ---
97
+ ### Phase 3: ACL helper
202
98
 
203
- ## Updated `IVpnClientConfig`
99
+ **File: `rust/src/acl.rs`**
204
100
 
205
- ```typescript
206
- export interface IVpnClientConfig {
207
- serverUrl: string;
208
- serverPublicKey: string;
209
- /** Client's Noise IK private key (base64) — REQUIRED for SmartVPN native transport */
210
- clientPrivateKey: string;
211
- /** Client's Noise IK public key (base64) — for reference/display */
212
- clientPublicKey: string;
213
- dns?: string[];
214
- mtu?: number;
215
- keepaliveIntervalSecs?: number;
216
- transport?: 'auto' | 'websocket' | 'quic' | 'wireguard';
217
- serverCertHash?: string;
218
- // WireGuard fields unchanged...
219
- wgPrivateKey?: string;
220
- wgAddress?: string;
221
- wgAddressPrefix?: number;
222
- wgPresharedKey?: string;
223
- wgPersistentKeepalive?: number;
224
- wgEndpoint?: string;
225
- wgAllowedIps?: string[];
101
+ Add a public function for the server-level pre-handshake check:
102
+ ```rust
103
+ /// Check whether a connection source IP is in a block list.
104
+ pub fn is_connection_blocked(ip: Ipv4Addr, block_list: &[String]) -> bool {
105
+ ip_matches_any(ip, block_list)
226
106
  }
227
107
  ```
228
108
 
229
- Note: `clientPrivateKey` and `clientPublicKey` are now **required** (not optional) for non-WireGuard transports.
230
-
231
- ---
232
-
233
- ## New IPC Commands
234
-
235
- Added to `TVpnServerCommands`:
236
-
237
- | Command | Params | Result | Description |
238
- |---------|--------|--------|-------------|
239
- | `createClient` | `{ client: Partial<IClientEntry> }` | `IClientConfigBundle` | Create client, generate keypairs, assign IP, return full config bundle |
240
- | `removeClient` | `{ clientId: string }` | `void` | Remove from registry + disconnect if connected |
241
- | `getClient` | `{ clientId: string }` | `IClientEntry` | Get a single client entry |
242
- | `listRegisteredClients` | `{}` | `{ clients: IClientEntry[] }` | List all registered clients |
243
- | `updateClient` | `{ clientId: string, update: Partial<IClientEntry> }` | `void` | Update ACLs, rate limits, tags, etc. |
244
- | `enableClient` | `{ clientId: string }` | `void` | Enable a disabled client |
245
- | `disableClient` | `{ clientId: string }` | `void` | Disable (but don't delete) |
246
- | `rotateClientKey` | `{ clientId: string }` | `IClientConfigBundle` | New keypairs, return fresh config bundle |
247
- | `exportClientConfig` | `{ clientId: string, format: 'smartvpn' \| 'wireguard' }` | `{ config: string }` | Re-export config (without secrets) |
248
- | `generateClientKeypair` | `{}` | `IVpnKeypair` | Generate a standalone Noise IK keypair |
249
-
250
- ---
251
-
252
- ## Implementation Plan
253
-
254
- ### Phase 1: Rust — Crypto (Replace NK with IK)
109
+ (Keeps `ip_matches_any` private; exposes only the specific check needed.)
255
110
 
256
- **File: `rust/src/crypto.rs`**
111
+ ### Phase 4: WebSocket listener integration
257
112
 
258
- - Change `NOISE_PATTERN` from NK to IK: `"Noise_IK_25519_ChaChaPoly_BLAKE2s"`
259
- - Replace `create_initiator(server_public_key)` → `create_initiator(client_private_key, server_public_key)`
260
- - `create_responder(private_key)` stays the same signature (IK responder only needs its own key)
261
- - After handshake, `get_remote_static()` on the responder returns the client's public key
262
- - Update `perform_handshake()` to pass client keypair
263
- - Update all tests
113
+ **File: `rust/src/server.rs` `run_ws_listener()`**
264
114
 
265
- ### Phase 2: Rust — Client Registry module
266
-
267
- **New file: `rust/src/client_registry.rs`**
268
- **Modify: `rust/src/lib.rs`** — add `pub mod client_registry;`
115
+ Between `listener.accept()` and `transport::accept_connection()`:
269
116
 
270
117
  ```rust
271
- pub struct ClientEntry {
272
- pub client_id: String,
273
- pub public_key: String,
274
- pub wg_public_key: Option<String>,
275
- pub security: Option<ClientSecurity>,
276
- pub priority: Option<u32>,
277
- pub enabled: Option<bool>,
278
- pub tags: Option<Vec<String>>,
279
- pub description: Option<String>,
280
- pub expires_at: Option<String>,
281
- pub assigned_ip: Option<String>,
282
- }
283
-
284
- /// Mirrors IClientSecurity — aligned with SmartProxy's IRouteSecurity
285
- pub struct ClientSecurity {
286
- pub ip_allow_list: Option<Vec<String>>,
287
- pub ip_block_list: Option<Vec<String>>,
288
- pub destination_allow_list: Option<Vec<String>>,
289
- pub destination_block_list: Option<Vec<String>>,
290
- pub max_connections: Option<u32>,
291
- pub rate_limit: Option<ClientRateLimit>,
118
+ // Determine real client address
119
+ let remote_addr = if state.config.proxy_protocol.unwrap_or(false) {
120
+ match proxy_protocol::read_proxy_header(&mut tcp_stream).await {
121
+ Ok(header) if header.is_local => {
122
+ // Health check probe — close gracefully
123
+ return;
124
+ }
125
+ Ok(header) => {
126
+ info!("PP v2: real client {} -> {}", header.src_addr, header.dst_addr);
127
+ Some(header.src_addr)
128
+ }
129
+ Err(e) => {
130
+ warn!("PP v2 parse failed from {}: {}", tcp_addr, e);
131
+ return; // Drop connection
132
+ }
133
+ }
134
+ } else {
135
+ Some(tcp_addr) // Direct connection — use TCP SocketAddr
136
+ };
137
+
138
+ // Pre-handshake server-level block list check
139
+ if let (Some(ref block_list), Some(ref addr)) = (&state.config.connection_ip_block_list, &remote_addr) {
140
+ if let std::net::IpAddr::V4(v4) = addr.ip() {
141
+ if acl::is_connection_blocked(v4, block_list) {
142
+ warn!("Connection blocked by server IP block list: {}", addr);
143
+ return;
144
+ }
145
+ }
292
146
  }
293
147
 
294
- pub struct ClientRateLimit {
295
- pub bytes_per_sec: u64,
296
- pub burst_bytes: u64,
297
- }
298
-
299
- pub struct ClientRegistry {
300
- entries: HashMap<String, ClientEntry>, // keyed by clientId
301
- key_index: HashMap<String, String>, // publicKey → clientId (fast lookup)
302
- }
148
+ // Then proceed with WS upgrade + handle_client_connection as before
303
149
  ```
304
150
 
305
- Methods: `add`, `remove`, `get_by_id`, `get_by_key`, `update`, `list`, `is_authorized` (enabled + not expired + key exists), `rotate_key`.
151
+ Key correctness note: `read_proxy_header` reads *exactly* the PP header bytes via `read_exact`. The `TcpStream` is then in a clean state for the WS HTTP upgrade. No buffered wrapper needed.
306
152
 
307
- ### Phase 3: Rust ACL enforcement module
153
+ ### Phase 5: Update `handle_client_connection` signature
308
154
 
309
- **New file: `rust/src/acl.rs`**
310
- **Modify: `rust/src/lib.rs`** — add `pub mod acl;`
155
+ **File: `rust/src/server.rs`**
311
156
 
157
+ Change signature:
312
158
  ```rust
313
- /// IP matching supports: exact, CIDR, wildcard, ranges — same as SmartProxy's IpMatcher
314
- pub fn check_acl(security: &ClientSecurity, src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> AclResult {
315
- // 1. Check ip_block_list / destination_block_list (deny overrides)
316
- // 2. Check ip_allow_list / destination_allow_list (explicit allow)
317
- // 3. Empty list = allow all
318
- }
159
+ async fn handle_client_connection(
160
+ state: Arc<ServerState>,
161
+ mut sink: Box<dyn TransportSink>,
162
+ mut stream: Box<dyn TransportStream>,
163
+ remote_addr: Option<std::net::SocketAddr>, // NEW
164
+ ) -> Result<()>
319
165
  ```
320
166
 
321
- Called in `server.rs` packet loop after decryption, before forwarding.
322
-
323
- ### Phase 4: Rust — Server changes
324
-
325
- **File: `rust/src/server.rs`**
326
-
327
- - Add `clients: Option<Vec<ClientEntry>>` to `ServerConfig`
328
- - Add `client_registry: RwLock<ClientRegistry>` to `ServerState` (no `auth_mode` — always IK)
329
- - Modify `handle_client_connection()`:
330
- - Always use `create_responder()` (now IK pattern)
331
- - Call `get_remote_static()` **before** `into_transport_mode()` to get client's public key
332
- - Verify against registry — reject unauthorized clients with Disconnect frame
333
- - Use registry entry for rate limits (overrides server defaults)
334
- - In packet loop: call `acl::check_acl()` on decrypted packets
335
- - Add `ClientInfo.authenticated_key: String` and `ClientInfo.registered_client_id: String` (no longer optional)
336
- - Add methods: `create_client()`, `remove_client()`, `update_client()`, `list_registered_clients()`, `rotate_client_key()`, `export_client_config()`
167
+ After Noise IK handshake + registry lookup (where `client_security` is available), add connection-level per-client ACL:
337
168
 
338
- ### Phase 5: Rust — Client changes
339
-
340
- **File: `rust/src/client.rs`**
169
+ ```rust
170
+ if let (Some(ref sec), Some(addr)) = (&client_security, &remote_addr) {
171
+ if let std::net::IpAddr::V4(v4) = addr.ip() {
172
+ if acl::is_connection_blocked(v4, sec.ip_block_list.as_deref().unwrap_or(&[])) {
173
+ anyhow::bail!("Client {} connection denied: source IP {} blocked", registered_client_id, addr);
174
+ }
175
+ if let Some(ref allow) = sec.ip_allow_list {
176
+ if !allow.is_empty() && !acl::is_ip_allowed(v4, allow) {
177
+ anyhow::bail!("Client {} connection denied: source IP {} not in allow list", registered_client_id, addr);
178
+ }
179
+ }
180
+ }
181
+ }
182
+ ```
341
183
 
342
- - Add `client_private_key: String` to `ClientConfig` (required, not optional)
343
- - `connect()` always uses `create_initiator(client_private_key, server_public_key)` (IK)
184
+ Populate `remote_addr` when building `ClientInfo`:
185
+ ```rust
186
+ remote_addr: remote_addr.map(|a| a.to_string()),
187
+ ```
344
188
 
345
- ### Phase 6: RustManagement IPC handlers
189
+ ### Phase 6: QUIC listener pass remote addr through
346
190
 
347
- **File: `rust/src/management.rs`**
191
+ **File: `rust/src/server.rs` — `run_quic_listener()`**
348
192
 
349
- Add handlers for all 10 new IPC commands following existing patterns.
193
+ QUIC doesn't use PROXY protocol. Just pass `conn.remote_address()` through:
194
+ ```rust
195
+ let remote = conn.remote_address();
196
+ // ...
197
+ handle_client_connection(state, Box::new(sink), Box::new(stream), Some(remote)).await
198
+ ```
350
199
 
351
- ### Phase 7: TypeScript Interfaces
200
+ ### Phase 7: TypeScript interface updates
352
201
 
353
202
  **File: `ts/smartvpn.interfaces.ts`**
354
203
 
355
- - Add `IClientEntry` interface
356
- - Add `IClientConfigBundle` interface
357
- - Update `IVpnServerConfig`: add required `clients: IClientEntry[]`
358
- - Update `IVpnClientConfig`: add required `clientPrivateKey: string`, `clientPublicKey: string`
359
- - Update `IVpnClientInfo`: add `authenticatedKey: string`, `registeredClientId: string`
360
- - Add new commands to `TVpnServerCommands`
361
-
362
- ### Phase 8: TypeScript — VpnServer class methods
363
-
364
- **File: `ts/smartvpn.classes.vpnserver.ts`**
365
-
366
- Add methods:
367
- - `createClient(opts)` → `IClientConfigBundle`
368
- - `removeClient(clientId)` → `void`
369
- - `getClient(clientId)` → `IClientEntry`
370
- - `listRegisteredClients()` → `IClientEntry[]`
371
- - `updateClient(clientId, update)` → `void`
372
- - `enableClient(clientId)` / `disableClient(clientId)`
373
- - `rotateClientKey(clientId)` → `IClientConfigBundle`
374
- - `exportClientConfig(clientId, format)` → `string | IVpnClientConfig`
375
-
376
- ### Phase 9: TypeScript — Config validation
377
-
378
- **File: `ts/smartvpn.classes.vpnconfig.ts`**
379
-
380
- - Server config: validate `clients` present, each entry has valid `clientId` + `publicKey`
381
- - Client config: validate `clientPrivateKey` and `clientPublicKey` present for non-WG transports
382
- - Validate CIDRs in ACL fields
383
-
384
- ### Phase 10: TypeScript — Hub config generation
385
-
386
- **File: `ts/smartvpn.classes.wgconfig.ts`** (extend existing)
387
-
388
- Add `generateClientConfigFromEntry(entry, serverConfig)` — produces WireGuard .conf from `IClientEntry`.
389
-
390
- ### Phase 11: Update existing tests
391
-
392
- All existing tests that use the old NK handshake or old config shapes need updating:
393
- - Rust tests in `crypto.rs`, `server.rs`, `client.rs`
394
- - TS tests in `test/test.vpnconfig.node.ts`, `test/test.flowcontrol.node.ts`, etc.
395
- - Tests now must provide client keypairs and client registry entries
396
-
397
- ---
398
-
399
- ## DX Highlights
400
-
401
- 1. **One call to create a client:**
402
- ```typescript
403
- const bundle = await server.createClient({ clientId: 'alice-laptop', tags: ['engineering'] });
404
- // bundle.smartvpnConfig — typed SmartVPN client config
405
- // bundle.wireguardConfig — standard WireGuard .conf string
406
- // bundle.secrets — private keys, shown only at creation time
407
- ```
204
+ Add to `IVpnServerConfig`:
205
+ ```typescript
206
+ /** Enable PROXY protocol v2 on incoming WebSocket connections.
207
+ * Required when behind a reverse proxy that sends PP v2 headers. */
208
+ proxyProtocol?: boolean;
209
+ /** Server-level IP block list applied at TCP accept time, before Noise handshake. */
210
+ connectionIpBlockList?: string[];
211
+ ```
408
212
 
409
- 2. **Typed config objects throughout** — no raw strings or JSON blobs
213
+ Add to `IVpnClientInfo`:
214
+ ```typescript
215
+ /** Real client IP:port (from PROXY protocol or direct TCP). */
216
+ remoteAddr?: string;
217
+ ```
410
218
 
411
- 3. **Dual transport from same definition** — register once, connect via SmartVPN or WireGuard
219
+ ### Phase 8: Tests
412
220
 
413
- 4. **ACLs are deny-overrides-allow** intuitive enterprise model
221
+ **Rust unit tests in `proxy_protocol.rs`:**
222
+ - `parse_valid_ipv4_header` — construct a valid PP v2 header with known IPs, verify parsed correctly
223
+ - `parse_valid_ipv6_header` — same for IPv6
224
+ - `parse_local_command` — health check probe returns `is_local: true`
225
+ - `reject_invalid_signature` — random bytes rejected
226
+ - `reject_truncated_header` — short reads fail gracefully
227
+ - `reject_v1_header` — PROXY v1 text format rejected (we only support v2)
414
228
 
415
- 5. **Hot management** add/remove/update/disable clients at runtime
229
+ **Rust unit tests in `acl.rs`:**
230
+ - `is_connection_blocked` with various IP patterns
416
231
 
417
- 6. **Key rotation** — `rotateClientKey()` generates new keys and returns a fresh config bundle
232
+ **TypeScript tests:**
233
+ - Config validation accepts `proxyProtocol: true` + `connectionIpBlockList`
418
234
 
419
235
  ---
420
236
 
421
- ## Verification Plan
422
-
423
- 1. **Rust unit tests:**
424
- - `crypto.rs`: IK handshake roundtrip, `get_remote_static()` returns correct key, wrong key fails
425
- - `client_registry.rs`: CRUD, `is_authorized` with enabled/disabled/expired
426
- - `acl.rs`: allow/deny logic, empty lists, deny-overrides-allow
427
-
428
- 2. **Rust integration tests:**
429
- - Server accepts authorized client
430
- - Server rejects unknown client public key
431
- - ACL filtering drops packets to blocked destinations
432
- - Runtime `createClient` / `removeClient` works
433
- - Disabled client rejected at handshake
434
-
435
- 3. **TypeScript tests:**
436
- - Config validation with required client fields
437
- - `createClient()` returns valid bundle with both formats
438
- - `exportClientConfig()` generates valid WireGuard .conf
439
- - Full IPC roundtrip: create client → connect → traffic → disconnect
237
+ ## Key Files to Modify
440
238
 
441
- 4. **Build:** `pnpm build` (TS + Rust), `cargo test`, `pnpm test`
239
+ | File | Changes |
240
+ |------|---------|
241
+ | `rust/src/proxy_protocol.rs` | **NEW** — PP v2 parser + tests |
242
+ | `rust/src/lib.rs` | Add `pub mod proxy_protocol;` |
243
+ | `rust/src/server.rs` | `ServerConfig` + `ClientInfo` fields, `run_ws_listener` PP integration, `handle_client_connection` signature + connection ACL, `run_quic_listener` pass-through |
244
+ | `rust/src/acl.rs` | Add `is_connection_blocked` public function |
245
+ | `ts/smartvpn.interfaces.ts` | `proxyProtocol`, `connectionIpBlockList`, `remoteAddr` |
442
246
 
443
247
  ---
444
248
 
445
- ## Key Files to Modify
249
+ ## Verification
446
250
 
447
- | File | Changes |
448
- |------|---------|
449
- | `rust/src/crypto.rs` | Replace NK with IK pattern, update initiator signature |
450
- | `rust/src/client_registry.rs` | **NEW** — client registry module |
451
- | `rust/src/acl.rs` | **NEW** — ACL evaluation module |
452
- | `rust/src/server.rs` | Registry integration, IK auth in handshake, ACL in packet loop |
453
- | `rust/src/client.rs` | Required `client_private_key`, IK initiator |
454
- | `rust/src/management.rs` | 10 new IPC command handlers |
455
- | `rust/src/lib.rs` | Register new modules |
456
- | `ts/smartvpn.interfaces.ts` | `IClientEntry`, `IClientConfigBundle`, updated configs & commands |
457
- | `ts/smartvpn.classes.vpnserver.ts` | New hub methods |
458
- | `ts/smartvpn.classes.vpnconfig.ts` | Updated validation rules |
459
- | `ts/smartvpn.classes.wgconfig.ts` | Config generation from client entries |
251
+ 1. `cargo test` all existing 121 tests + new PP parser tests pass
252
+ 2. `pnpm test` — all 79 TS tests pass (no PP in test setup, just config validation)
253
+ 3. Manual: `socat` or test harness to send a PP v2 header before WS upgrade, verify server logs real IP
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartvpn',
6
- version: '1.8.0',
6
+ version: '1.9.0',
7
7
  description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
8
8
  }
@@ -102,6 +102,13 @@ export interface IVpnServerConfig {
102
102
  wgPeers?: IWgPeerConfig[];
103
103
  /** Pre-registered clients for Noise IK authentication */
104
104
  clients?: IClientEntry[];
105
+ /** Enable PROXY protocol v2 on incoming WebSocket connections.
106
+ * Required when behind a reverse proxy that sends PP v2 headers (HAProxy, SmartProxy).
107
+ * SECURITY: Must be false when accepting direct client connections. */
108
+ proxyProtocol?: boolean;
109
+ /** Server-level IP block list — applied at TCP accept, before Noise handshake.
110
+ * Supports exact IPs, CIDR, wildcards, ranges. */
111
+ connectionIpBlockList?: string[];
105
112
  }
106
113
 
107
114
  export interface IVpnServerOptions {
@@ -156,6 +163,8 @@ export interface IVpnClientInfo {
156
163
  authenticatedKey: string;
157
164
  /** Registered client ID from the client registry */
158
165
  registeredClientId: string;
166
+ /** Real client IP:port (from PROXY protocol or direct TCP connection) */
167
+ remoteAddr?: string;
159
168
  }
160
169
 
161
170
  export interface IVpnServerStatistics extends IVpnStatistics {