@push.rocks/smartvpn 1.13.0 → 1.15.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.classes.vpnserver.d.ts +14 -0
- package/dist_ts/smartvpn.classes.vpnserver.js +90 -1
- package/dist_ts/smartvpn.interfaces.d.ts +20 -0
- package/dist_ts/smartvpn.plugins.d.ts +2 -1
- package/dist_ts/smartvpn.plugins.js +3 -2
- package/package.json +2 -1
- package/readme.md +169 -21
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.classes.vpnserver.ts +110 -0
- package/ts/smartvpn.interfaces.ts +21 -0
- package/ts/smartvpn.plugins.ts +2 -1
|
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.15.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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHlFQUF5RTtDQUN2RixDQUFBIn0=
|
|
@@ -6,6 +6,10 @@ import type { IVpnServerOptions, IVpnServerConfig, IVpnStatus, IVpnServerStatist
|
|
|
6
6
|
export declare class VpnServer extends plugins.events.EventEmitter {
|
|
7
7
|
private bridge;
|
|
8
8
|
private options;
|
|
9
|
+
private nft?;
|
|
10
|
+
private nftHealthInterval?;
|
|
11
|
+
private nftSubnet?;
|
|
12
|
+
private nftPolicy?;
|
|
9
13
|
constructor(options: IVpnServerOptions);
|
|
10
14
|
/**
|
|
11
15
|
* Start the daemon bridge (spawn or connect).
|
|
@@ -104,6 +108,16 @@ export declare class VpnServer extends plugins.events.EventEmitter {
|
|
|
104
108
|
* Generate a standalone Noise IK keypair (not tied to a client).
|
|
105
109
|
*/
|
|
106
110
|
generateClientKeypair(): Promise<IVpnKeypair>;
|
|
111
|
+
/**
|
|
112
|
+
* Set up nftables rules for TUN mode destination policy.
|
|
113
|
+
* Also starts a 60-second health check interval to re-apply if rules are removed externally.
|
|
114
|
+
*/
|
|
115
|
+
private setupTunDestinationPolicy;
|
|
116
|
+
/**
|
|
117
|
+
* Apply destination policy as nftables rules.
|
|
118
|
+
* Order: blockList (drop) → allowList (accept) → default action.
|
|
119
|
+
*/
|
|
120
|
+
private applyDestinationPolicyRules;
|
|
107
121
|
/**
|
|
108
122
|
* Stop the daemon bridge.
|
|
109
123
|
*/
|
|
@@ -30,6 +30,10 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
30
30
|
const cfg = config || this.options.config;
|
|
31
31
|
if (cfg) {
|
|
32
32
|
await this.bridge.sendCommand('start', { config: cfg });
|
|
33
|
+
// For TUN mode with a destination policy, set up nftables rules
|
|
34
|
+
if (cfg.forwardingMode === 'tun' && cfg.destinationPolicy) {
|
|
35
|
+
await this.setupTunDestinationPolicy(cfg.subnet, cfg.destinationPolicy);
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
@@ -180,10 +184,95 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
180
184
|
async generateClientKeypair() {
|
|
181
185
|
return this.bridge.sendCommand('generateClientKeypair', {});
|
|
182
186
|
}
|
|
187
|
+
// ── TUN Destination Policy via nftables ──────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Set up nftables rules for TUN mode destination policy.
|
|
190
|
+
* Also starts a 60-second health check interval to re-apply if rules are removed externally.
|
|
191
|
+
*/
|
|
192
|
+
async setupTunDestinationPolicy(subnet, policy) {
|
|
193
|
+
this.nftSubnet = subnet;
|
|
194
|
+
this.nftPolicy = policy;
|
|
195
|
+
this.nft = new plugins.smartnftables.SmartNftables({
|
|
196
|
+
tableName: 'smartvpn_tun',
|
|
197
|
+
dryRun: process.getuid?.() !== 0,
|
|
198
|
+
});
|
|
199
|
+
await this.nft.initialize();
|
|
200
|
+
await this.applyDestinationPolicyRules();
|
|
201
|
+
// Health check: re-apply rules if they disappear
|
|
202
|
+
this.nftHealthInterval = setInterval(async () => {
|
|
203
|
+
if (!this.nft)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
const exists = await this.nft.tableExists();
|
|
207
|
+
if (!exists) {
|
|
208
|
+
console.warn('[smartvpn] nftables rules missing, re-applying destination policy');
|
|
209
|
+
this.nft = new plugins.smartnftables.SmartNftables({
|
|
210
|
+
tableName: 'smartvpn_tun',
|
|
211
|
+
});
|
|
212
|
+
await this.nft.initialize();
|
|
213
|
+
await this.applyDestinationPolicyRules();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.warn(`[smartvpn] nftables health check failed: ${err}`);
|
|
218
|
+
}
|
|
219
|
+
}, 60_000);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Apply destination policy as nftables rules.
|
|
223
|
+
* Order: blockList (drop) → allowList (accept) → default action.
|
|
224
|
+
*/
|
|
225
|
+
async applyDestinationPolicyRules() {
|
|
226
|
+
if (!this.nft || !this.nftSubnet || !this.nftPolicy)
|
|
227
|
+
return;
|
|
228
|
+
const subnet = this.nftSubnet;
|
|
229
|
+
const policy = this.nftPolicy;
|
|
230
|
+
const family = 'ip';
|
|
231
|
+
const table = 'smartvpn_tun';
|
|
232
|
+
const commands = [];
|
|
233
|
+
// 1. Block list (deny wins — evaluated first)
|
|
234
|
+
if (policy.blockList) {
|
|
235
|
+
for (const dest of policy.blockList) {
|
|
236
|
+
commands.push(`nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} drop`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// 2. Allow list (pass through directly — skip DNAT)
|
|
240
|
+
if (policy.allowList) {
|
|
241
|
+
for (const dest of policy.allowList) {
|
|
242
|
+
commands.push(`nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} accept`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// 3. Default action
|
|
246
|
+
switch (policy.default) {
|
|
247
|
+
case 'forceTarget': {
|
|
248
|
+
const target = policy.target || '127.0.0.1';
|
|
249
|
+
commands.push(`nft add rule ${family} ${table} prerouting ip saddr ${subnet} dnat to ${target}`);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'block':
|
|
253
|
+
commands.push(`nft add rule ${family} ${table} prerouting ip saddr ${subnet} drop`);
|
|
254
|
+
break;
|
|
255
|
+
case 'allow':
|
|
256
|
+
// No rule needed — kernel default allows
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
if (commands.length > 0) {
|
|
260
|
+
await this.nft.applyRuleGroup('vpn-destination-policy', commands);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
183
263
|
/**
|
|
184
264
|
* Stop the daemon bridge.
|
|
185
265
|
*/
|
|
186
266
|
stop() {
|
|
267
|
+
// Clean up nftables rules
|
|
268
|
+
if (this.nftHealthInterval) {
|
|
269
|
+
clearInterval(this.nftHealthInterval);
|
|
270
|
+
this.nftHealthInterval = undefined;
|
|
271
|
+
}
|
|
272
|
+
if (this.nft) {
|
|
273
|
+
this.nft.cleanup().catch(() => { }); // best-effort cleanup
|
|
274
|
+
this.nft = undefined;
|
|
275
|
+
}
|
|
187
276
|
this.bridge.stop();
|
|
188
277
|
}
|
|
189
278
|
/**
|
|
@@ -193,4 +282,4 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
193
282
|
return this.bridge.running;
|
|
194
283
|
}
|
|
195
284
|
}
|
|
196
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
285
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnR2cG4uY2xhc3Nlcy52cG5zZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHZwbi5jbGFzc2VzLnZwbnNlcnZlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHVCQUF1QixDQUFDO0FBQ2pELE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQWlCNUQ7O0dBRUc7QUFDSCxNQUFNLE9BQU8sU0FBVSxTQUFRLE9BQU8sQ0FBQyxNQUFNLENBQUMsWUFBWTtJQVF4RCxZQUFZLE9BQTBCO1FBQ3BDLEtBQUssRUFBRSxDQUFDO1FBQ1IsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLFNBQVMsQ0FBcUI7WUFDOUMsU0FBUyxFQUFFLE9BQU8sQ0FBQyxTQUFTO1lBQzVCLElBQUksRUFBRSxRQUFRO1NBQ2YsQ0FBQyxDQUFDO1FBRUgsd0JBQXdCO1FBQ3hCLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxDQUFDLElBQW1CLEVBQUUsTUFBcUIsRUFBRSxFQUFFO1lBQ3BFLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDdEMsQ0FBQyxDQUFDLENBQUM7UUFDSCxJQUFJLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxhQUFhLEVBQUUsR0FBRyxFQUFFO1lBQ2pDLElBQUksQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDM0IsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBRUQ7O09BRUc7SUFDSSxLQUFLLENBQUMsS0FBSyxDQUFDLE1BQXlCO1FBQzFDLE1BQU0sT0FBTyxHQUFHLE1BQU0sSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQztRQUMxQyxJQUFJLENBQUMsT0FBTyxF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
|
|
@@ -108,6 +108,26 @@ export interface IVpnServerConfig {
|
|
|
108
108
|
* tunnel IP as the source address. This allows downstream services (e.g. SmartProxy)
|
|
109
109
|
* to see the real VPN client identity instead of 127.0.0.1. */
|
|
110
110
|
socketForwardProxyProtocol?: boolean;
|
|
111
|
+
/** Destination routing policy for VPN client traffic (socket mode).
|
|
112
|
+
* Controls where decrypted traffic goes: allow through, block, or redirect to a target.
|
|
113
|
+
* Default: all traffic passes through (backward compatible). */
|
|
114
|
+
destinationPolicy?: IDestinationPolicy;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Destination routing policy for VPN client traffic.
|
|
118
|
+
* Evaluated per-packet in the NAT engine before per-client ACLs.
|
|
119
|
+
*/
|
|
120
|
+
export interface IDestinationPolicy {
|
|
121
|
+
/** Default action for traffic not matching allow/block lists */
|
|
122
|
+
default: 'forceTarget' | 'block' | 'allow';
|
|
123
|
+
/** Target IP address for 'forceTarget' mode (e.g. '127.0.0.1'). Required when default is 'forceTarget'. */
|
|
124
|
+
target?: string;
|
|
125
|
+
/** Destinations that pass through directly — not rewritten, not blocked.
|
|
126
|
+
* Supports: exact IP, CIDR, wildcards (192.168.190.*), ranges. */
|
|
127
|
+
allowList?: string[];
|
|
128
|
+
/** Destinations that are always blocked. Overrides allowList (deny wins).
|
|
129
|
+
* Supports: exact IP, CIDR, wildcards, ranges. */
|
|
130
|
+
blockList?: string[];
|
|
111
131
|
}
|
|
112
132
|
export interface IVpnServerOptions {
|
|
113
133
|
transport: TVpnTransportOptions;
|
|
@@ -4,6 +4,7 @@ import * as os from 'os';
|
|
|
4
4
|
import * as url from 'url';
|
|
5
5
|
import * as events from 'events';
|
|
6
6
|
export { path, fs, os, url, events };
|
|
7
|
+
import * as smartnftables from '@push.rocks/smartnftables';
|
|
7
8
|
import * as smartpath from '@push.rocks/smartpath';
|
|
8
9
|
import * as smartrust from '@push.rocks/smartrust';
|
|
9
|
-
export { smartpath, smartrust };
|
|
10
|
+
export { smartnftables, smartpath, smartrust };
|
|
@@ -6,7 +6,8 @@ import * as url from 'url';
|
|
|
6
6
|
import * as events from 'events';
|
|
7
7
|
export { path, fs, os, url, events };
|
|
8
8
|
// @push.rocks
|
|
9
|
+
import * as smartnftables from '@push.rocks/smartnftables';
|
|
9
10
|
import * as smartpath from '@push.rocks/smartpath';
|
|
10
11
|
import * as smartrust from '@push.rocks/smartrust';
|
|
11
|
-
export { smartpath, smartrust };
|
|
12
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
12
|
+
export { smartnftables, smartpath, smartrust };
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnR2cG4ucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3NtYXJ0dnBuLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYztBQUNkLE9BQU8sS0FBSyxJQUFJLE1BQU0sTUFBTSxDQUFDO0FBQzdCLE9BQU8sS0FBSyxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBQ3pCLE9BQU8sS0FBSyxFQUFFLE1BQU0sSUFBSSxDQUFDO0FBQ3pCLE9BQU8sS0FBSyxHQUFHLE1BQU0sS0FBSyxDQUFDO0FBQzNCLE9BQU8sS0FBSyxNQUFNLE1BQU0sUUFBUSxDQUFDO0FBRWpDLE9BQU8sRUFBRSxJQUFJLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFFckMsY0FBYztBQUNkLE9BQU8sS0FBSyxhQUFhLE1BQU0sMkJBQTJCLENBQUM7QUFDM0QsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssU0FBUyxNQUFNLHVCQUF1QixDQUFDO0FBRW5ELE9BQU8sRUFBRSxhQUFhLEVBQUUsU0FBUyxFQUFFLFNBQVMsRUFBRSxDQUFDIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartvpn",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@push.rocks/smartnftables": "1.1.0",
|
|
32
33
|
"@push.rocks/smartpath": "^6.0.0",
|
|
33
34
|
"@push.rocks/smartrust": "^1.3.2"
|
|
34
35
|
},
|
package/readme.md
CHANGED
|
@@ -10,6 +10,7 @@ A high-performance VPN solution with a **TypeScript control plane** and a **Rust
|
|
|
10
10
|
🔄 **Hub API**: one `createClient()` call generates keys, assigns IP, returns both SmartVPN + WireGuard configs
|
|
11
11
|
📡 **Real-time telemetry**: RTT, jitter, loss ratio, link health — all via typed APIs
|
|
12
12
|
🌐 **Unified forwarding pipeline**: all transports share the same engine — TUN (kernel), userspace NAT (no root), or testing mode
|
|
13
|
+
🎯 **Destination routing policy**: force-target, block, or allow traffic per destination with nftables integration
|
|
13
14
|
|
|
14
15
|
## Issue Reporting and Security
|
|
15
16
|
|
|
@@ -36,11 +37,38 @@ The package ships with pre-compiled Rust binaries for **linux/amd64** and **linu
|
|
|
36
37
|
│ Config validation │ │ WS + QUIC + WireGuard │
|
|
37
38
|
│ Hub: client management │ │ TUN device, IP pool, NAT │
|
|
38
39
|
│ WireGuard .conf generation │ │ Rate limiting, ACLs, QoS │
|
|
40
|
+
│ nftables destination policy │ │ Destination routing, nftables│
|
|
39
41
|
└──────────────────────────────┘ └───────────────────────────────┘
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
**Split-plane design** — TypeScript handles orchestration, config, and DX; Rust handles every hot-path byte with zero-copy async I/O (tokio, mimalloc).
|
|
43
45
|
|
|
46
|
+
### IPC Transport Modes
|
|
47
|
+
|
|
48
|
+
The bridge between TypeScript and Rust supports two transport modes:
|
|
49
|
+
|
|
50
|
+
| Mode | Use Case | How It Works |
|
|
51
|
+
|------|----------|-------------|
|
|
52
|
+
| **stdio** | Development, testing | Spawns the Rust daemon as a child process, communicates over stdin/stdout |
|
|
53
|
+
| **socket** | Production | Connects to an already-running daemon via Unix domain socket, with optional auto-reconnect |
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Development: spawn the daemon
|
|
57
|
+
const server = new VpnServer({ transport: { transport: 'stdio' } });
|
|
58
|
+
|
|
59
|
+
// Production: connect to running daemon
|
|
60
|
+
const server = new VpnServer({
|
|
61
|
+
transport: {
|
|
62
|
+
transport: 'socket',
|
|
63
|
+
socketPath: '/var/run/smartvpn.sock',
|
|
64
|
+
autoReconnect: true,
|
|
65
|
+
reconnectBaseDelayMs: 100,
|
|
66
|
+
reconnectMaxDelayMs: 5000,
|
|
67
|
+
maxReconnectAttempts: 10,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
44
72
|
## Quick Start 🚀
|
|
45
73
|
|
|
46
74
|
### 1. Start a VPN Server (Hub)
|
|
@@ -54,8 +82,8 @@ await server.start({
|
|
|
54
82
|
privateKey: '<server-noise-private-key-base64>',
|
|
55
83
|
publicKey: '<server-noise-public-key-base64>',
|
|
56
84
|
subnet: '10.8.0.0/24',
|
|
57
|
-
transportMode: 'all',
|
|
58
|
-
forwardingMode: 'tun',
|
|
85
|
+
transportMode: 'all', // WebSocket + QUIC + WireGuard simultaneously (default)
|
|
86
|
+
forwardingMode: 'tun', // 'tun' (kernel), 'socket' (userspace NAT), or 'testing'
|
|
59
87
|
wgPrivateKey: '<server-wg-private-key-base64>', // required for WireGuard transport
|
|
60
88
|
enableNat: true,
|
|
61
89
|
dns: ['1.1.1.1', '8.8.8.8'],
|
|
@@ -67,7 +95,7 @@ await server.start({
|
|
|
67
95
|
```typescript
|
|
68
96
|
const bundle = await server.createClient({
|
|
69
97
|
clientId: 'alice-laptop',
|
|
70
|
-
|
|
98
|
+
serverDefinedClientTags: ['engineering'], // trusted tags for access control
|
|
71
99
|
security: {
|
|
72
100
|
destinationAllowList: ['10.0.0.0/8'], // can only reach internal network
|
|
73
101
|
destinationBlockList: ['10.0.0.99'], // except this host
|
|
@@ -155,6 +183,47 @@ await server.start({
|
|
|
155
183
|
- `remoteAddr` field on `IVpnClientInfo` exposes the real client IP for monitoring
|
|
156
184
|
- **Security**: must be `false` (default) when accepting direct connections — only enable behind a trusted proxy
|
|
157
185
|
|
|
186
|
+
### 🎯 Destination Routing Policy
|
|
187
|
+
|
|
188
|
+
Control where decrypted VPN client traffic goes — force it to a specific target, block it, or allow it through. Evaluated per-packet before per-client ACLs.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
await server.start({
|
|
192
|
+
// ...
|
|
193
|
+
forwardingMode: 'socket', // userspace NAT mode
|
|
194
|
+
destinationPolicy: {
|
|
195
|
+
default: 'forceTarget', // redirect all traffic to a target
|
|
196
|
+
target: '127.0.0.1', // target IP for 'forceTarget' mode
|
|
197
|
+
allowList: ['10.0.0.0/8'], // these destinations pass through directly
|
|
198
|
+
blockList: ['10.0.0.99'], // always blocked (deny overrides allow)
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Policy modes:**
|
|
204
|
+
|
|
205
|
+
| Mode | Behavior |
|
|
206
|
+
|------|----------|
|
|
207
|
+
| `'forceTarget'` | Rewrites destination IP to `target` — funnels all traffic through a single endpoint |
|
|
208
|
+
| `'block'` | Drops all traffic not explicitly in `allowList` |
|
|
209
|
+
| `'allow'` | Passes all traffic through (default, backward compatible) |
|
|
210
|
+
|
|
211
|
+
In **TUN mode**, destination policies are enforced via **nftables** rules (using `@push.rocks/smartnftables`). A 60-second health check automatically re-applies rules if they're removed externally.
|
|
212
|
+
|
|
213
|
+
In **socket mode**, the policy is evaluated in the userspace NAT engine before per-client ACLs.
|
|
214
|
+
|
|
215
|
+
### 🔗 Socket Forward Proxy Protocol
|
|
216
|
+
|
|
217
|
+
When using `forwardingMode: 'socket'` (userspace NAT), you can prepend **PROXY protocol v2 headers** on outbound TCP connections. This conveys the VPN client's tunnel IP as the source address to downstream services (e.g., SmartProxy):
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
await server.start({
|
|
221
|
+
// ...
|
|
222
|
+
forwardingMode: 'socket',
|
|
223
|
+
socketForwardProxyProtocol: true, // downstream sees VPN client IP, not 127.0.0.1
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
158
227
|
### 📦 Packet Forwarding Modes
|
|
159
228
|
|
|
160
229
|
SmartVPN supports three forwarding modes, configurable per-server and per-client:
|
|
@@ -190,6 +259,30 @@ The userspace NAT mode extracts destination IP/port from IP packets, opens a rea
|
|
|
190
259
|
- **Dead-peer detection**: 180s inactivity timeout
|
|
191
260
|
- **MTU management**: Automatic overhead calculation (IP+TCP+WS+Noise = 79 bytes)
|
|
192
261
|
|
|
262
|
+
### 🏷️ Client Tags (Trusted vs Informational)
|
|
263
|
+
|
|
264
|
+
SmartVPN separates server-managed tags from client-reported tags:
|
|
265
|
+
|
|
266
|
+
| Field | Set By | Trust Level | Use For |
|
|
267
|
+
|-------|--------|-------------|---------|
|
|
268
|
+
| `serverDefinedClientTags` | Server admin (via `createClient` / `updateClient`) | ✅ Trusted | Access control, routing, billing |
|
|
269
|
+
| `clientDefinedClientTags` | Client (reported after connection) | ⚠️ Informational | Diagnostics, client self-identification |
|
|
270
|
+
| `tags` | *(deprecated)* | — | Legacy alias for `serverDefinedClientTags` |
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Server-side: trusted tags
|
|
274
|
+
await server.createClient({
|
|
275
|
+
clientId: 'alice-laptop',
|
|
276
|
+
serverDefinedClientTags: ['engineering', 'office-berlin'],
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Client-side: informational tags (reported to server)
|
|
280
|
+
await client.connect({
|
|
281
|
+
// ...
|
|
282
|
+
clientDefinedClientTags: ['macOS', 'v2.1.0'],
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
193
286
|
### 🔄 Hub Client Management
|
|
194
287
|
|
|
195
288
|
The server acts as a **hub** — one API to manage all clients:
|
|
@@ -205,7 +298,7 @@ const all = await server.listRegisteredClients();
|
|
|
205
298
|
// Update (ACLs, tags, description, rate limits...)
|
|
206
299
|
await server.updateClient('bob-phone', {
|
|
207
300
|
security: { destinationAllowList: ['0.0.0.0/0'] },
|
|
208
|
-
|
|
301
|
+
serverDefinedClientTags: ['mobile', 'field-ops'],
|
|
209
302
|
});
|
|
210
303
|
|
|
211
304
|
// Enable / Disable
|
|
@@ -243,46 +336,100 @@ const conf = WgConfigGenerator.generateClientConfig({
|
|
|
243
336
|
// → standard WireGuard .conf compatible with wg-quick, iOS, Android
|
|
244
337
|
```
|
|
245
338
|
|
|
339
|
+
Server configs too:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
const serverConf = WgConfigGenerator.generateServerConfig({
|
|
343
|
+
privateKey: '<server-wg-private-key>',
|
|
344
|
+
address: '10.8.0.1/24',
|
|
345
|
+
listenPort: 51820,
|
|
346
|
+
enableNat: true,
|
|
347
|
+
natInterface: 'eth0',
|
|
348
|
+
peers: [
|
|
349
|
+
{ publicKey: '<client-wg-public-key>', allowedIps: ['10.8.0.2/32'] },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
246
354
|
### 🖥️ System Service Installation
|
|
247
355
|
|
|
356
|
+
Generate systemd (Linux) or launchd (macOS) service units:
|
|
357
|
+
|
|
248
358
|
```typescript
|
|
249
359
|
import { VpnInstaller } from '@push.rocks/smartvpn';
|
|
250
360
|
|
|
251
361
|
const unit = VpnInstaller.generateServiceUnit({
|
|
362
|
+
binaryPath: '/usr/local/bin/smartvpn_daemon',
|
|
363
|
+
socketPath: '/var/run/smartvpn.sock',
|
|
252
364
|
mode: 'server',
|
|
253
|
-
configPath: '/etc/smartvpn/server.json',
|
|
254
365
|
});
|
|
255
|
-
// unit.platform
|
|
256
|
-
// unit.content
|
|
366
|
+
// unit.platform → 'linux' | 'macos'
|
|
367
|
+
// unit.content → systemd unit file or launchd plist
|
|
257
368
|
// unit.installPath → /etc/systemd/system/smartvpn-server.service
|
|
258
369
|
```
|
|
259
370
|
|
|
371
|
+
You can also call `generateSystemdUnit()` or `generateLaunchdPlist()` directly for platform-specific options like custom descriptions.
|
|
372
|
+
|
|
373
|
+
### 📢 Events
|
|
374
|
+
|
|
375
|
+
Both `VpnServer` and `VpnClient` extend `EventEmitter` and emit typed events:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
server.on('client-connected', (info: IVpnClientInfo) => {
|
|
379
|
+
console.log(`${info.registeredClientId} connected from ${info.remoteAddr} via ${info.transportType}`);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
server.on('client-disconnected', ({ clientId, reason }) => {
|
|
383
|
+
console.log(`${clientId} disconnected: ${reason}`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
client.on('status', (status: IVpnStatus) => {
|
|
387
|
+
console.log(`State: ${status.state}, IP: ${status.assignedIp}`);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Both server and client emit:
|
|
391
|
+
server.on('exit', ({ code, signal }) => { /* daemon process exited */ });
|
|
392
|
+
server.on('reconnected', () => { /* socket transport reconnected */ });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
| Event | Emitted By | Payload |
|
|
396
|
+
|-------|-----------|---------|
|
|
397
|
+
| `status` | Both | `IVpnStatus` — connection state changes |
|
|
398
|
+
| `error` | Both | `{ message, code? }` |
|
|
399
|
+
| `client-connected` | Server | `IVpnClientInfo` — full client info including transport type |
|
|
400
|
+
| `client-disconnected` | Server | `{ clientId, reason? }` |
|
|
401
|
+
| `exit` | Both | `{ code, signal }` — daemon process exited |
|
|
402
|
+
| `reconnected` | Both | `void` — socket transport reconnected |
|
|
403
|
+
|
|
260
404
|
## API Reference 📖
|
|
261
405
|
|
|
262
406
|
### Classes
|
|
263
407
|
|
|
264
408
|
| Class | Description |
|
|
265
409
|
|-------|-------------|
|
|
266
|
-
| `VpnServer` | Manages the Rust daemon in server mode. Hub methods for client CRUD. |
|
|
267
|
-
| `VpnClient` | Manages the Rust daemon in client mode. Connect, disconnect, telemetry. |
|
|
268
|
-
| `VpnBridge<T>` | Low-level typed IPC bridge (stdio or Unix socket). |
|
|
269
|
-
| `VpnConfig` | Static config validation and file I/O. |
|
|
270
|
-
| `VpnInstaller` | Generates systemd/launchd service files. |
|
|
271
|
-
| `WgConfigGenerator` | Generates standard WireGuard `.conf` files. |
|
|
410
|
+
| `VpnServer` | Manages the Rust daemon in server mode. Hub methods for client CRUD, telemetry, rate limits, WireGuard peer management. |
|
|
411
|
+
| `VpnClient` | Manages the Rust daemon in client mode. Connect, disconnect, status, telemetry. |
|
|
412
|
+
| `VpnBridge<T>` | Low-level typed IPC bridge (stdio or Unix socket). Handles spawn, connect, reconnect, and typed command dispatch. |
|
|
413
|
+
| `VpnConfig` | Static config validation and JSON file I/O. Validates keys, addresses, CIDRs, MTU, etc. |
|
|
414
|
+
| `VpnInstaller` | Generates systemd/launchd service files for daemon deployment. |
|
|
415
|
+
| `WgConfigGenerator` | Generates standard WireGuard `.conf` files (client and server). |
|
|
272
416
|
|
|
273
417
|
### Key Interfaces
|
|
274
418
|
|
|
275
419
|
| Interface | Purpose |
|
|
276
420
|
|-----------|---------|
|
|
277
|
-
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, forwarding mode, clients, proxy protocol) |
|
|
278
|
-
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, forwarding mode, WG options) |
|
|
279
|
-
| `IClientEntry` | Server-side client definition (ID, keys, security, priority, tags, expiry) |
|
|
421
|
+
| `IVpnServerConfig` | Server configuration (listen addr, keys, subnet, transport mode, forwarding mode, clients, proxy protocol, destination policy) |
|
|
422
|
+
| `IVpnClientConfig` | Client configuration (server URL, keys, transport, forwarding mode, WG options, client-defined tags) |
|
|
423
|
+
| `IClientEntry` | Server-side client definition (ID, keys, security, priority, server/client tags, expiry) |
|
|
280
424
|
| `IClientSecurity` | Per-client ACLs and rate limits (SmartProxy-aligned naming) |
|
|
281
425
|
| `IClientRateLimit` | Rate limiting config (bytesPerSec, burstBytes) |
|
|
282
|
-
| `IClientConfigBundle` | Full config bundle returned by `createClient()` |
|
|
283
|
-
| `IVpnClientInfo` | Connected client info (IP, stats, authenticated key, remote addr) |
|
|
426
|
+
| `IClientConfigBundle` | Full config bundle returned by `createClient()` — includes SmartVPN config, WireGuard .conf, and secrets |
|
|
427
|
+
| `IVpnClientInfo` | Connected client info (IP, stats, authenticated key, remote addr, transport type) |
|
|
284
428
|
| `IVpnConnectionQuality` | RTT, jitter, loss ratio, link health |
|
|
429
|
+
| `IVpnMtuInfo` | TUN MTU, effective MTU, overhead bytes, oversized packet stats |
|
|
285
430
|
| `IVpnKeypair` | Base64-encoded public/private key pair |
|
|
431
|
+
| `IDestinationPolicy` | Destination routing policy (forceTarget / block / allow with allow/block lists) |
|
|
432
|
+
| `IVpnEventMap` | Typed event map for server and client EventEmitter |
|
|
286
433
|
|
|
287
434
|
### Server IPC Commands
|
|
288
435
|
|
|
@@ -317,7 +464,7 @@ const unit = VpnInstaller.generateServiceUnit({
|
|
|
317
464
|
// All transports simultaneously (default) — WS + QUIC + WireGuard
|
|
318
465
|
{ transportMode: 'all', listenAddr: '0.0.0.0:443', wgPrivateKey: '...', wgListenPort: 51820 }
|
|
319
466
|
|
|
320
|
-
// WS + QUIC only
|
|
467
|
+
// WS + QUIC only
|
|
321
468
|
{ transportMode: 'both', listenAddr: '0.0.0.0:443', quicListenAddr: '0.0.0.0:4433' }
|
|
322
469
|
|
|
323
470
|
// WebSocket only
|
|
@@ -376,7 +523,7 @@ pnpm install
|
|
|
376
523
|
# Build (TypeScript + Rust cross-compile)
|
|
377
524
|
pnpm build
|
|
378
525
|
|
|
379
|
-
# Run all tests
|
|
526
|
+
# Run all tests
|
|
380
527
|
pnpm test
|
|
381
528
|
|
|
382
529
|
# Run Rust tests directly
|
|
@@ -393,6 +540,7 @@ smartvpn/
|
|
|
393
540
|
├── ts/ # TypeScript control plane
|
|
394
541
|
│ ├── index.ts # All exports
|
|
395
542
|
│ ├── smartvpn.interfaces.ts # Interfaces, types, IPC command maps
|
|
543
|
+
│ ├── smartvpn.plugins.ts # Dependency imports
|
|
396
544
|
│ ├── smartvpn.classes.vpnserver.ts
|
|
397
545
|
│ ├── smartvpn.classes.vpnclient.ts
|
|
398
546
|
│ ├── smartvpn.classes.vpnbridge.ts
|
|
@@ -417,7 +565,7 @@ smartvpn/
|
|
|
417
565
|
│ ├── ratelimit.rs # Token bucket
|
|
418
566
|
│ ├── userspace_nat.rs # Userspace TCP/UDP NAT proxy
|
|
419
567
|
│ └── ... # tunnel, network, telemetry, qos, mtu, reconnect
|
|
420
|
-
├── test/ #
|
|
568
|
+
├── test/ # Test files
|
|
421
569
|
├── dist_ts/ # Compiled TypeScript
|
|
422
570
|
└── dist_rust/ # Cross-compiled binaries (linux amd64 + arm64)
|
|
423
571
|
```
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
IWgPeerInfo,
|
|
13
13
|
IClientEntry,
|
|
14
14
|
IClientConfigBundle,
|
|
15
|
+
IDestinationPolicy,
|
|
15
16
|
TVpnServerCommands,
|
|
16
17
|
} from './smartvpn.interfaces.js';
|
|
17
18
|
|
|
@@ -21,6 +22,10 @@ import type {
|
|
|
21
22
|
export class VpnServer extends plugins.events.EventEmitter {
|
|
22
23
|
private bridge: VpnBridge<TVpnServerCommands>;
|
|
23
24
|
private options: IVpnServerOptions;
|
|
25
|
+
private nft?: plugins.smartnftables.SmartNftables;
|
|
26
|
+
private nftHealthInterval?: ReturnType<typeof setInterval>;
|
|
27
|
+
private nftSubnet?: string;
|
|
28
|
+
private nftPolicy?: IDestinationPolicy;
|
|
24
29
|
|
|
25
30
|
constructor(options: IVpnServerOptions) {
|
|
26
31
|
super();
|
|
@@ -50,6 +55,11 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
50
55
|
const cfg = config || this.options.config;
|
|
51
56
|
if (cfg) {
|
|
52
57
|
await this.bridge.sendCommand('start', { config: cfg });
|
|
58
|
+
|
|
59
|
+
// For TUN mode with a destination policy, set up nftables rules
|
|
60
|
+
if (cfg.forwardingMode === 'tun' && cfg.destinationPolicy) {
|
|
61
|
+
await this.setupTunDestinationPolicy(cfg.subnet, cfg.destinationPolicy);
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -229,10 +239,110 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
229
239
|
return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
// ── TUN Destination Policy via nftables ──────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set up nftables rules for TUN mode destination policy.
|
|
246
|
+
* Also starts a 60-second health check interval to re-apply if rules are removed externally.
|
|
247
|
+
*/
|
|
248
|
+
private async setupTunDestinationPolicy(subnet: string, policy: IDestinationPolicy): Promise<void> {
|
|
249
|
+
this.nftSubnet = subnet;
|
|
250
|
+
this.nftPolicy = policy;
|
|
251
|
+
this.nft = new plugins.smartnftables.SmartNftables({
|
|
252
|
+
tableName: 'smartvpn_tun',
|
|
253
|
+
dryRun: process.getuid?.() !== 0,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await this.nft.initialize();
|
|
257
|
+
await this.applyDestinationPolicyRules();
|
|
258
|
+
|
|
259
|
+
// Health check: re-apply rules if they disappear
|
|
260
|
+
this.nftHealthInterval = setInterval(async () => {
|
|
261
|
+
if (!this.nft) return;
|
|
262
|
+
try {
|
|
263
|
+
const exists = await this.nft.tableExists();
|
|
264
|
+
if (!exists) {
|
|
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
|
+
}
|
|
275
|
+
}, 60_000);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Apply destination policy as nftables rules.
|
|
280
|
+
* Order: blockList (drop) → allowList (accept) → default action.
|
|
281
|
+
*/
|
|
282
|
+
private async applyDestinationPolicyRules(): Promise<void> {
|
|
283
|
+
if (!this.nft || !this.nftSubnet || !this.nftPolicy) return;
|
|
284
|
+
|
|
285
|
+
const subnet = this.nftSubnet;
|
|
286
|
+
const policy = this.nftPolicy;
|
|
287
|
+
const family = 'ip';
|
|
288
|
+
const table = 'smartvpn_tun';
|
|
289
|
+
const commands: string[] = [];
|
|
290
|
+
|
|
291
|
+
// 1. Block list (deny wins — evaluated first)
|
|
292
|
+
if (policy.blockList) {
|
|
293
|
+
for (const dest of policy.blockList) {
|
|
294
|
+
commands.push(
|
|
295
|
+
`nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} drop`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 2. Allow list (pass through directly — skip DNAT)
|
|
301
|
+
if (policy.allowList) {
|
|
302
|
+
for (const dest of policy.allowList) {
|
|
303
|
+
commands.push(
|
|
304
|
+
`nft add rule ${family} ${table} prerouting ip saddr ${subnet} ip daddr ${dest} accept`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3. Default action
|
|
310
|
+
switch (policy.default) {
|
|
311
|
+
case 'forceTarget': {
|
|
312
|
+
const target = policy.target || '127.0.0.1';
|
|
313
|
+
commands.push(
|
|
314
|
+
`nft add rule ${family} ${table} prerouting ip saddr ${subnet} dnat to ${target}`
|
|
315
|
+
);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case 'block':
|
|
319
|
+
commands.push(
|
|
320
|
+
`nft add rule ${family} ${table} prerouting ip saddr ${subnet} drop`
|
|
321
|
+
);
|
|
322
|
+
break;
|
|
323
|
+
case 'allow':
|
|
324
|
+
// No rule needed — kernel default allows
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (commands.length > 0) {
|
|
329
|
+
await this.nft.applyRuleGroup('vpn-destination-policy', commands);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
232
333
|
/**
|
|
233
334
|
* Stop the daemon bridge.
|
|
234
335
|
*/
|
|
235
336
|
public stop(): void {
|
|
337
|
+
// Clean up nftables rules
|
|
338
|
+
if (this.nftHealthInterval) {
|
|
339
|
+
clearInterval(this.nftHealthInterval);
|
|
340
|
+
this.nftHealthInterval = undefined;
|
|
341
|
+
}
|
|
342
|
+
if (this.nft) {
|
|
343
|
+
this.nft.cleanup().catch(() => {}); // best-effort cleanup
|
|
344
|
+
this.nft = undefined;
|
|
345
|
+
}
|
|
236
346
|
this.bridge.stop();
|
|
237
347
|
}
|
|
238
348
|
|
|
@@ -125,6 +125,27 @@ export interface IVpnServerConfig {
|
|
|
125
125
|
* tunnel IP as the source address. This allows downstream services (e.g. SmartProxy)
|
|
126
126
|
* to see the real VPN client identity instead of 127.0.0.1. */
|
|
127
127
|
socketForwardProxyProtocol?: boolean;
|
|
128
|
+
/** Destination routing policy for VPN client traffic (socket mode).
|
|
129
|
+
* Controls where decrypted traffic goes: allow through, block, or redirect to a target.
|
|
130
|
+
* Default: all traffic passes through (backward compatible). */
|
|
131
|
+
destinationPolicy?: IDestinationPolicy;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Destination routing policy for VPN client traffic.
|
|
136
|
+
* Evaluated per-packet in the NAT engine before per-client ACLs.
|
|
137
|
+
*/
|
|
138
|
+
export interface IDestinationPolicy {
|
|
139
|
+
/** Default action for traffic not matching allow/block lists */
|
|
140
|
+
default: 'forceTarget' | 'block' | 'allow';
|
|
141
|
+
/** Target IP address for 'forceTarget' mode (e.g. '127.0.0.1'). Required when default is 'forceTarget'. */
|
|
142
|
+
target?: string;
|
|
143
|
+
/** Destinations that pass through directly — not rewritten, not blocked.
|
|
144
|
+
* Supports: exact IP, CIDR, wildcards (192.168.190.*), ranges. */
|
|
145
|
+
allowList?: string[];
|
|
146
|
+
/** Destinations that are always blocked. Overrides allowList (deny wins).
|
|
147
|
+
* Supports: exact IP, CIDR, wildcards, ranges. */
|
|
148
|
+
blockList?: string[];
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
export interface IVpnServerOptions {
|
package/ts/smartvpn.plugins.ts
CHANGED
|
@@ -8,7 +8,8 @@ import * as events from 'events';
|
|
|
8
8
|
export { path, fs, os, url, events };
|
|
9
9
|
|
|
10
10
|
// @push.rocks
|
|
11
|
+
import * as smartnftables from '@push.rocks/smartnftables';
|
|
11
12
|
import * as smartpath from '@push.rocks/smartpath';
|
|
12
13
|
import * as smartrust from '@push.rocks/smartrust';
|
|
13
14
|
|
|
14
|
-
export { smartpath, smartrust };
|
|
15
|
+
export { smartnftables, smartpath, smartrust };
|