@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.
- package/dist_rust/smartvpn_daemon_linux_amd64 +0 -0
- package/dist_rust/smartvpn_daemon_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartvpn.interfaces.d.ts +9 -0
- package/package.json +1 -1
- package/readme.md +31 -3
- package/readme.plan.md +176 -382
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.interfaces.ts +9 -0
|
Binary file
|
|
Binary file
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartvpn',
|
|
6
|
-
version: '1.
|
|
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
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 +
|
|
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
|
-
#
|
|
1
|
+
# PROXY Protocol v2 Support for SmartVPN WebSocket Transport
|
|
2
2
|
|
|
3
3
|
## Context
|
|
4
4
|
|
|
5
|
-
SmartVPN's
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
### The Hub Model
|
|
11
|
+
## Design
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
### Two-Phase ACL with Real Client IP
|
|
21
14
|
|
|
22
15
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
### No New Dependencies
|
|
61
29
|
|
|
62
|
-
|
|
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
|
-
|
|
32
|
+
### Scope: WebSocket Only
|
|
65
33
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
## Implementation
|
|
113
41
|
|
|
114
|
-
|
|
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
|
-
|
|
44
|
+
**New file: `rust/src/proxy_protocol.rs`**
|
|
124
45
|
|
|
46
|
+
PP v2 binary format:
|
|
125
47
|
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
### Phase 2: Server config + client info fields
|
|
163
77
|
|
|
164
|
-
|
|
78
|
+
**File: `rust/src/server.rs` — `ServerConfig`**
|
|
165
79
|
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
---
|
|
97
|
+
### Phase 3: ACL helper
|
|
202
98
|
|
|
203
|
-
|
|
99
|
+
**File: `rust/src/acl.rs`**
|
|
204
100
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
### Phase 4: WebSocket listener integration
|
|
257
112
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
153
|
+
### Phase 5: Update `handle_client_connection` signature
|
|
308
154
|
|
|
309
|
-
**
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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:
|
|
189
|
+
### Phase 6: QUIC listener — pass remote addr through
|
|
346
190
|
|
|
347
|
-
**File: `rust/src/
|
|
191
|
+
**File: `rust/src/server.rs` — `run_quic_listener()`**
|
|
348
192
|
|
|
349
|
-
|
|
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
|
|
200
|
+
### Phase 7: TypeScript interface updates
|
|
352
201
|
|
|
353
202
|
**File: `ts/smartvpn.interfaces.ts`**
|
|
354
203
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
213
|
+
Add to `IVpnClientInfo`:
|
|
214
|
+
```typescript
|
|
215
|
+
/** Real client IP:port (from PROXY protocol or direct TCP). */
|
|
216
|
+
remoteAddr?: string;
|
|
217
|
+
```
|
|
410
218
|
|
|
411
|
-
|
|
219
|
+
### Phase 8: Tests
|
|
412
220
|
|
|
413
|
-
|
|
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
|
-
|
|
229
|
+
**Rust unit tests in `acl.rs`:**
|
|
230
|
+
- `is_connection_blocked` with various IP patterns
|
|
416
231
|
|
|
417
|
-
|
|
232
|
+
**TypeScript tests:**
|
|
233
|
+
- Config validation accepts `proxyProtocol: true` + `connectionIpBlockList`
|
|
418
234
|
|
|
419
235
|
---
|
|
420
236
|
|
|
421
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
249
|
+
## Verification
|
|
446
250
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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 {
|