@push.rocks/smartvpn 1.22.0 → 2.1.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/assets/third-party-licenses.md +157 -0
- package/dist_rust/{smartvpn_daemon_linux_amd64 → smartvpn_daemon_linux_amd64_musl} +0 -0
- package/dist_rust/smartvpn_daemon_linux_amd64_musl.tsrust-build.json +14 -0
- package/dist_rust/{smartvpn_daemon_linux_arm64 → smartvpn_daemon_linux_arm64_musl} +0 -0
- package/dist_rust/smartvpn_daemon_linux_arm64_musl.tsrust-build.json +14 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/smartvpn.classes.vpnbridge.d.ts +4 -3
- package/dist_ts/smartvpn.classes.vpnbridge.js +27 -47
- package/dist_ts/smartvpn.classes.vpnclient.d.ts +5 -4
- package/dist_ts/smartvpn.classes.vpnclient.js +10 -5
- package/dist_ts/smartvpn.classes.vpnconfig.d.ts +1 -0
- package/dist_ts/smartvpn.classes.vpnconfig.js +52 -14
- package/dist_ts/smartvpn.classes.vpnserver.d.ts +17 -4
- package/dist_ts/smartvpn.classes.vpnserver.js +80 -40
- package/dist_ts/smartvpn.interfaces.d.ts +155 -16
- package/dist_ts/smartvpn.paths.d.ts +2 -0
- package/dist_ts/smartvpn.paths.js +14 -1
- package/notices/aho-corasick-license.txt +21 -0
- package/notices/cargo-dependencies.html +5202 -0
- package/notices/compiler-builtins-license.txt +275 -0
- package/notices/defmt-license.txt +25 -0
- package/notices/inventory.json +1427 -0
- package/notices/llvm-libunwind-license.txt +311 -0
- package/notices/mit-source-attributions.txt +31 -0
- package/notices/musl-copyright.txt +193 -0
- package/notices/proc-macro-error2-license.txt +21 -0
- package/notices/rust-standard-library.html +8266 -0
- package/notices/valuable-license.txt +25 -0
- package/package.json +15 -7
- package/readme.md +344 -5
- package/third-party-notices.md +81 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.classes.vpnbridge.ts +27 -52
- package/ts/smartvpn.classes.vpnclient.ts +10 -5
- package/ts/smartvpn.classes.vpnconfig.ts +49 -13
- package/ts/smartvpn.classes.vpnserver.ts +94 -40
- package/ts/smartvpn.interfaces.ts +159 -16
- package/ts/smartvpn.paths.ts +19 -0
- package/readme.hints.md +0 -8
- package/readme.plan.md +0 -253
|
@@ -21,18 +21,18 @@ export type TVpnTransportOptions = IVpnTransportStdio | IVpnTransportSocket;
|
|
|
21
21
|
// Client configuration
|
|
22
22
|
// ============================================================================
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
interface IVpnClientConfigBase {
|
|
25
25
|
/** Server WebSocket URL, e.g. wss://vpn.example.com/tunnel */
|
|
26
|
-
serverUrl
|
|
26
|
+
serverUrl?: string;
|
|
27
27
|
/** Server's static public key (base64) for Noise IK handshake */
|
|
28
28
|
serverPublicKey: string;
|
|
29
29
|
/** Client's Noise IK private key (base64) — required for SmartVPN native transport */
|
|
30
|
-
clientPrivateKey
|
|
30
|
+
clientPrivateKey?: string;
|
|
31
31
|
/** Client's Noise IK public key (base64) — for reference/display */
|
|
32
|
-
clientPublicKey
|
|
32
|
+
clientPublicKey?: string;
|
|
33
33
|
/** Optional DNS servers to use while connected */
|
|
34
34
|
dns?: string[];
|
|
35
|
-
/**
|
|
35
|
+
/** Inner TUN MTU in whole IP bytes, 576..65472 (default: 1420); not outer link MTU. */
|
|
36
36
|
mtu?: number;
|
|
37
37
|
/** Keepalive interval in seconds (default: 30) */
|
|
38
38
|
keepaliveIntervalSecs?: number;
|
|
@@ -41,7 +41,7 @@ export interface IVpnClientConfig {
|
|
|
41
41
|
/** For QUIC: SHA-256 hash of server certificate (base64) for cert pinning */
|
|
42
42
|
serverCertHash?: string;
|
|
43
43
|
/** Forwarding mode: 'tun' (TUN device, requires root) or 'testing' (no TUN).
|
|
44
|
-
* Default: 'testing'. */
|
|
44
|
+
* Default: native 'testing', WireGuard 'tun'. */
|
|
45
45
|
forwardingMode?: 'tun' | 'testing';
|
|
46
46
|
/** WireGuard: client private key (base64, X25519) */
|
|
47
47
|
wgPrivateKey?: string;
|
|
@@ -61,9 +61,37 @@ export interface IVpnClientConfig {
|
|
|
61
61
|
clientDefinedClientTags?: string[];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Native transports authenticate with Noise IK, independently of WireGuard. */
|
|
65
|
+
export interface IVpnNativeClientConfig extends IVpnClientConfigBase {
|
|
66
|
+
/** Explicit managed opt-in. Rejects a missing or differently bound server assignment. */
|
|
67
|
+
managedNetwork?: IManagedNetworkBinding;
|
|
68
|
+
transport?: 'auto' | 'websocket' | 'quic';
|
|
69
|
+
serverUrl: string;
|
|
70
|
+
clientPrivateKey: string;
|
|
71
|
+
clientPublicKey: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** WireGuard uses its own key and address; Noise credentials are not required. */
|
|
75
|
+
export interface IVpnWireguardClientConfig extends IVpnClientConfigBase {
|
|
76
|
+
transport: 'wireguard';
|
|
77
|
+
wgPrivateKey: string;
|
|
78
|
+
wgAddress: string;
|
|
79
|
+
wgEndpoint: string;
|
|
80
|
+
/** Explicit peer source/destination routing authority. Must not be empty. */
|
|
81
|
+
wgAllowedIps: string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type TVpnClientConfig = IVpnNativeClientConfig | IVpnWireguardClientConfig;
|
|
85
|
+
/** Retains the published config type name; both names enforce the transport union. */
|
|
86
|
+
export type IVpnClientConfig = TVpnClientConfig;
|
|
87
|
+
|
|
64
88
|
export interface IVpnClientOptions {
|
|
65
89
|
transport: TVpnTransportOptions;
|
|
66
90
|
config?: IVpnClientConfig;
|
|
91
|
+
/** Linux stdio only. Caller-owned network namespace FD, retained through every spawn.
|
|
92
|
+
* The complete native client enters before threads, sockets, TUN or readiness.
|
|
93
|
+
* The caller owns namespace creation, underlay routing, DNS and packet policy. */
|
|
94
|
+
networkNamespaceFd?: number;
|
|
67
95
|
}
|
|
68
96
|
|
|
69
97
|
// ============================================================================
|
|
@@ -85,7 +113,7 @@ export interface IVpnServerConfig {
|
|
|
85
113
|
subnet: string;
|
|
86
114
|
/** DNS servers pushed to clients */
|
|
87
115
|
dns?: string[];
|
|
88
|
-
/**
|
|
116
|
+
/** Inner TUN/forwarding packet ceiling in whole IP bytes, 576..65472 (default: 1420). */
|
|
89
117
|
mtu?: number;
|
|
90
118
|
/** Keepalive interval in seconds (default: 30) */
|
|
91
119
|
keepaliveIntervalSecs?: number;
|
|
@@ -93,8 +121,10 @@ export interface IVpnServerConfig {
|
|
|
93
121
|
enableNat?: boolean;
|
|
94
122
|
/** Forwarding mode: 'tun' (kernel TUN, requires root), 'socket' (userspace NAT),
|
|
95
123
|
* 'bridge' (L2 bridge to host LAN), 'hybrid' (per-client socket+bridge),
|
|
96
|
-
*
|
|
97
|
-
forwardingMode?: 'tun' | 'socket' | 'bridge' | 'hybrid' | 'testing';
|
|
124
|
+
* 'managed' (caller-owned node relay without host networking), or 'testing'. Default: 'testing'. */
|
|
125
|
+
forwardingMode?: 'tun' | 'socket' | 'bridge' | 'hybrid' | 'testing' | 'managed';
|
|
126
|
+
/** Required in managed mode. Bound to this process lifetime; not an authentication credential. */
|
|
127
|
+
managedAuthorityId?: string;
|
|
98
128
|
/** Default rate limit for new clients (bytes/sec). Omit for unlimited. */
|
|
99
129
|
defaultRateLimitBytesPerSec?: number;
|
|
100
130
|
/** Default burst size for new clients (bytes). Omit for unlimited. */
|
|
@@ -196,6 +226,8 @@ export type TVpnConnectionState =
|
|
|
196
226
|
| 'error';
|
|
197
227
|
|
|
198
228
|
export interface IVpnStatus {
|
|
229
|
+
/** Authenticated native assignment for this connection, absent after retirement. */
|
|
230
|
+
managedNetwork?: IManagedNetworkAssignment;
|
|
199
231
|
state: TVpnConnectionState;
|
|
200
232
|
assignedIp?: string;
|
|
201
233
|
serverAddr?: string;
|
|
@@ -276,11 +308,16 @@ export interface IVpnConnectionQuality {
|
|
|
276
308
|
// ============================================================================
|
|
277
309
|
|
|
278
310
|
export interface IVpnMtuInfo {
|
|
279
|
-
|
|
311
|
+
/** Actual inner TUN ceiling; null in testing mode because no TUN exists. */
|
|
312
|
+
tunMtu: number | null;
|
|
313
|
+
/** Enforced inner packet ceiling, not an estimated outer path MTU. */
|
|
280
314
|
effectiveMtu: number;
|
|
281
|
-
|
|
282
|
-
|
|
315
|
+
/** Null when the transport has not measured the outer link. */
|
|
316
|
+
linkMtu: number | null;
|
|
317
|
+
/** Null when the transport has not measured encapsulation overhead. */
|
|
318
|
+
overheadBytes: number | null;
|
|
283
319
|
oversizedPacketsDropped: number;
|
|
320
|
+
/** Feedback packets successfully written to the client TUN, not just generated. */
|
|
284
321
|
icmpTooBigSent: number;
|
|
285
322
|
}
|
|
286
323
|
|
|
@@ -384,6 +421,21 @@ export interface IClientEntry {
|
|
|
384
421
|
vlanId?: number;
|
|
385
422
|
}
|
|
386
423
|
|
|
424
|
+
/** Admin-writable settings. Omission preserves a setting; null clears it.
|
|
425
|
+
* Identity, keys, assignment and client-reported tags have separate owners.
|
|
426
|
+
* A supplied security object replaces the complete previous security object. */
|
|
427
|
+
export type TClientUpdateOptions = {
|
|
428
|
+
[TField in keyof Omit<
|
|
429
|
+
IClientEntry,
|
|
430
|
+
'clientId' | 'publicKey' | 'wgPublicKey' | 'assignedIp' | 'clientDefinedClientTags'
|
|
431
|
+
>]?: IClientEntry[TField] | null;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/** Creation generates server-owned keys and assignment; clientId is required. */
|
|
435
|
+
export interface IClientCreateOptions extends TClientUpdateOptions {
|
|
436
|
+
clientId: string;
|
|
437
|
+
}
|
|
438
|
+
|
|
387
439
|
/**
|
|
388
440
|
* Complete client config bundle — returned by createClient() and rotateClientKey().
|
|
389
441
|
* Contains everything the client needs to connect.
|
|
@@ -392,7 +444,7 @@ export interface IClientConfigBundle {
|
|
|
392
444
|
/** The server-side client entry */
|
|
393
445
|
entry: IClientEntry;
|
|
394
446
|
/** Ready-to-use SmartVPN client config (typed object) */
|
|
395
|
-
smartvpnConfig:
|
|
447
|
+
smartvpnConfig: IVpnNativeClientConfig;
|
|
396
448
|
/** Ready-to-use WireGuard .conf file content (string) */
|
|
397
449
|
wireguardConfig: string;
|
|
398
450
|
/** Client's private keys (ONLY returned at creation time, not stored server-side) */
|
|
@@ -441,10 +493,13 @@ export type TVpnClientCommands = {
|
|
|
441
493
|
getStatus: { params: Record<string, never>; result: IVpnStatus };
|
|
442
494
|
getStatistics: { params: Record<string, never>; result: IVpnStatistics };
|
|
443
495
|
getConnectionQuality: { params: Record<string, never>; result: IVpnConnectionQuality };
|
|
444
|
-
getMtuInfo: { params: Record<string, never>; result: IVpnMtuInfo };
|
|
496
|
+
getMtuInfo: { params: Record<string, never>; result: IVpnMtuInfo | null };
|
|
445
497
|
};
|
|
446
498
|
|
|
447
499
|
export type TVpnServerCommands = {
|
|
500
|
+
reconcileManagedNetwork: { params: { snapshot: IManagedNetworkSnapshot }; result: IManagedNetworkStatus };
|
|
501
|
+
getManagedNetworkStatus: { params: Record<string, never>; result: IManagedNetworkStatus };
|
|
502
|
+
getManagedNodeProjection: { params: { nodeId: string }; result: IManagedNodeProjection };
|
|
448
503
|
start: { params: { config: IVpnServerConfig }; result: void };
|
|
449
504
|
stop: { params: Record<string, never>; result: void };
|
|
450
505
|
getStatus: { params: Record<string, never>; result: IVpnStatus };
|
|
@@ -460,11 +515,11 @@ export type TVpnServerCommands = {
|
|
|
460
515
|
removeWgPeer: { params: { publicKey: string }; result: void };
|
|
461
516
|
listWgPeers: { params: Record<string, never>; result: { peers: IWgPeerInfo[] } };
|
|
462
517
|
// Client Registry (Hub) commands
|
|
463
|
-
createClient: { params: { client:
|
|
518
|
+
createClient: { params: { client: IClientCreateOptions }; result: IClientConfigBundle };
|
|
464
519
|
removeClient: { params: { clientId: string }; result: void };
|
|
465
520
|
getClient: { params: { clientId: string }; result: IClientEntry };
|
|
466
521
|
listRegisteredClients: { params: Record<string, never>; result: { clients: IClientEntry[] } };
|
|
467
|
-
updateClient: { params: { clientId: string; update:
|
|
522
|
+
updateClient: { params: { clientId: string; update: TClientUpdateOptions }; result: void };
|
|
468
523
|
enableClient: { params: { clientId: string }; result: void };
|
|
469
524
|
disableClient: { params: { clientId: string }; result: void };
|
|
470
525
|
rotateClientKey: { params: { clientId: string }; result: IClientConfigBundle };
|
|
@@ -472,6 +527,94 @@ export type TVpnServerCommands = {
|
|
|
472
527
|
generateClientKeypair: { params: Record<string, never>; result: IVpnKeypair };
|
|
473
528
|
};
|
|
474
529
|
|
|
530
|
+
/** Complete schema-v1 authority. No patch semantics or implicit/reverse grants. */
|
|
531
|
+
export interface IManagedNetworkSnapshot {
|
|
532
|
+
schemaVersion: 1;
|
|
533
|
+
authorityId: string;
|
|
534
|
+
/** Positive JSON-safe integer, monotonically increasing within the server lifetime. */
|
|
535
|
+
revision: number;
|
|
536
|
+
nodes: IManagedNetworkNode[];
|
|
537
|
+
grants: IManagedNetworkGrant[];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export interface IManagedNetworkNode {
|
|
541
|
+
nodeId: string;
|
|
542
|
+
publicKey: string;
|
|
543
|
+
wgPublicKey?: string;
|
|
544
|
+
controlAddress: string;
|
|
545
|
+
controlPolicyDomain: string;
|
|
546
|
+
enabled: boolean;
|
|
547
|
+
expiresAt?: string;
|
|
548
|
+
workloadPrefixes: IManagedNetworkPrefix[];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export interface IManagedNetworkPrefix {
|
|
552
|
+
cidr: string;
|
|
553
|
+
policyDomain: string;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface IManagedNetworkGrant {
|
|
557
|
+
sourceDomain: string;
|
|
558
|
+
destinationDomain: string;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export interface IManagedNetworkStatus {
|
|
562
|
+
authorityId: string;
|
|
563
|
+
lifetimeId: string;
|
|
564
|
+
state: 'idle' | 'applying' | 'failed';
|
|
565
|
+
appliedRevision: number | null;
|
|
566
|
+
pendingRevision: number | null;
|
|
567
|
+
stage: 'draining' | 'preparingTransport' | null;
|
|
568
|
+
lastError: string | null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export interface IManagedNodeProjection {
|
|
572
|
+
schemaVersion: 1;
|
|
573
|
+
authorityId: string;
|
|
574
|
+
lifetimeId: string;
|
|
575
|
+
appliedRevision: number | null;
|
|
576
|
+
nodeId: string;
|
|
577
|
+
/** Configuration is applied/admissible, not proof of connectivity or host readiness. */
|
|
578
|
+
ready: boolean;
|
|
579
|
+
config: IManagedNodeConfig | null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export interface IManagedNetworkAssignment {
|
|
583
|
+
schemaVersion: 1;
|
|
584
|
+
authorityId: string;
|
|
585
|
+
lifetimeId: string;
|
|
586
|
+
appliedRevision: number;
|
|
587
|
+
nodeId: string;
|
|
588
|
+
ownedPrefixes: string[];
|
|
589
|
+
/** Remote destinations only; never local workload prefixes or a default route. */
|
|
590
|
+
routes: string[];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export interface IManagedNetworkBinding {
|
|
594
|
+
authorityId: string;
|
|
595
|
+
nodeId: string;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface IManagedNodeConfig {
|
|
599
|
+
/** Caller-owned control address with /32; local workload prefixes are not hub routes. */
|
|
600
|
+
address: string;
|
|
601
|
+
gateway: string;
|
|
602
|
+
ownedPrefixes: IManagedNetworkPrefix[];
|
|
603
|
+
outgoingRoutes: string[];
|
|
604
|
+
incomingSources: string[];
|
|
605
|
+
mtu: number;
|
|
606
|
+
wireguard: IManagedWireGuardProjection | null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export interface IManagedWireGuardProjection {
|
|
610
|
+
publicKey: string;
|
|
611
|
+
serverPublicKey: string;
|
|
612
|
+
endpoint: string;
|
|
613
|
+
/** Union of remote outgoing/incoming prefixes plus hub MTU-feedback gateway /32.
|
|
614
|
+
* The hub still enforces directed grants. Never includes local workload prefixes. */
|
|
615
|
+
allowedIps: string[];
|
|
616
|
+
}
|
|
617
|
+
|
|
475
618
|
// ============================================================================
|
|
476
619
|
// Installer
|
|
477
620
|
// ============================================================================
|
package/ts/smartvpn.paths.ts
CHANGED
|
@@ -4,3 +4,22 @@ export const packageDir = plugins.path.join(
|
|
|
4
4
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
|
5
5
|
'../',
|
|
6
6
|
);
|
|
7
|
+
|
|
8
|
+
/** Select only an explicitly requested binary or this package's Linux musl artifact. */
|
|
9
|
+
export function getBinaryPath(
|
|
10
|
+
binaryName: string,
|
|
11
|
+
platform: NodeJS.Platform = process.platform,
|
|
12
|
+
arch: string = process.arch,
|
|
13
|
+
override: string | undefined = process.env.SMARTVPN_RUST_BINARY,
|
|
14
|
+
): string {
|
|
15
|
+
// Smartrust validates explicit paths, including empty/invalid overrides, without
|
|
16
|
+
// repairing their permissions or falling through to an unrelated executable.
|
|
17
|
+
if (override !== undefined) {
|
|
18
|
+
return override;
|
|
19
|
+
}
|
|
20
|
+
if (platform !== 'linux' || (arch !== 'x64' && arch !== 'arm64')) {
|
|
21
|
+
throw new Error(`SmartVPN does not bundle a daemon for ${platform}/${arch}; set SMARTVPN_RUST_BINARY to an explicitly built executable or use socket transport.`);
|
|
22
|
+
}
|
|
23
|
+
const suffix = arch === 'x64' ? 'linux_amd64_musl' : 'linux_arm64_musl';
|
|
24
|
+
return plugins.path.join(packageDir, 'dist_rust', `${binaryName}_${suffix}`);
|
|
25
|
+
}
|
package/readme.hints.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# smartvpn hints
|
|
2
|
-
|
|
3
|
-
## Static Rust Binaries
|
|
4
|
-
|
|
5
|
-
- The Rust binaries in `dist_rust/` are statically linked (static-pie) via `"static": true` in the `@git.zone/tsrust` block of `.smartconfig.json` (tsrust >= 1.4.1). They run on both glibc (Debian/Ubuntu) and musl (Alpine) systems.
|
|
6
|
-
- tsrust injects `RUSTFLAGS="-C target-feature=+crt-static"` only into its own per-target cargo invocations and verifies the result (no `PT_INTERP` ELF header). Keep `rust/.cargo/config.toml` free of `rustflags` entries: the injected env variable would replace them, and a repo-wide `rustflags` would also break plain `cargo test`/`cargo check` (proc-macros cannot build with `+crt-static` on linux-gnu without an explicit `--target`).
|
|
7
|
-
- `pnpm run test:rust` runs the Rust unit tests; plain `cargo test` in `rust/` works as well.
|
|
8
|
-
- Verify linkage manually with `ldd dist_rust/<binary>_linux_amd64` → "statically linked".
|
package/readme.plan.md
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
# PROXY Protocol v2 Support for SmartVPN WebSocket Transport
|
|
2
|
-
|
|
3
|
-
## Context
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
PROXY protocol v2 solves this by letting the proxy prepend a binary header with the real client IP/port before the WebSocket upgrade.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Design
|
|
12
|
-
|
|
13
|
-
### Two-Phase ACL with Real Client IP
|
|
14
|
-
|
|
15
|
-
```
|
|
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
|
|
23
|
-
```
|
|
24
|
-
|
|
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.
|
|
27
|
-
|
|
28
|
-
### No New Dependencies
|
|
29
|
-
|
|
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.
|
|
31
|
-
|
|
32
|
-
### Scope: WebSocket Only
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Implementation
|
|
41
|
-
|
|
42
|
-
### Phase 1: New Rust module `proxy_protocol.rs`
|
|
43
|
-
|
|
44
|
-
**New file: `rust/src/proxy_protocol.rs`**
|
|
45
|
-
|
|
46
|
-
PP v2 binary format:
|
|
47
|
-
```
|
|
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)
|
|
54
|
-
```
|
|
55
|
-
|
|
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
|
-
}
|
|
62
|
-
|
|
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
|
-
```
|
|
67
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
**Modify: `rust/src/lib.rs`** — add `pub mod proxy_protocol;`
|
|
75
|
-
|
|
76
|
-
### Phase 2: Server config + client info fields
|
|
77
|
-
|
|
78
|
-
**File: `rust/src/server.rs` — `ServerConfig`**
|
|
79
|
-
|
|
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>>,
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**File: `rust/src/server.rs` — `ClientInfo`**
|
|
90
|
-
|
|
91
|
-
Add:
|
|
92
|
-
```rust
|
|
93
|
-
/// Real client IP:port (from PROXY protocol header or direct TCP connection).
|
|
94
|
-
pub remote_addr: Option<String>,
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Phase 3: ACL helper
|
|
98
|
-
|
|
99
|
-
**File: `rust/src/acl.rs`**
|
|
100
|
-
|
|
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)
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
(Keeps `ip_matches_any` private; exposes only the specific check needed.)
|
|
110
|
-
|
|
111
|
-
### Phase 4: WebSocket listener integration
|
|
112
|
-
|
|
113
|
-
**File: `rust/src/server.rs` — `run_ws_listener()`**
|
|
114
|
-
|
|
115
|
-
Between `listener.accept()` and `transport::accept_connection()`:
|
|
116
|
-
|
|
117
|
-
```rust
|
|
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
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Then proceed with WS upgrade + handle_client_connection as before
|
|
149
|
-
```
|
|
150
|
-
|
|
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.
|
|
152
|
-
|
|
153
|
-
### Phase 5: Update `handle_client_connection` signature
|
|
154
|
-
|
|
155
|
-
**File: `rust/src/server.rs`**
|
|
156
|
-
|
|
157
|
-
Change signature:
|
|
158
|
-
```rust
|
|
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<()>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
After Noise IK handshake + registry lookup (where `client_security` is available), add connection-level per-client ACL:
|
|
168
|
-
|
|
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
|
-
```
|
|
183
|
-
|
|
184
|
-
Populate `remote_addr` when building `ClientInfo`:
|
|
185
|
-
```rust
|
|
186
|
-
remote_addr: remote_addr.map(|a| a.to_string()),
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Phase 6: QUIC listener — pass remote addr through
|
|
190
|
-
|
|
191
|
-
**File: `rust/src/server.rs` — `run_quic_listener()`**
|
|
192
|
-
|
|
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
|
-
```
|
|
199
|
-
|
|
200
|
-
### Phase 7: TypeScript interface updates
|
|
201
|
-
|
|
202
|
-
**File: `ts/smartvpn.interfaces.ts`**
|
|
203
|
-
|
|
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
|
-
```
|
|
212
|
-
|
|
213
|
-
Add to `IVpnClientInfo`:
|
|
214
|
-
```typescript
|
|
215
|
-
/** Real client IP:port (from PROXY protocol or direct TCP). */
|
|
216
|
-
remoteAddr?: string;
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
### Phase 8: Tests
|
|
220
|
-
|
|
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)
|
|
228
|
-
|
|
229
|
-
**Rust unit tests in `acl.rs`:**
|
|
230
|
-
- `is_connection_blocked` with various IP patterns
|
|
231
|
-
|
|
232
|
-
**TypeScript tests:**
|
|
233
|
-
- Config validation accepts `proxyProtocol: true` + `connectionIpBlockList`
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## Key Files to Modify
|
|
238
|
-
|
|
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` |
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Verification
|
|
250
|
-
|
|
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
|