@push.rocks/smartvpn 1.21.0 → 2.0.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 +3 -3
- package/dist_ts/smartvpn.classes.vpnbridge.js +13 -45
- package/dist_ts/smartvpn.classes.vpnclient.d.ts +5 -4
- package/dist_ts/smartvpn.classes.vpnclient.js +9 -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 +151 -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 +8 -6
- package/readme.hints.md +69 -3
- package/readme.md +313 -5
- package/third-party-notices.md +81 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.classes.vpnbridge.ts +12 -50
- package/ts/smartvpn.classes.vpnclient.ts +9 -5
- package/ts/smartvpn.classes.vpnconfig.ts +49 -13
- package/ts/smartvpn.classes.vpnserver.ts +94 -40
- package/ts/smartvpn.interfaces.ts +155 -16
- package/ts/smartvpn.paths.ts +19 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
These notices accompany SmartVPN's static Linux amd64 and arm64 musl executables.
|
|
4
|
+
SmartVPN's own code is MIT-licensed under [license.md](./license.md).
|
|
5
|
+
|
|
6
|
+
## Cargo dependencies
|
|
7
|
+
|
|
8
|
+
[Cargo dependency notices](./notices/cargo-dependencies.html) reproduce the selected
|
|
9
|
+
upstream license and copyright texts for 270 third-party packages. Eight additional
|
|
10
|
+
locked, feature-inactive packages are covered by these supplemental MIT notices:
|
|
11
|
+
|
|
12
|
+
- [aho-corasick 1.1.4](./notices/aho-corasick-license.txt), from the published crate's `LICENSE-MIT`.
|
|
13
|
+
- [defmt 0.3.100 and 1.0.1, defmt-macros 1.0.1, defmt-parser 1.0.0](./notices/defmt-license.txt),
|
|
14
|
+
from their shared upstream `LICENSE-MIT`. The respective source commits are
|
|
15
|
+
`f6decae6e09d1216aff052aa0733a6b5e632a100`, `8459657ef6df5e6f3386512744110caef5f0612a`
|
|
16
|
+
(also macros), and `4a8cdb44891ed57b8ff5a023b6bec7137c48708f` in
|
|
17
|
+
[knurling-rs/defmt](https://github.com/knurling-rs/defmt).
|
|
18
|
+
All three source files have SHA-256 `2710a622a896bba67356913d4d0492cab5465f61b2ecce6d880aeb483834fb50`;
|
|
19
|
+
the copy adds only a final newline.
|
|
20
|
+
- [proc-macro-error2 2.0.1 and proc-macro-error-attr2 2.0.0](./notices/proc-macro-error2-license.txt),
|
|
21
|
+
from their identical published `LICENSE-MIT` files.
|
|
22
|
+
- [valuable 0.1.1](./notices/valuable-license.txt), from
|
|
23
|
+
[its pinned upstream LICENSE](https://github.com/tokio-rs/valuable/blob/9efc29b6e58cef28f6566a47aa7e142a55fead77/LICENSE).
|
|
24
|
+
|
|
25
|
+
Together these cover all 278 locked registry packages, conservatively including
|
|
26
|
+
build-only and target-inactive dependencies. Listing a package does not mean it
|
|
27
|
+
is linked into either Linux executable. MIT is preferred where offered as an
|
|
28
|
+
alternative; other selected terms include Apache-2.0, BSD, ISC, Unicode-3.0,
|
|
29
|
+
CDLA-Permissive-2.0 and the existing tun 0.7.22 dependency's WTFPL.
|
|
30
|
+
The exact package versions, declared license expressions and reviewed file hashes
|
|
31
|
+
are recorded in [inventory.json](./notices/inventory.json).
|
|
32
|
+
|
|
33
|
+
The [additional MIT source attributions](./notices/mit-source-attributions.txt)
|
|
34
|
+
retain cesu8 1.1.0's Rust Project/Eric Kidd source headers and siphasher 1.0.2's
|
|
35
|
+
Rust Project/Frank Denis `COPYING` attribution, together with the selected MIT terms.
|
|
36
|
+
cesu8's historical Rust copyright inventory is also retained conservatively in
|
|
37
|
+
the generated notices. Combined upstream license files remain intact; additional
|
|
38
|
+
terms concerning unlinked components are not a claim that those components ship.
|
|
39
|
+
The earlier [Linux route backend notices](./assets/third-party-licenses.md) remain
|
|
40
|
+
as supplementary exact upstream attribution, not the complete distribution index.
|
|
41
|
+
|
|
42
|
+
## Compiler and static runtime
|
|
43
|
+
|
|
44
|
+
`rust-toolchain.toml` pins Rust 1.95.0 and both musl targets. Components supplied
|
|
45
|
+
by that compiler are outside Cargo.lock and have separate notices:
|
|
46
|
+
|
|
47
|
+
- [Rust standard-library inventory](./notices/rust-standard-library.html), from the
|
|
48
|
+
official toolchain's `share/doc/rust/COPYRIGHT-library.html`. Its source SHA-256
|
|
49
|
+
is `90567e2718bf7fd65a71a3a43c5596488e80e5f51ed02bfea6fec54458b5f3d1`;
|
|
50
|
+
the copy adds only a final newline. This upstream inventory covers other targets too.
|
|
51
|
+
- [musl copyright](./notices/musl-copyright.txt), from
|
|
52
|
+
[musl 1.2.5](https://git.musl-libc.org/cgit/musl/tree/COPYRIGHT?h=v1.2.5).
|
|
53
|
+
The C build toolchains use musl commit `0784374d561435f7c787a555aeab8ede699ed298`
|
|
54
|
+
with the two security patches selected by
|
|
55
|
+
[Rust 1.95.0's build recipe](https://github.com/rust-lang/rust/blob/1.95.0/src/ci/docker/scripts/musl-toolchain.sh).
|
|
56
|
+
The final executables link Rust's self-contained musl CRT/libraries.
|
|
57
|
+
- [Compiler-builtins license](./notices/compiler-builtins-license.txt), from
|
|
58
|
+
[Rust 1.95.0](https://github.com/rust-lang/rust/blob/1.95.0/library/compiler-builtins/LICENSE.txt),
|
|
59
|
+
retaining MIT and Apache-2.0 WITH LLVM-exception terms.
|
|
60
|
+
- [LLVM libunwind license](./notices/llvm-libunwind-license.txt), from
|
|
61
|
+
[Rust's pinned LLVM revision](https://github.com/rust-lang/llvm-project/blob/1cb4e3833c1919c2e6fb579a23ac0e2b22587b7e/libunwind/LICENSE.TXT),
|
|
62
|
+
including its additional third-party terms.
|
|
63
|
+
|
|
64
|
+
The TypeScript dependency graph is installed independently through the npm
|
|
65
|
+
manifest; its packages are not bundled into these Rust executables.
|
|
66
|
+
|
|
67
|
+
## Reproduction and review
|
|
68
|
+
|
|
69
|
+
From the repository root, with cargo-about 0.9.1:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
cargo about generate --manifest-path rust/Cargo.toml --config rust/about.toml --locked --fail --output-file notices/cargo-dependencies.html rust/about.hbs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The configuration selects checksum-verified upstream notices when published
|
|
76
|
+
crates omit or misname them. Network access is required for pinned Git sources.
|
|
77
|
+
After a Cargo graph, compiler, or notice-input change, regenerate and review the
|
|
78
|
+
notices, refresh the inventory, and rerun distribution review. Tests reject stale
|
|
79
|
+
hashes, incomplete graph coverage, generic copyright placeholders and missing npm
|
|
80
|
+
notice paths. Keep this index, `license.md`, `assets/third-party-licenses.md` and
|
|
81
|
+
the entire `notices/` directory beside any separately distributed executable.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -6,49 +6,6 @@ import type {
|
|
|
6
6
|
} from './smartvpn.interfaces.js';
|
|
7
7
|
import type { TCommandMap } from '@push.rocks/smartrust';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Get the package root directory.
|
|
11
|
-
*/
|
|
12
|
-
function getPackageRoot(): string {
|
|
13
|
-
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
|
14
|
-
return plugins.path.resolve(thisDir, '..');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Map Node.js platform/arch to tsrust's platform suffix.
|
|
19
|
-
*/
|
|
20
|
-
function getTsrustPlatformSuffix(): string | null {
|
|
21
|
-
const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
|
|
22
|
-
const osMap: Record<string, string> = { linux: 'linux', darwin: 'macos' };
|
|
23
|
-
const os = osMap[process.platform];
|
|
24
|
-
const arch = archMap[process.arch];
|
|
25
|
-
if (os && arch) {
|
|
26
|
-
return `${os}_${arch}`;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build local search paths for the smartvpn daemon binary.
|
|
33
|
-
*/
|
|
34
|
-
function buildLocalPaths(binaryName: string): string[] {
|
|
35
|
-
const packageRoot = getPackageRoot();
|
|
36
|
-
const suffix = getTsrustPlatformSuffix();
|
|
37
|
-
const paths: string[] = [];
|
|
38
|
-
|
|
39
|
-
// dist_rust/ (tsrust cross-compiled output)
|
|
40
|
-
if (suffix) {
|
|
41
|
-
paths.push(plugins.path.join(packageRoot, 'dist_rust', `${binaryName}_${suffix}`));
|
|
42
|
-
}
|
|
43
|
-
paths.push(plugins.path.join(packageRoot, 'dist_rust', binaryName));
|
|
44
|
-
|
|
45
|
-
// Local dev build paths
|
|
46
|
-
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', binaryName));
|
|
47
|
-
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', binaryName));
|
|
48
|
-
|
|
49
|
-
return paths;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
9
|
/**
|
|
53
10
|
* Shared bridge wrapper around smartrust RustBridge.
|
|
54
11
|
* Supports stdio mode (dev: spawn child process) and socket mode (production: connect to running daemon).
|
|
@@ -71,9 +28,11 @@ export class VpnBridge<TCommands extends TCommandMap> extends plugins.events.Eve
|
|
|
71
28
|
|
|
72
29
|
this.bridge = new plugins.smartrust.RustBridge<TCommands>({
|
|
73
30
|
binaryName,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
31
|
+
binaryPath: options.transport.transport === 'stdio'
|
|
32
|
+
? paths.getBinaryPath(binaryName)
|
|
33
|
+
: undefined,
|
|
34
|
+
localPaths: [],
|
|
35
|
+
searchSystemPath: false,
|
|
77
36
|
cliArgs: ['--management', '--mode', this.mode],
|
|
78
37
|
maxPayloadSize: 10 * 1024 * 1024, // 10 MB
|
|
79
38
|
});
|
|
@@ -85,6 +44,9 @@ export class VpnBridge<TCommands extends TCommandMap> extends plugins.events.Eve
|
|
|
85
44
|
this.bridge.on('reconnected', () => {
|
|
86
45
|
this.emit('reconnected');
|
|
87
46
|
});
|
|
47
|
+
this.bridge.on('stderr', (line: string) => {
|
|
48
|
+
this.emit('stderr', line);
|
|
49
|
+
});
|
|
88
50
|
|
|
89
51
|
// Forward management events from the daemon
|
|
90
52
|
// smartrust emits 'management:<eventName>' for unsolicited events
|
|
@@ -126,11 +88,11 @@ export class VpnBridge<TCommands extends TCommandMap> extends plugins.events.Eve
|
|
|
126
88
|
}
|
|
127
89
|
|
|
128
90
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
91
|
+
* Confirm closure of the owned transport. Socket mode leaves the remote daemon alive;
|
|
92
|
+
* stdio mode waits for child exit and stdio closure. Failure remains retryable upstream.
|
|
131
93
|
*/
|
|
132
|
-
public stop(): void {
|
|
133
|
-
this.bridge.
|
|
94
|
+
public async stop(): Promise<void> {
|
|
95
|
+
await this.bridge.terminate();
|
|
134
96
|
}
|
|
135
97
|
|
|
136
98
|
/**
|
|
@@ -32,6 +32,9 @@ export class VpnClient extends plugins.events.EventEmitter {
|
|
|
32
32
|
this.bridge.on('reconnected', () => {
|
|
33
33
|
this.emit('reconnected');
|
|
34
34
|
});
|
|
35
|
+
this.bridge.on('stderr', (line: string) => {
|
|
36
|
+
this.emit('stderr', line);
|
|
37
|
+
});
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -81,17 +84,18 @@ export class VpnClient extends plugins.events.EventEmitter {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Get MTU information
|
|
87
|
+
* Get active runtime MTU information, or null when disconnected.
|
|
88
|
+
* Unknown outer-path measurements remain null.
|
|
85
89
|
*/
|
|
86
|
-
public async getMtuInfo(): Promise<IVpnMtuInfo> {
|
|
90
|
+
public async getMtuInfo(): Promise<IVpnMtuInfo | null> {
|
|
87
91
|
return this.bridge.sendCommand('getMtuInfo', {} as Record<string, never>);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/**
|
|
91
|
-
* Stop the daemon bridge.
|
|
95
|
+
* Stop the daemon bridge and wait for confirmed ownership release.
|
|
92
96
|
*/
|
|
93
|
-
public stop(): void {
|
|
94
|
-
this.bridge.stop();
|
|
97
|
+
public async stop(): Promise<void> {
|
|
98
|
+
await this.bridge.stop();
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/**
|
|
@@ -8,6 +8,12 @@ import type {
|
|
|
8
8
|
* VPN configuration loader, saver, and validator.
|
|
9
9
|
*/
|
|
10
10
|
export class VpnConfig {
|
|
11
|
+
private static validateMtu(mtu?: number): void {
|
|
12
|
+
if (mtu !== undefined && (!Number.isInteger(mtu) || mtu < 576 || mtu > 65472)) {
|
|
13
|
+
throw new Error('VpnConfig: mtu must be an integer between 576 and 65472 inner IP bytes');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Validate a client config object. Throws on invalid config.
|
|
13
19
|
*/
|
|
@@ -31,14 +37,26 @@ export class VpnConfig {
|
|
|
31
37
|
if (config.wgPresharedKey) {
|
|
32
38
|
VpnConfig.validateBase64Key(config.wgPresharedKey, 'wgPresharedKey');
|
|
33
39
|
}
|
|
34
|
-
if (config.wgAllowedIps) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if (!Array.isArray(config.wgAllowedIps) || config.wgAllowedIps.length === 0) {
|
|
41
|
+
throw new Error('VpnConfig: wgAllowedIps must be a nonempty array for WireGuard transport');
|
|
42
|
+
}
|
|
43
|
+
for (const cidr of config.wgAllowedIps) {
|
|
44
|
+
if (!VpnConfig.isValidCidr(cidr)) {
|
|
45
|
+
throw new Error(`VpnConfig: invalid allowedIp CIDR: ${cidr}`);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
48
|
+
if (!VpnConfig.isValidIp(config.wgAddress)) {
|
|
49
|
+
throw new Error('VpnConfig: wgAddress must be an IPv4 address');
|
|
50
|
+
}
|
|
51
|
+
if (config.wgAddressPrefix !== undefined && (!Number.isInteger(config.wgAddressPrefix) || config.wgAddressPrefix < 0 || config.wgAddressPrefix > 32)) {
|
|
52
|
+
throw new Error('VpnConfig: wgAddressPrefix must be an integer from 0 through 32');
|
|
53
|
+
}
|
|
41
54
|
} else {
|
|
55
|
+
if (config.managedNetwork && (!config.managedNetwork.authorityId || !config.managedNetwork.nodeId
|
|
56
|
+
|| config.managedNetwork.authorityId.trim() !== config.managedNetwork.authorityId
|
|
57
|
+
|| config.managedNetwork.nodeId.trim() !== config.managedNetwork.nodeId)) {
|
|
58
|
+
throw new Error('VpnConfig: managedNetwork requires an explicit authorityId and nodeId');
|
|
59
|
+
}
|
|
42
60
|
if (!config.serverUrl) {
|
|
43
61
|
throw new Error('VpnConfig: serverUrl is required');
|
|
44
62
|
}
|
|
@@ -61,9 +79,7 @@ export class VpnConfig {
|
|
|
61
79
|
}
|
|
62
80
|
VpnConfig.validateBase64Key(config.clientPublicKey, 'clientPublicKey');
|
|
63
81
|
}
|
|
64
|
-
|
|
65
|
-
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
66
|
-
}
|
|
82
|
+
VpnConfig.validateMtu(config.mtu);
|
|
67
83
|
if (config.keepaliveIntervalSecs !== undefined && config.keepaliveIntervalSecs < 1) {
|
|
68
84
|
throw new Error('VpnConfig: keepaliveIntervalSecs must be >= 1');
|
|
69
85
|
}
|
|
@@ -80,16 +96,38 @@ export class VpnConfig {
|
|
|
80
96
|
* Validate a server config object. Throws on invalid config.
|
|
81
97
|
*/
|
|
82
98
|
public static validateServerConfig(config: IVpnServerConfig): void {
|
|
99
|
+
if (config.forwardingMode === 'managed') {
|
|
100
|
+
if (!config.managedAuthorityId || config.managedAuthorityId.trim() !== config.managedAuthorityId) {
|
|
101
|
+
throw new Error('VpnConfig: managed mode requires managedAuthorityId');
|
|
102
|
+
}
|
|
103
|
+
if (config.clients?.length || config.wgPeers?.length || config.enableNat === true
|
|
104
|
+
|| config.destinationPolicy !== undefined || config.clientAllowedIPs !== undefined || config.dns !== undefined
|
|
105
|
+
|| config.bridgeLanSubnet !== undefined || config.bridgePhysicalInterface !== undefined
|
|
106
|
+
|| config.bridgeIpRangeStart !== undefined || config.bridgeIpRangeEnd !== undefined
|
|
107
|
+
|| config.socketForwardProxyProtocol === true || config.socketForwardProxyProtocolSource !== undefined
|
|
108
|
+
|| config.socketForwardProxyProtocolVpnMetadata === true) {
|
|
109
|
+
throw new Error('VpnConfig: managed mode does not accept standalone clients, peers, routing, DNS, NAT or bridge configuration');
|
|
110
|
+
}
|
|
111
|
+
if (config.wgPrivateKey && ['all', 'wireguard'].includes(config.transportMode ?? 'all') && !config.serverEndpoint) {
|
|
112
|
+
throw new Error('VpnConfig: managed WireGuard requires an explicit serverEndpoint');
|
|
113
|
+
}
|
|
114
|
+
} else if (config.managedAuthorityId !== undefined) {
|
|
115
|
+
throw new Error('VpnConfig: managedAuthorityId requires managed mode');
|
|
116
|
+
}
|
|
117
|
+
if (config.wgPrivateKey) { VpnConfig.validateBase64Key(config.wgPrivateKey, 'wgPrivateKey'); }
|
|
83
118
|
if (config.transportMode === 'wireguard') {
|
|
84
119
|
// WireGuard server validation
|
|
85
120
|
if (!config.privateKey) {
|
|
86
121
|
throw new Error('VpnConfig: privateKey is required');
|
|
87
122
|
}
|
|
88
123
|
VpnConfig.validateBase64Key(config.privateKey, 'privateKey');
|
|
89
|
-
if (
|
|
124
|
+
if (config.forwardingMode === 'managed' && !config.wgPrivateKey) {
|
|
125
|
+
throw new Error('VpnConfig: managed WireGuard requires wgPrivateKey');
|
|
126
|
+
}
|
|
127
|
+
if (config.forwardingMode !== 'managed' && (!config.wgPeers || config.wgPeers.length === 0)) {
|
|
90
128
|
throw new Error('VpnConfig: at least one wgPeers entry is required for WireGuard mode');
|
|
91
129
|
}
|
|
92
|
-
for (const peer of config.wgPeers) {
|
|
130
|
+
for (const peer of config.wgPeers ?? []) {
|
|
93
131
|
if (!peer.publicKey) {
|
|
94
132
|
throw new Error('VpnConfig: peer publicKey is required');
|
|
95
133
|
}
|
|
@@ -138,9 +176,7 @@ export class VpnConfig {
|
|
|
138
176
|
}
|
|
139
177
|
}
|
|
140
178
|
}
|
|
141
|
-
|
|
142
|
-
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
143
|
-
}
|
|
179
|
+
VpnConfig.validateMtu(config.mtu);
|
|
144
180
|
if (config.keepaliveIntervalSecs !== undefined && config.keepaliveIntervalSecs < 1) {
|
|
145
181
|
throw new Error('VpnConfig: keepaliveIntervalSecs must be >= 1');
|
|
146
182
|
}
|
|
@@ -11,9 +11,14 @@ import type {
|
|
|
11
11
|
IWgPeerConfig,
|
|
12
12
|
IWgPeerInfo,
|
|
13
13
|
IClientEntry,
|
|
14
|
+
IClientCreateOptions,
|
|
15
|
+
TClientUpdateOptions,
|
|
14
16
|
IClientConfigBundle,
|
|
15
17
|
IDestinationPolicy,
|
|
16
18
|
TVpnServerCommands,
|
|
19
|
+
IManagedNetworkSnapshot,
|
|
20
|
+
IManagedNetworkStatus,
|
|
21
|
+
IManagedNodeProjection,
|
|
17
22
|
} from './smartvpn.interfaces.js';
|
|
18
23
|
|
|
19
24
|
/**
|
|
@@ -26,6 +31,10 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
26
31
|
private nftHealthInterval?: ReturnType<typeof setInterval>;
|
|
27
32
|
private nftSubnet?: string;
|
|
28
33
|
private nftPolicy?: IDestinationPolicy;
|
|
34
|
+
private nftHealthCheck?: Promise<void>;
|
|
35
|
+
private startOperation?: Promise<void>;
|
|
36
|
+
private stopOperation?: Promise<void>;
|
|
37
|
+
private remotePolicyStopAcknowledged = false;
|
|
29
38
|
|
|
30
39
|
constructor(options: IVpnServerOptions) {
|
|
31
40
|
super();
|
|
@@ -42,12 +51,28 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
42
51
|
this.bridge.on('reconnected', () => {
|
|
43
52
|
this.emit('reconnected');
|
|
44
53
|
});
|
|
54
|
+
this.bridge.on('stderr', (line: string) => {
|
|
55
|
+
this.emit('stderr', line);
|
|
56
|
+
});
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
48
60
|
* Start the daemon bridge (spawn or connect).
|
|
49
61
|
*/
|
|
50
62
|
public async start(config?: IVpnServerConfig): Promise<void> {
|
|
63
|
+
if (this.startOperation || this.stopOperation || this.nft) {
|
|
64
|
+
throw new Error('VpnServer: finish the current lifecycle operation and cleanup before starting');
|
|
65
|
+
}
|
|
66
|
+
const operation = this.startOwnedResources(config);
|
|
67
|
+
this.startOperation = operation;
|
|
68
|
+
try {
|
|
69
|
+
await operation;
|
|
70
|
+
} finally {
|
|
71
|
+
this.startOperation = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async startOwnedResources(config?: IVpnServerConfig): Promise<void> {
|
|
51
76
|
const started = await this.bridge.start();
|
|
52
77
|
if (!started) {
|
|
53
78
|
throw new Error('VpnServer: failed to start daemon bridge');
|
|
@@ -77,6 +102,20 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
77
102
|
return this.bridge.sendCommand('getStatus', {} as Record<string, never>);
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
/** Apply a complete caller-owned snapshot. An IPC timeout does not cancel its owner.
|
|
106
|
+
* Inspect status and replay the exact target after uncertainty or a pending failure. */
|
|
107
|
+
public async reconcileManagedNetwork(snapshot: IManagedNetworkSnapshot): Promise<IManagedNetworkStatus> {
|
|
108
|
+
return this.bridge.sendCommand('reconcileManagedNetwork', { snapshot });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public async getManagedNetworkStatus(): Promise<IManagedNetworkStatus> {
|
|
112
|
+
return this.bridge.sendCommand('getManagedNetworkStatus', {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async getManagedNodeProjection(nodeId: string): Promise<IManagedNodeProjection> {
|
|
116
|
+
return this.bridge.sendCommand('getManagedNodeProjection', { nodeId });
|
|
117
|
+
}
|
|
118
|
+
|
|
80
119
|
/**
|
|
81
120
|
* Get server statistics.
|
|
82
121
|
*/
|
|
@@ -170,7 +209,7 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
170
209
|
* Create a new client. Generates keypairs, assigns IP, returns full config bundle.
|
|
171
210
|
* The secrets (private keys) are only returned at creation time.
|
|
172
211
|
*/
|
|
173
|
-
public async createClient(opts:
|
|
212
|
+
public async createClient(opts: IClientCreateOptions): Promise<IClientConfigBundle> {
|
|
174
213
|
return this.bridge.sendCommand('createClient', { client: opts });
|
|
175
214
|
}
|
|
176
215
|
|
|
@@ -199,7 +238,7 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
199
238
|
/**
|
|
200
239
|
* Update a registered client's fields (ACLs, tags, description, etc.).
|
|
201
240
|
*/
|
|
202
|
-
public async updateClient(clientId: string, update:
|
|
241
|
+
public async updateClient(clientId: string, update: TClientUpdateOptions): Promise<void> {
|
|
203
242
|
await this.bridge.sendCommand('updateClient', { clientId, update });
|
|
204
243
|
}
|
|
205
244
|
|
|
@@ -246,6 +285,7 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
246
285
|
* Also starts a 60-second health check interval to re-apply if rules are removed externally.
|
|
247
286
|
*/
|
|
248
287
|
private async setupTunDestinationPolicy(subnet: string, policy: IDestinationPolicy): Promise<void> {
|
|
288
|
+
this.remotePolicyStopAcknowledged = false;
|
|
249
289
|
this.nftSubnet = subnet;
|
|
250
290
|
this.nftPolicy = policy;
|
|
251
291
|
this.nft = new plugins.smartnftables.SmartNftables({
|
|
@@ -257,24 +297,32 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
257
297
|
await this.applyDestinationPolicyRules();
|
|
258
298
|
|
|
259
299
|
// Health check: re-apply rules if they disappear
|
|
260
|
-
this.nftHealthInterval = setInterval(
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
console.warn('[smartvpn] nftables rules missing, re-applying destination policy');
|
|
266
|
-
this.nft = new plugins.smartnftables.SmartNftables({
|
|
267
|
-
tableName: 'smartvpn_tun',
|
|
268
|
-
});
|
|
269
|
-
await this.nft.initialize();
|
|
270
|
-
await this.applyDestinationPolicyRules();
|
|
271
|
-
}
|
|
272
|
-
} catch (err) {
|
|
273
|
-
console.warn(`[smartvpn] nftables health check failed: ${err}`);
|
|
274
|
-
}
|
|
300
|
+
this.nftHealthInterval = setInterval(() => {
|
|
301
|
+
if (this.nftHealthCheck || this.stopOperation) return;
|
|
302
|
+
this.nftHealthCheck = this.checkTunDestinationPolicy().finally(() => {
|
|
303
|
+
this.nftHealthCheck = undefined;
|
|
304
|
+
});
|
|
275
305
|
}, 60_000);
|
|
276
306
|
}
|
|
277
307
|
|
|
308
|
+
private async checkTunDestinationPolicy(): Promise<void> {
|
|
309
|
+
if (!this.nft) return;
|
|
310
|
+
try {
|
|
311
|
+
const exists = await this.nft.tableExists();
|
|
312
|
+
if (!exists) {
|
|
313
|
+
console.warn('[smartvpn] nftables rules missing, re-applying destination policy');
|
|
314
|
+
this.nft = new plugins.smartnftables.SmartNftables({
|
|
315
|
+
tableName: 'smartvpn_tun',
|
|
316
|
+
dryRun: process.getuid?.() !== 0,
|
|
317
|
+
});
|
|
318
|
+
await this.nft.initialize();
|
|
319
|
+
await this.applyDestinationPolicyRules();
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.warn(`[smartvpn] nftables health check failed: ${err}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
278
326
|
/**
|
|
279
327
|
* Apply destination policy as nftables rules.
|
|
280
328
|
* Order: blockList (drop) → allowList (accept) → default action.
|
|
@@ -331,37 +379,43 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
331
379
|
}
|
|
332
380
|
|
|
333
381
|
/**
|
|
334
|
-
*
|
|
382
|
+
* Drain startup, confirm daemon closure, then drain policy work and clean up.
|
|
383
|
+
* Failure preserves ownership so callers can retry; no timeout implies success.
|
|
335
384
|
*/
|
|
336
|
-
public
|
|
337
|
-
|
|
385
|
+
public stop(): Promise<void> {
|
|
386
|
+
if (this.stopOperation) return this.stopOperation;
|
|
387
|
+
const operation = this.stopOwnedResources();
|
|
388
|
+
this.stopOperation = operation;
|
|
389
|
+
void operation.then(
|
|
390
|
+
() => { this.stopOperation = undefined; },
|
|
391
|
+
() => { this.stopOperation = undefined; },
|
|
392
|
+
);
|
|
393
|
+
return operation;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async stopOwnedResources(): Promise<void> {
|
|
397
|
+
// A failed start is still reported to its caller; stop must clean its partial resources.
|
|
398
|
+
await this.startOperation?.catch(() => undefined);
|
|
338
399
|
if (this.nftHealthInterval) {
|
|
339
400
|
clearInterval(this.nftHealthInterval);
|
|
340
401
|
this.nftHealthInterval = undefined;
|
|
341
402
|
}
|
|
403
|
+
// A socket disconnect cannot stop a remote packet worker. If this facade
|
|
404
|
+
// installed its host policy, stop that server before releasing enforcement.
|
|
405
|
+
if (this.nft && this.options.transport.transport === 'socket' && !this.remotePolicyStopAcknowledged) {
|
|
406
|
+
await this.stopServer();
|
|
407
|
+
this.remotePolicyStopAcknowledged = true;
|
|
408
|
+
}
|
|
409
|
+
// In stdio mode, keep policy enforced until the owned worker has exited.
|
|
410
|
+
await this.bridge.stop();
|
|
411
|
+
await this.nftHealthCheck;
|
|
342
412
|
if (this.nft) {
|
|
343
|
-
|
|
344
|
-
await this.nft.cleanup();
|
|
345
|
-
} catch (e) {
|
|
346
|
-
console.warn(`[smartvpn] nftables cleanup failed: ${e}`);
|
|
347
|
-
}
|
|
413
|
+
await this.nft.cleanup();
|
|
348
414
|
this.nft = undefined;
|
|
349
415
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!this.bridge.running) {
|
|
354
|
-
resolve();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
const timeout = setTimeout(() => resolve(), 5000);
|
|
358
|
-
this.bridge.once('exit', () => {
|
|
359
|
-
clearTimeout(timeout);
|
|
360
|
-
resolve();
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
this.bridge.stop();
|
|
364
|
-
await exitPromise;
|
|
416
|
+
this.nftSubnet = undefined;
|
|
417
|
+
this.nftPolicy = undefined;
|
|
418
|
+
this.remotePolicyStopAcknowledged = false;
|
|
365
419
|
}
|
|
366
420
|
|
|
367
421
|
/**
|