@push.rocks/smartvpn 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartvpn',
6
- version: '1.1.0',
6
+ version: '1.3.0',
7
7
  description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxzQkFBc0I7SUFDNUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHlFQUF5RTtDQUN2RixDQUFBIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartvpn",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "description": "A VPN solution with TypeScript control plane and Rust data plane daemon",
6
6
  "type": "module",
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @push.rocks/smartvpn
2
2
 
3
- A high-performance VPN solution with a **TypeScript control plane** and a **Rust data plane daemon**. Manage VPN connections with clean, typed APIs while all networking heavy lifting — encryption, tunneling, packet forwarding — runs at native speed in Rust.
3
+ A high-performance VPN with a **TypeScript control plane** and a **Rust data plane daemon**. Manage VPN connections with clean, fully-typed APIs while all networking heavy lifting — encryption, tunneling, QoS, rate limiting — runs at native speed in Rust.
4
4
 
5
5
  ## Issue Reporting and Security
6
6
 
@@ -9,8 +9,6 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- npm install @push.rocks/smartvpn
13
- # or
14
12
  pnpm install @push.rocks/smartvpn
15
13
  ```
16
14
 
@@ -18,17 +16,21 @@ pnpm install @push.rocks/smartvpn
18
16
 
19
17
  ```
20
18
  TypeScript (control plane) Rust (data plane)
21
- ┌──────────────────────────┐ ┌───────────────────────────────┐
22
- │ VpnClient / VpnServer │ │ smartvpn_daemon
23
- │ └─ VpnBridge │──stdio/──▶ │ ├─ management (JSON IPC)
24
- │ └─ RustBridge │ socket │ ├─ transport (WebSocket/TLS)
25
- │ (smartrust) │ │ ├─ crypto (Noise NK + XCha)
26
- └──────────────────────────┘ │ ├─ codec (binary framing)
27
- │ ├─ keepalive (app-level)
28
- │ ├─ tunnel (TUN device)
29
- │ ├─ network (NAT/IP pool)
30
- └─ reconnect (backoff) │
31
- └───────────────────────────────┘
19
+ ┌──────────────────────────┐ ┌────────────────────────────────────┐
20
+ │ VpnClient / VpnServer │ │ smartvpn_daemon
21
+ │ └─ VpnBridge │──stdio/──▶ │ ├─ management (JSON IPC)
22
+ │ └─ RustBridge │ socket │ ├─ transport (WebSocket/TLS)
23
+ │ (smartrust) │ │ ├─ crypto (Noise NK + XCha20)
24
+ └──────────────────────────┘ │ ├─ codec (binary framing)
25
+ │ ├─ keepalive (adaptive state FSM)
26
+ │ ├─ telemetry (RTT/jitter/loss)
27
+ │ ├─ qos (classify + priority Q)
28
+ ├─ ratelimit (token bucket) │
29
+ │ ├─ mtu (overhead calc + ICMP) │
30
+ │ ├─ tunnel (TUN device) │
31
+ │ ├─ network (NAT/IP pool) │
32
+ │ └─ reconnect (exp. backoff) │
33
+ └────────────────────────────────────┘
32
34
  ```
33
35
 
34
36
  **Key design decisions:**
@@ -37,8 +39,10 @@ TypeScript (control plane) Rust (data plane)
37
39
  |----------|--------|-----|
38
40
  | Transport | WebSocket over HTTPS | Works through Cloudflare and other terminating proxies |
39
41
  | Encryption | Noise NK + XChaCha20-Poly1305 | Strong forward secrecy, large nonce space (no counter needed) |
40
- | Keepalive | App-level (not WS pings) | Cloudflare drops WS ping frames; app-level pings survive |
41
- | IPC | JSON lines over stdio/Unix socket | `stdio` for dev, `socket` for production (daemon stays alive) |
42
+ | Keepalive | Adaptive app-level pings | Cloudflare drops WS pings; interval adapts to link health (10–60s) |
43
+ | QoS | Packet classification + priority queues | DNS/SSH/ICMP always drain first; bulk flows get deprioritized |
44
+ | Rate limiting | Per-client token bucket | Byte-granular, dynamically reconfigurable via IPC |
45
+ | IPC | JSON lines over stdio / Unix socket | `stdio` for dev, `socket` for production (daemon stays alive) |
42
46
  | Binary protocol | `[type:1B][length:4B][payload:NB]` | Minimal overhead, easy to parse at wire speed |
43
47
 
44
48
  ## 🚀 Quick Start
@@ -48,15 +52,12 @@ TypeScript (control plane) Rust (data plane)
48
52
  ```typescript
49
53
  import { VpnClient } from '@push.rocks/smartvpn';
50
54
 
51
- // Development: spawn the Rust daemon as a child process
52
55
  const client = new VpnClient({
53
56
  transport: { transport: 'stdio' },
54
57
  });
55
58
 
56
- // Start the daemon bridge
57
59
  await client.start();
58
60
 
59
- // Connect to a VPN server
60
61
  const { assignedIp } = await client.connect({
61
62
  serverUrl: 'wss://vpn.example.com/tunnel',
62
63
  serverPublicKey: 'BASE64_SERVER_PUBLIC_KEY',
@@ -67,15 +68,23 @@ const { assignedIp } = await client.connect({
67
68
 
68
69
  console.log(`Connected! Assigned IP: ${assignedIp}`);
69
70
 
70
- // Check status
71
- const status = await client.getStatus();
72
- console.log(status); // { state: 'connected', assignedIp: '10.8.0.2', ... }
73
-
74
- // Get traffic stats
71
+ // Connection quality (adaptive keepalive + telemetry)
72
+ const quality = await client.getConnectionQuality();
73
+ console.log(quality);
74
+ // {
75
+ // srttMs: 42.5, jitterMs: 3.2, minRttMs: 38.0, maxRttMs: 67.0,
76
+ // lossRatio: 0.0, consecutiveTimeouts: 0,
77
+ // linkHealth: 'healthy', currentKeepaliveIntervalSecs: 60
78
+ // }
79
+
80
+ // MTU info
81
+ const mtu = await client.getMtuInfo();
82
+ console.log(mtu);
83
+ // { tunMtu: 1420, effectiveMtu: 1421, linkMtu: 1500, overheadBytes: 79, ... }
84
+
85
+ // Traffic stats (includes quality snapshot)
75
86
  const stats = await client.getStatistics();
76
- console.log(stats); // { bytesSent, bytesReceived, packetsSent, ... }
77
87
 
78
- // Disconnect
79
88
  await client.disconnect();
80
89
  client.stop();
81
90
  ```
@@ -89,33 +98,44 @@ const server = new VpnServer({
89
98
  transport: { transport: 'stdio' },
90
99
  });
91
100
 
92
- // Start the daemon and the VPN server
101
+ // Generate a Noise keypair first
102
+ await server.start();
103
+ // If you don't have keys yet:
104
+ const keypair = await server.generateKeypair();
105
+
106
+ // Start the VPN listener (or pass config to start() directly)
93
107
  await server.start({
94
108
  listenAddr: '0.0.0.0:443',
95
- privateKey: 'BASE64_PRIVATE_KEY',
96
- publicKey: 'BASE64_PUBLIC_KEY',
109
+ privateKey: keypair.privateKey,
110
+ publicKey: keypair.publicKey,
97
111
  subnet: '10.8.0.0/24',
98
112
  dns: ['1.1.1.1'],
99
113
  mtu: 1420,
100
114
  enableNat: true,
115
+ // Optional: default rate limit for all new clients
116
+ defaultRateLimitBytesPerSec: 10_000_000, // 10 MB/s
117
+ defaultBurstBytes: 20_000_000, // 20 MB burst
101
118
  });
102
119
 
103
- // Generate a Noise keypair
104
- const keypair = await server.generateKeypair();
105
- console.log(keypair); // { publicKey: '...', privateKey: '...' }
106
-
107
120
  // List connected clients
108
121
  const clients = await server.listClients();
109
- // [{ clientId, assignedIp, connectedSince, bytesSent, bytesReceived }]
110
122
 
111
- // Disconnect a specific client
112
- await server.disconnectClient('some-client-id');
123
+ // Per-client rate limiting (live, no reconnect needed)
124
+ await server.setClientRateLimit('client-id', 5_000_000, 10_000_000);
125
+ await server.removeClientRateLimit('client-id'); // unlimited
126
+
127
+ // Per-client telemetry
128
+ const telemetry = await server.getClientTelemetry('client-id');
129
+ console.log(telemetry);
130
+ // {
131
+ // clientId, assignedIp, lastKeepaliveAt, keepalivesReceived,
132
+ // packetsDropped, bytesDropped, bytesReceived, bytesSent,
133
+ // rateLimitBytesPerSec, burstBytes
134
+ // }
113
135
 
114
- // Get server stats
115
- const stats = await server.getStatistics();
116
- // { bytesSent, bytesReceived, activeClients, totalConnections, ... }
136
+ // Kick a client
137
+ await server.disconnectClient('client-id');
117
138
 
118
- // Stop
119
139
  await server.stopServer();
120
140
  server.stop();
121
141
  ```
@@ -151,7 +171,9 @@ When using socket transport, `client.stop()` closes the socket but **does not ki
151
171
  | `connect(config?)` | `Promise<{ assignedIp }>` | Connect to VPN server |
152
172
  | `disconnect()` | `Promise<void>` | Disconnect from VPN |
153
173
  | `getStatus()` | `Promise<IVpnStatus>` | Current connection state |
154
- | `getStatistics()` | `Promise<IVpnStatistics>` | Traffic statistics |
174
+ | `getStatistics()` | `Promise<IVpnStatistics>` | Traffic stats + connection quality |
175
+ | `getConnectionQuality()` | `Promise<IVpnConnectionQuality>` | RTT, jitter, loss, link health |
176
+ | `getMtuInfo()` | `Promise<IVpnMtuInfo>` | MTU info and overhead breakdown |
155
177
  | `stop()` | `void` | Kill/close the daemon bridge |
156
178
  | `running` | `boolean` | Whether bridge is active |
157
179
 
@@ -163,9 +185,12 @@ When using socket transport, `client.stop()` closes the socket but **does not ki
163
185
  | `stopServer()` | `Promise<void>` | Stop the VPN server |
164
186
  | `getStatus()` | `Promise<IVpnStatus>` | Server connection state |
165
187
  | `getStatistics()` | `Promise<IVpnServerStatistics>` | Server stats (includes client counts) |
166
- | `listClients()` | `Promise<IVpnClientInfo[]>` | Connected clients |
188
+ | `listClients()` | `Promise<IVpnClientInfo[]>` | Connected clients with QoS stats |
167
189
  | `disconnectClient(id)` | `Promise<void>` | Kick a client |
168
190
  | `generateKeypair()` | `Promise<IVpnKeypair>` | Generate Noise NK keypair |
191
+ | `setClientRateLimit(id, rate, burst)` | `Promise<void>` | Set per-client rate limit (bytes/sec) |
192
+ | `removeClientRateLimit(id)` | `Promise<void>` | Remove rate limit (unlimited) |
193
+ | `getClientTelemetry(id)` | `Promise<IVpnClientTelemetry>` | Per-client telemetry + drop stats |
169
194
  | `stop()` | `void` | Kill/close the daemon bridge |
170
195
 
171
196
  ### `VpnConfig`
@@ -191,26 +216,23 @@ Generate system service units for the daemon:
191
216
  ```typescript
192
217
  import { VpnInstaller } from '@push.rocks/smartvpn';
193
218
 
194
- // Auto-detect platform
195
219
  const platform = VpnInstaller.detectPlatform(); // 'linux' | 'macos' | 'windows' | 'unknown'
196
220
 
197
- // Generate systemd unit (Linux)
221
+ // Linux (systemd)
198
222
  const unit = VpnInstaller.generateSystemdUnit({
199
223
  binaryPath: '/usr/local/bin/smartvpn_daemon',
200
224
  socketPath: '/var/run/smartvpn.sock',
201
225
  mode: 'server',
202
226
  });
203
- // unit.content = full systemd .service file
204
- // unit.installPath = '/etc/systemd/system/smartvpn-server.service'
205
227
 
206
- // Generate launchd plist (macOS)
228
+ // macOS (launchd)
207
229
  const plist = VpnInstaller.generateLaunchdPlist({
208
230
  binaryPath: '/usr/local/bin/smartvpn_daemon',
209
231
  socketPath: '/var/run/smartvpn.sock',
210
232
  mode: 'client',
211
233
  });
212
234
 
213
- // Auto-detect and generate
235
+ // Auto-detect platform
214
236
  const serviceUnit = VpnInstaller.generateServiceUnit({
215
237
  binaryPath: '/usr/local/bin/smartvpn_daemon',
216
238
  socketPath: '/var/run/smartvpn.sock',
@@ -223,8 +245,6 @@ const serviceUnit = VpnInstaller.generateServiceUnit({
223
245
  Both `VpnClient` and `VpnServer` extend `EventEmitter`:
224
246
 
225
247
  ```typescript
226
- client.on('status', (status) => { /* IVpnStatus */ });
227
- client.on('error', (err) => { /* { message, code? } */ });
228
248
  client.on('exit', ({ code, signal }) => { /* daemon exited */ });
229
249
  client.on('reconnected', () => { /* socket reconnected */ });
230
250
 
@@ -232,13 +252,84 @@ server.on('client-connected', (info) => { /* IVpnClientInfo */ });
232
252
  server.on('client-disconnected', ({ clientId, reason }) => { /* ... */ });
233
253
  ```
234
254
 
255
+ ## 📊 QoS System
256
+
257
+ The Rust daemon includes a full QoS stack that operates on decrypted IP packets:
258
+
259
+ ### Adaptive Keepalive
260
+
261
+ The keepalive system automatically adjusts its interval based on connection quality:
262
+
263
+ | Link Health | Keepalive Interval | Triggered When |
264
+ |-------------|-------------------|----------------|
265
+ | 🟢 Healthy | 60s | Jitter < 30ms, loss < 2%, no timeouts |
266
+ | 🟡 Degraded | 30s | Jitter > 50ms, loss > 5%, or 1+ timeout |
267
+ | 🔴 Critical | 10s | Loss > 20% or 2+ consecutive timeouts |
268
+
269
+ State transitions include hysteresis (3 consecutive good checks to upgrade, 2 to recover) to prevent flapping. Dead peer detection fires after 3 consecutive timeouts in Critical state.
270
+
271
+ ### Packet Classification
272
+
273
+ IP packets are classified into three priority levels by inspecting headers (no deep packet inspection):
274
+
275
+ | Priority | Traffic |
276
+ |----------|---------|
277
+ | **High** | ICMP, DNS (port 53), SSH (port 22), small packets (< 128 bytes) |
278
+ | **Normal** | Everything else |
279
+ | **Low** | Bulk flows exceeding 1 MB within a 60s window |
280
+
281
+ Priority channels drain with biased `tokio::select!` — high-priority packets always go first.
282
+
283
+ ### Smart Packet Dropping
284
+
285
+ Under backpressure, packets are dropped intelligently:
286
+
287
+ 1. **Low** queue full → drop silently
288
+ 2. **Normal** queue full → drop
289
+ 3. **High** queue full → wait 5ms, then drop as last resort
290
+
291
+ Drop statistics are tracked per priority level and exposed via telemetry.
292
+
293
+ ### Per-Client Rate Limiting
294
+
295
+ Token bucket algorithm with byte granularity:
296
+
297
+ ```typescript
298
+ // Set: 10 MB/s sustained, 20 MB burst
299
+ await server.setClientRateLimit('client-id', 10_000_000, 20_000_000);
300
+
301
+ // Check drops via telemetry
302
+ const t = await server.getClientTelemetry('client-id');
303
+ console.log(`Dropped: ${t.packetsDropped} packets, ${t.bytesDropped} bytes`);
304
+
305
+ // Remove limit
306
+ await server.removeClientRateLimit('client-id');
307
+ ```
308
+
309
+ Rate limits can be changed live without disconnecting the client.
310
+
311
+ ### Path MTU
312
+
313
+ Tunnel overhead is calculated precisely:
314
+
315
+ | Layer | Bytes |
316
+ |-------|-------|
317
+ | IP header | 20 |
318
+ | TCP header (with timestamps) | 32 |
319
+ | WebSocket framing | 6 |
320
+ | VPN frame header | 5 |
321
+ | Noise AEAD tag | 16 |
322
+ | **Total overhead** | **79** |
323
+
324
+ For a standard 1500-byte Ethernet link, effective TUN MTU = **1421 bytes**. The default TUN MTU of 1420 is conservative and correct. Oversized packets get an ICMP "Fragmentation Needed" (Type 3, Code 4) written back into the TUN, so the source TCP adjusts its MSS automatically.
325
+
235
326
  ## 🔐 Security Model
236
327
 
237
328
  The VPN uses a **Noise NK** handshake pattern:
238
329
 
239
330
  1. **NK** = client does **N**ot authenticate, but **K**nows the server's static public key
240
- 2. The client generates an ephemeral keypair, performs `e, es` (Diffie-Hellman with server's static key)
241
- 3. Server responds with `e, ee` (Diffie-Hellman with both ephemeral keys)
331
+ 2. The client generates an ephemeral keypair, performs `e, es` (DH with server's static key)
332
+ 3. Server responds with `e, ee` (DH with both ephemeral keys)
242
333
  4. Result: forward-secret transport keys derived from both DH operations
243
334
 
244
335
  Post-handshake, all IP packets are encrypted with **XChaCha20-Poly1305**:
@@ -261,8 +352,8 @@ Inside the WebSocket tunnel, packets use a simple binary framing:
261
352
  | `HandshakeInit` | `0x01` | Client → Server handshake |
262
353
  | `HandshakeResp` | `0x02` | Server → Client handshake |
263
354
  | `IpPacket` | `0x10` | Encrypted IP packet |
264
- | `Keepalive` | `0x20` | App-level ping |
265
- | `KeepaliveAck` | `0x21` | App-level pong |
355
+ | `Keepalive` | `0x20` | App-level ping (8-byte timestamp payload) |
356
+ | `KeepaliveAck` | `0x21` | App-level pong (echoes timestamp for RTT) |
266
357
  | `SessionResume` | `0x30` | Resume a dropped session |
267
358
  | `SessionResumeOk` | `0x31` | Resume accepted |
268
359
  | `SessionResumeErr` | `0x32` | Resume rejected |
@@ -270,8 +361,6 @@ Inside the WebSocket tunnel, packets use a simple binary framing:
270
361
 
271
362
  ## 🛠️ Rust Daemon CLI
272
363
 
273
- The Rust binary supports several modes:
274
-
275
364
  ```bash
276
365
  # Development: stdio management (JSON lines on stdin/stdout)
277
366
  smartvpn_daemon --management --mode client
@@ -290,16 +379,14 @@ smartvpn_daemon --generate-keypair
290
379
  # Install dependencies
291
380
  pnpm install
292
381
 
293
- # Build TypeScript + cross-compile Rust
382
+ # Build TypeScript + cross-compile Rust (amd64 + arm64)
294
383
  pnpm build
295
384
 
296
385
  # Build Rust only (debug)
297
386
  cd rust && cargo build
298
387
 
299
- # Run Rust tests
388
+ # Run all tests (71 Rust + 32 TypeScript)
300
389
  cd rust && cargo test
301
-
302
- # Run TypeScript tests
303
390
  pnpm test
304
391
  ```
305
392
 
@@ -323,25 +410,27 @@ type TVpnTransportOptions =
323
410
 
324
411
  // Client config
325
412
  interface IVpnClientConfig {
326
- serverUrl: string; // e.g. 'wss://vpn.example.com/tunnel'
327
- serverPublicKey: string; // base64-encoded Noise static key
413
+ serverUrl: string;
414
+ serverPublicKey: string;
328
415
  dns?: string[];
329
- mtu?: number; // default: 1420
330
- keepaliveIntervalSecs?: number; // default: 30
416
+ mtu?: number;
417
+ keepaliveIntervalSecs?: number;
331
418
  }
332
419
 
333
420
  // Server config
334
421
  interface IVpnServerConfig {
335
- listenAddr: string; // e.g. '0.0.0.0:443'
336
- privateKey: string; // base64 Noise static private key
337
- publicKey: string; // base64 Noise static public key
338
- subnet: string; // e.g. '10.8.0.0/24'
422
+ listenAddr: string;
423
+ privateKey: string;
424
+ publicKey: string;
425
+ subnet: string;
339
426
  tlsCert?: string;
340
427
  tlsKey?: string;
341
428
  dns?: string[];
342
429
  mtu?: number;
343
430
  keepaliveIntervalSecs?: number;
344
431
  enableNat?: boolean;
432
+ defaultRateLimitBytesPerSec?: number;
433
+ defaultBurstBytes?: number;
345
434
  }
346
435
 
347
436
  // Status
@@ -365,6 +454,7 @@ interface IVpnStatistics {
365
454
  keepalivesSent: number;
366
455
  keepalivesReceived: number;
367
456
  uptimeSeconds: number;
457
+ quality?: IVpnConnectionQuality;
368
458
  }
369
459
 
370
460
  interface IVpnServerStatistics extends IVpnStatistics {
@@ -372,12 +462,57 @@ interface IVpnServerStatistics extends IVpnStatistics {
372
462
  totalConnections: number;
373
463
  }
374
464
 
465
+ // Connection quality (QoS)
466
+ type TVpnLinkHealth = 'healthy' | 'degraded' | 'critical';
467
+
468
+ interface IVpnConnectionQuality {
469
+ srttMs: number;
470
+ jitterMs: number;
471
+ minRttMs: number;
472
+ maxRttMs: number;
473
+ lossRatio: number;
474
+ consecutiveTimeouts: number;
475
+ linkHealth: TVpnLinkHealth;
476
+ currentKeepaliveIntervalSecs: number;
477
+ }
478
+
479
+ // MTU info
480
+ interface IVpnMtuInfo {
481
+ tunMtu: number;
482
+ effectiveMtu: number;
483
+ linkMtu: number;
484
+ overheadBytes: number;
485
+ oversizedPacketsDropped: number;
486
+ icmpTooBigSent: number;
487
+ }
488
+
489
+ // Client info (with QoS fields)
375
490
  interface IVpnClientInfo {
376
491
  clientId: string;
377
492
  assignedIp: string;
378
493
  connectedSince: string;
379
494
  bytesSent: number;
380
495
  bytesReceived: number;
496
+ packetsDropped: number;
497
+ bytesDropped: number;
498
+ lastKeepaliveAt?: string;
499
+ keepalivesReceived: number;
500
+ rateLimitBytesPerSec?: number;
501
+ burstBytes?: number;
502
+ }
503
+
504
+ // Per-client telemetry
505
+ interface IVpnClientTelemetry {
506
+ clientId: string;
507
+ assignedIp: string;
508
+ lastKeepaliveAt?: string;
509
+ keepalivesReceived: number;
510
+ packetsDropped: number;
511
+ bytesDropped: number;
512
+ bytesReceived: number;
513
+ bytesSent: number;
514
+ rateLimitBytesPerSec?: number;
515
+ burstBytes?: number;
381
516
  }
382
517
 
383
518
  interface IVpnKeypair {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartvpn',
6
- version: '1.1.0',
6
+ version: '1.3.0',
7
7
  description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
8
8
  }