@push.rocks/smartvpn 1.6.0 → 1.8.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 → 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.vpnconfig.js +22 -1
- package/dist_ts/smartvpn.classes.vpnserver.d.ts +42 -1
- package/dist_ts/smartvpn.classes.vpnserver.js +65 -1
- package/dist_ts/smartvpn.interfaces.d.ts +143 -1
- package/package.json +6 -6
- package/readme.md +246 -773
- package/readme.plan.md +459 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartvpn.classes.vpnconfig.ts +21 -0
- package/ts/smartvpn.classes.vpnserver.ts +77 -0
- package/ts/smartvpn.interfaces.ts +100 -1
package/readme.plan.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Enterprise Auth & Client Management for SmartVPN
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
SmartVPN's Noise NK mode currently allows **any client that knows the server's public key** to connect — no per-client identity or access control. The goal is to make SmartVPN enterprise-ready with:
|
|
6
|
+
|
|
7
|
+
1. **Per-client cryptographic authentication** (Noise IK handshake)
|
|
8
|
+
2. **Rich client definitions** with ACLs, rate limits, and priority
|
|
9
|
+
3. **Hub-generated configs** — server generates typed SmartVPN client configs AND WireGuard .conf files from the same client definition
|
|
10
|
+
4. **Top-notch DX** — one `createClient()` call gives you everything
|
|
11
|
+
|
|
12
|
+
**This is a breaking change.** No backward compatibility with the old NK anonymous mode.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Design Overview
|
|
17
|
+
|
|
18
|
+
### The Hub Model
|
|
19
|
+
|
|
20
|
+
The server acts as a **hub** that manages client definitions. Each client definition is the **single source of truth** from which both SmartVPN native configs and WireGuard configs are generated.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Hub (Server)
|
|
24
|
+
└── Client Registry
|
|
25
|
+
├── "alice-laptop" → SmartVPN config OR WireGuard .conf
|
|
26
|
+
├── "bob-phone" → SmartVPN config OR WireGuard .conf
|
|
27
|
+
└── "office-gw" → SmartVPN config OR WireGuard .conf
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Authentication: NK → IK (Breaking Change)
|
|
31
|
+
|
|
32
|
+
**Old (removed):** `Noise_NK_25519_ChaChaPoly_BLAKE2s` — client is anonymous
|
|
33
|
+
**New (always):** `Noise_IK_25519_ChaChaPoly_BLAKE2s` — client presents its static key during handshake
|
|
34
|
+
|
|
35
|
+
IK is a 2-message handshake (same count as NK), so **the frame protocol stays identical**. Changes:
|
|
36
|
+
- `create_initiator()` now requires `(client_private_key, server_public_key)` — always
|
|
37
|
+
- `create_responder()` remains `(server_private_key)` — but now uses IK pattern
|
|
38
|
+
- After handshake, server extracts client's public key via `get_remote_static()` and verifies against registry
|
|
39
|
+
- Old NK functions are replaced, not kept alongside
|
|
40
|
+
|
|
41
|
+
**Every client must have a keypair. Every server must have a client registry.**
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Core Interface: `IClientEntry`
|
|
46
|
+
|
|
47
|
+
This is the server-side client definition — the central config object.
|
|
48
|
+
Naming and structure are aligned with SmartProxy's `IRouteConfig` / `IRouteSecurity` patterns.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
export interface IClientEntry {
|
|
52
|
+
/** Human-readable client ID (e.g. "alice-laptop") */
|
|
53
|
+
clientId: string;
|
|
54
|
+
|
|
55
|
+
/** Client's Noise IK public key (base64) — for SmartVPN native transport */
|
|
56
|
+
publicKey: string;
|
|
57
|
+
/** Client's WireGuard public key (base64) — for WireGuard transport */
|
|
58
|
+
wgPublicKey?: string;
|
|
59
|
+
|
|
60
|
+
// ── Security (aligned with SmartProxy IRouteSecurity pattern) ─────────
|
|
61
|
+
|
|
62
|
+
security?: IClientSecurity;
|
|
63
|
+
|
|
64
|
+
// ── QoS ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/** Traffic priority (lower = higher priority, default: 100) */
|
|
67
|
+
priority?: number;
|
|
68
|
+
|
|
69
|
+
// ── Metadata (aligned with SmartProxy IRouteConfig pattern) ────────────
|
|
70
|
+
|
|
71
|
+
/** Whether this client is enabled (default: true) */
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
/** Tags for grouping (e.g. ["engineering", "office"]) */
|
|
74
|
+
tags?: string[];
|
|
75
|
+
/** Optional description */
|
|
76
|
+
description?: string;
|
|
77
|
+
/** Optional expiry (ISO 8601 timestamp, omit = never expires) */
|
|
78
|
+
expiresAt?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Security settings per client — mirrors SmartProxy's IRouteSecurity structure.
|
|
83
|
+
* Uses the same ipAllowList/ipBlockList naming convention.
|
|
84
|
+
* Adds VPN-specific destination filtering (destinationAllowList/destinationBlockList).
|
|
85
|
+
*/
|
|
86
|
+
export interface IClientSecurity {
|
|
87
|
+
/** Source IPs/CIDRs the client may connect FROM (empty = any).
|
|
88
|
+
* Supports: exact IP, CIDR, wildcard (192.168.1.*), ranges (1.1.1.1-1.1.1.5).
|
|
89
|
+
* Same format as SmartProxy's ipAllowList. */
|
|
90
|
+
ipAllowList?: string[];
|
|
91
|
+
/** Source IPs blocked — overrides ipAllowList (deny wins).
|
|
92
|
+
* Same format as SmartProxy's ipBlockList. */
|
|
93
|
+
ipBlockList?: string[];
|
|
94
|
+
/** Destination IPs/CIDRs the client may reach through the VPN (empty = all) */
|
|
95
|
+
destinationAllowList?: string[];
|
|
96
|
+
/** Destination IPs blocked — overrides destinationAllowList (deny wins) */
|
|
97
|
+
destinationBlockList?: string[];
|
|
98
|
+
/** Max concurrent connections from this client */
|
|
99
|
+
maxConnections?: number;
|
|
100
|
+
/** Per-client rate limiting */
|
|
101
|
+
rateLimit?: IClientRateLimit;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IClientRateLimit {
|
|
105
|
+
/** Max throughput in bytes/sec */
|
|
106
|
+
bytesPerSec: number;
|
|
107
|
+
/** Burst allowance in bytes */
|
|
108
|
+
burstBytes: number;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### SmartProxy Alignment Notes
|
|
113
|
+
|
|
114
|
+
| Pattern | SmartProxy | SmartVPN |
|
|
115
|
+
|---------|-----------|---------|
|
|
116
|
+
| ACL naming | `ipAllowList` / `ipBlockList` | Same — `ipAllowList` / `ipBlockList` |
|
|
117
|
+
| Security grouping | `security: IRouteSecurity` sub-object | Same — `security: IClientSecurity` sub-object |
|
|
118
|
+
| Rate limit structure | `rateLimit: IRouteRateLimit` object | Same pattern — `rateLimit: IClientRateLimit` object |
|
|
119
|
+
| IP format support | Exact, CIDR, wildcard, ranges | Same formats |
|
|
120
|
+
| Metadata fields | `priority`, `tags`, `enabled`, `description` | Same fields |
|
|
121
|
+
| ACL evaluation | Block-first, then allow-list | Same — deny overrides allow |
|
|
122
|
+
|
|
123
|
+
### ACL Evaluation Order
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
1. Check ipBlockList / destinationBlockList first (explicit deny wins)
|
|
127
|
+
2. If denied, DROP
|
|
128
|
+
3. Check ipAllowList / destinationAllowList (explicit allow)
|
|
129
|
+
4. If ipAllowList is empty → allow any source
|
|
130
|
+
5. If destinationAllowList is empty → allow all destinations
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Hub Config Generation
|
|
136
|
+
|
|
137
|
+
### `createClient()` — The One-Call DX
|
|
138
|
+
|
|
139
|
+
When the hub creates a client, it:
|
|
140
|
+
1. Generates a Noise IK keypair for the client
|
|
141
|
+
2. Generates a WireGuard keypair for the client
|
|
142
|
+
3. Allocates a VPN IP address
|
|
143
|
+
4. Stores the `IClientEntry` in the registry
|
|
144
|
+
5. Returns a **complete config bundle** with everything the client needs
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
export interface IClientConfigBundle {
|
|
148
|
+
/** The server-side client entry */
|
|
149
|
+
entry: IClientEntry;
|
|
150
|
+
/** Ready-to-use SmartVPN client config (typed object) */
|
|
151
|
+
smartvpnConfig: IVpnClientConfig;
|
|
152
|
+
/** Ready-to-use WireGuard .conf file content (string) */
|
|
153
|
+
wireguardConfig: string;
|
|
154
|
+
/** Client's private keys (ONLY returned at creation time, not stored server-side) */
|
|
155
|
+
secrets: {
|
|
156
|
+
noisePrivateKey: string;
|
|
157
|
+
wgPrivateKey: string;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `secrets` are returned **only at creation time** — the server stores only public keys.
|
|
163
|
+
|
|
164
|
+
### `exportClientConfig()` — Re-export (without secrets)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): IVpnClientConfig | string
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Updated `IVpnServerConfig`
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
export interface IVpnServerConfig {
|
|
176
|
+
listenAddr: string;
|
|
177
|
+
tlsCert?: string;
|
|
178
|
+
tlsKey?: string;
|
|
179
|
+
privateKey: string; // Server's Noise static private key (base64)
|
|
180
|
+
publicKey: string; // Server's Noise static public key (base64)
|
|
181
|
+
subnet: string;
|
|
182
|
+
dns?: string[];
|
|
183
|
+
mtu?: number;
|
|
184
|
+
keepaliveIntervalSecs?: number;
|
|
185
|
+
enableNat?: boolean;
|
|
186
|
+
defaultRateLimitBytesPerSec?: number;
|
|
187
|
+
defaultBurstBytes?: number;
|
|
188
|
+
transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard';
|
|
189
|
+
quicListenAddr?: string;
|
|
190
|
+
quicIdleTimeoutSecs?: number;
|
|
191
|
+
wgListenPort?: number;
|
|
192
|
+
wgPeers?: IWgPeerConfig[]; // Keep for raw WG mode
|
|
193
|
+
|
|
194
|
+
/** Pre-registered clients — REQUIRED for SmartVPN native transport */
|
|
195
|
+
clients: IClientEntry[];
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Note: `clients` is now **required** (not optional), and there is no `authMode` field — IK is always used.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Updated `IVpnClientConfig`
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
export interface IVpnClientConfig {
|
|
207
|
+
serverUrl: string;
|
|
208
|
+
serverPublicKey: string;
|
|
209
|
+
/** Client's Noise IK private key (base64) — REQUIRED for SmartVPN native transport */
|
|
210
|
+
clientPrivateKey: string;
|
|
211
|
+
/** Client's Noise IK public key (base64) — for reference/display */
|
|
212
|
+
clientPublicKey: string;
|
|
213
|
+
dns?: string[];
|
|
214
|
+
mtu?: number;
|
|
215
|
+
keepaliveIntervalSecs?: number;
|
|
216
|
+
transport?: 'auto' | 'websocket' | 'quic' | 'wireguard';
|
|
217
|
+
serverCertHash?: string;
|
|
218
|
+
// WireGuard fields unchanged...
|
|
219
|
+
wgPrivateKey?: string;
|
|
220
|
+
wgAddress?: string;
|
|
221
|
+
wgAddressPrefix?: number;
|
|
222
|
+
wgPresharedKey?: string;
|
|
223
|
+
wgPersistentKeepalive?: number;
|
|
224
|
+
wgEndpoint?: string;
|
|
225
|
+
wgAllowedIps?: string[];
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Note: `clientPrivateKey` and `clientPublicKey` are now **required** (not optional) for non-WireGuard transports.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## New IPC Commands
|
|
234
|
+
|
|
235
|
+
Added to `TVpnServerCommands`:
|
|
236
|
+
|
|
237
|
+
| Command | Params | Result | Description |
|
|
238
|
+
|---------|--------|--------|-------------|
|
|
239
|
+
| `createClient` | `{ client: Partial<IClientEntry> }` | `IClientConfigBundle` | Create client, generate keypairs, assign IP, return full config bundle |
|
|
240
|
+
| `removeClient` | `{ clientId: string }` | `void` | Remove from registry + disconnect if connected |
|
|
241
|
+
| `getClient` | `{ clientId: string }` | `IClientEntry` | Get a single client entry |
|
|
242
|
+
| `listRegisteredClients` | `{}` | `{ clients: IClientEntry[] }` | List all registered clients |
|
|
243
|
+
| `updateClient` | `{ clientId: string, update: Partial<IClientEntry> }` | `void` | Update ACLs, rate limits, tags, etc. |
|
|
244
|
+
| `enableClient` | `{ clientId: string }` | `void` | Enable a disabled client |
|
|
245
|
+
| `disableClient` | `{ clientId: string }` | `void` | Disable (but don't delete) |
|
|
246
|
+
| `rotateClientKey` | `{ clientId: string }` | `IClientConfigBundle` | New keypairs, return fresh config bundle |
|
|
247
|
+
| `exportClientConfig` | `{ clientId: string, format: 'smartvpn' \| 'wireguard' }` | `{ config: string }` | Re-export config (without secrets) |
|
|
248
|
+
| `generateClientKeypair` | `{}` | `IVpnKeypair` | Generate a standalone Noise IK keypair |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Implementation Plan
|
|
253
|
+
|
|
254
|
+
### Phase 1: Rust — Crypto (Replace NK with IK)
|
|
255
|
+
|
|
256
|
+
**File: `rust/src/crypto.rs`**
|
|
257
|
+
|
|
258
|
+
- Change `NOISE_PATTERN` from NK to IK: `"Noise_IK_25519_ChaChaPoly_BLAKE2s"`
|
|
259
|
+
- Replace `create_initiator(server_public_key)` → `create_initiator(client_private_key, server_public_key)`
|
|
260
|
+
- `create_responder(private_key)` stays the same signature (IK responder only needs its own key)
|
|
261
|
+
- After handshake, `get_remote_static()` on the responder returns the client's public key
|
|
262
|
+
- Update `perform_handshake()` to pass client keypair
|
|
263
|
+
- Update all tests
|
|
264
|
+
|
|
265
|
+
### Phase 2: Rust — Client Registry module
|
|
266
|
+
|
|
267
|
+
**New file: `rust/src/client_registry.rs`**
|
|
268
|
+
**Modify: `rust/src/lib.rs`** — add `pub mod client_registry;`
|
|
269
|
+
|
|
270
|
+
```rust
|
|
271
|
+
pub struct ClientEntry {
|
|
272
|
+
pub client_id: String,
|
|
273
|
+
pub public_key: String,
|
|
274
|
+
pub wg_public_key: Option<String>,
|
|
275
|
+
pub security: Option<ClientSecurity>,
|
|
276
|
+
pub priority: Option<u32>,
|
|
277
|
+
pub enabled: Option<bool>,
|
|
278
|
+
pub tags: Option<Vec<String>>,
|
|
279
|
+
pub description: Option<String>,
|
|
280
|
+
pub expires_at: Option<String>,
|
|
281
|
+
pub assigned_ip: Option<String>,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Mirrors IClientSecurity — aligned with SmartProxy's IRouteSecurity
|
|
285
|
+
pub struct ClientSecurity {
|
|
286
|
+
pub ip_allow_list: Option<Vec<String>>,
|
|
287
|
+
pub ip_block_list: Option<Vec<String>>,
|
|
288
|
+
pub destination_allow_list: Option<Vec<String>>,
|
|
289
|
+
pub destination_block_list: Option<Vec<String>>,
|
|
290
|
+
pub max_connections: Option<u32>,
|
|
291
|
+
pub rate_limit: Option<ClientRateLimit>,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
pub struct ClientRateLimit {
|
|
295
|
+
pub bytes_per_sec: u64,
|
|
296
|
+
pub burst_bytes: u64,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
pub struct ClientRegistry {
|
|
300
|
+
entries: HashMap<String, ClientEntry>, // keyed by clientId
|
|
301
|
+
key_index: HashMap<String, String>, // publicKey → clientId (fast lookup)
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Methods: `add`, `remove`, `get_by_id`, `get_by_key`, `update`, `list`, `is_authorized` (enabled + not expired + key exists), `rotate_key`.
|
|
306
|
+
|
|
307
|
+
### Phase 3: Rust — ACL enforcement module
|
|
308
|
+
|
|
309
|
+
**New file: `rust/src/acl.rs`**
|
|
310
|
+
**Modify: `rust/src/lib.rs`** — add `pub mod acl;`
|
|
311
|
+
|
|
312
|
+
```rust
|
|
313
|
+
/// IP matching supports: exact, CIDR, wildcard, ranges — same as SmartProxy's IpMatcher
|
|
314
|
+
pub fn check_acl(security: &ClientSecurity, src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> AclResult {
|
|
315
|
+
// 1. Check ip_block_list / destination_block_list (deny overrides)
|
|
316
|
+
// 2. Check ip_allow_list / destination_allow_list (explicit allow)
|
|
317
|
+
// 3. Empty list = allow all
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Called in `server.rs` packet loop after decryption, before forwarding.
|
|
322
|
+
|
|
323
|
+
### Phase 4: Rust — Server changes
|
|
324
|
+
|
|
325
|
+
**File: `rust/src/server.rs`**
|
|
326
|
+
|
|
327
|
+
- Add `clients: Option<Vec<ClientEntry>>` to `ServerConfig`
|
|
328
|
+
- Add `client_registry: RwLock<ClientRegistry>` to `ServerState` (no `auth_mode` — always IK)
|
|
329
|
+
- Modify `handle_client_connection()`:
|
|
330
|
+
- Always use `create_responder()` (now IK pattern)
|
|
331
|
+
- Call `get_remote_static()` **before** `into_transport_mode()` to get client's public key
|
|
332
|
+
- Verify against registry — reject unauthorized clients with Disconnect frame
|
|
333
|
+
- Use registry entry for rate limits (overrides server defaults)
|
|
334
|
+
- In packet loop: call `acl::check_acl()` on decrypted packets
|
|
335
|
+
- Add `ClientInfo.authenticated_key: String` and `ClientInfo.registered_client_id: String` (no longer optional)
|
|
336
|
+
- Add methods: `create_client()`, `remove_client()`, `update_client()`, `list_registered_clients()`, `rotate_client_key()`, `export_client_config()`
|
|
337
|
+
|
|
338
|
+
### Phase 5: Rust — Client changes
|
|
339
|
+
|
|
340
|
+
**File: `rust/src/client.rs`**
|
|
341
|
+
|
|
342
|
+
- Add `client_private_key: String` to `ClientConfig` (required, not optional)
|
|
343
|
+
- `connect()` always uses `create_initiator(client_private_key, server_public_key)` (IK)
|
|
344
|
+
|
|
345
|
+
### Phase 6: Rust — Management IPC handlers
|
|
346
|
+
|
|
347
|
+
**File: `rust/src/management.rs`**
|
|
348
|
+
|
|
349
|
+
Add handlers for all 10 new IPC commands following existing patterns.
|
|
350
|
+
|
|
351
|
+
### Phase 7: TypeScript — Interfaces
|
|
352
|
+
|
|
353
|
+
**File: `ts/smartvpn.interfaces.ts`**
|
|
354
|
+
|
|
355
|
+
- Add `IClientEntry` interface
|
|
356
|
+
- Add `IClientConfigBundle` interface
|
|
357
|
+
- Update `IVpnServerConfig`: add required `clients: IClientEntry[]`
|
|
358
|
+
- Update `IVpnClientConfig`: add required `clientPrivateKey: string`, `clientPublicKey: string`
|
|
359
|
+
- Update `IVpnClientInfo`: add `authenticatedKey: string`, `registeredClientId: string`
|
|
360
|
+
- Add new commands to `TVpnServerCommands`
|
|
361
|
+
|
|
362
|
+
### Phase 8: TypeScript — VpnServer class methods
|
|
363
|
+
|
|
364
|
+
**File: `ts/smartvpn.classes.vpnserver.ts`**
|
|
365
|
+
|
|
366
|
+
Add methods:
|
|
367
|
+
- `createClient(opts)` → `IClientConfigBundle`
|
|
368
|
+
- `removeClient(clientId)` → `void`
|
|
369
|
+
- `getClient(clientId)` → `IClientEntry`
|
|
370
|
+
- `listRegisteredClients()` → `IClientEntry[]`
|
|
371
|
+
- `updateClient(clientId, update)` → `void`
|
|
372
|
+
- `enableClient(clientId)` / `disableClient(clientId)`
|
|
373
|
+
- `rotateClientKey(clientId)` → `IClientConfigBundle`
|
|
374
|
+
- `exportClientConfig(clientId, format)` → `string | IVpnClientConfig`
|
|
375
|
+
|
|
376
|
+
### Phase 9: TypeScript — Config validation
|
|
377
|
+
|
|
378
|
+
**File: `ts/smartvpn.classes.vpnconfig.ts`**
|
|
379
|
+
|
|
380
|
+
- Server config: validate `clients` present, each entry has valid `clientId` + `publicKey`
|
|
381
|
+
- Client config: validate `clientPrivateKey` and `clientPublicKey` present for non-WG transports
|
|
382
|
+
- Validate CIDRs in ACL fields
|
|
383
|
+
|
|
384
|
+
### Phase 10: TypeScript — Hub config generation
|
|
385
|
+
|
|
386
|
+
**File: `ts/smartvpn.classes.wgconfig.ts`** (extend existing)
|
|
387
|
+
|
|
388
|
+
Add `generateClientConfigFromEntry(entry, serverConfig)` — produces WireGuard .conf from `IClientEntry`.
|
|
389
|
+
|
|
390
|
+
### Phase 11: Update existing tests
|
|
391
|
+
|
|
392
|
+
All existing tests that use the old NK handshake or old config shapes need updating:
|
|
393
|
+
- Rust tests in `crypto.rs`, `server.rs`, `client.rs`
|
|
394
|
+
- TS tests in `test/test.vpnconfig.node.ts`, `test/test.flowcontrol.node.ts`, etc.
|
|
395
|
+
- Tests now must provide client keypairs and client registry entries
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## DX Highlights
|
|
400
|
+
|
|
401
|
+
1. **One call to create a client:**
|
|
402
|
+
```typescript
|
|
403
|
+
const bundle = await server.createClient({ clientId: 'alice-laptop', tags: ['engineering'] });
|
|
404
|
+
// bundle.smartvpnConfig — typed SmartVPN client config
|
|
405
|
+
// bundle.wireguardConfig — standard WireGuard .conf string
|
|
406
|
+
// bundle.secrets — private keys, shown only at creation time
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
2. **Typed config objects throughout** — no raw strings or JSON blobs
|
|
410
|
+
|
|
411
|
+
3. **Dual transport from same definition** — register once, connect via SmartVPN or WireGuard
|
|
412
|
+
|
|
413
|
+
4. **ACLs are deny-overrides-allow** — intuitive enterprise model
|
|
414
|
+
|
|
415
|
+
5. **Hot management** — add/remove/update/disable clients at runtime
|
|
416
|
+
|
|
417
|
+
6. **Key rotation** — `rotateClientKey()` generates new keys and returns a fresh config bundle
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Verification Plan
|
|
422
|
+
|
|
423
|
+
1. **Rust unit tests:**
|
|
424
|
+
- `crypto.rs`: IK handshake roundtrip, `get_remote_static()` returns correct key, wrong key fails
|
|
425
|
+
- `client_registry.rs`: CRUD, `is_authorized` with enabled/disabled/expired
|
|
426
|
+
- `acl.rs`: allow/deny logic, empty lists, deny-overrides-allow
|
|
427
|
+
|
|
428
|
+
2. **Rust integration tests:**
|
|
429
|
+
- Server accepts authorized client
|
|
430
|
+
- Server rejects unknown client public key
|
|
431
|
+
- ACL filtering drops packets to blocked destinations
|
|
432
|
+
- Runtime `createClient` / `removeClient` works
|
|
433
|
+
- Disabled client rejected at handshake
|
|
434
|
+
|
|
435
|
+
3. **TypeScript tests:**
|
|
436
|
+
- Config validation with required client fields
|
|
437
|
+
- `createClient()` returns valid bundle with both formats
|
|
438
|
+
- `exportClientConfig()` generates valid WireGuard .conf
|
|
439
|
+
- Full IPC roundtrip: create client → connect → traffic → disconnect
|
|
440
|
+
|
|
441
|
+
4. **Build:** `pnpm build` (TS + Rust), `cargo test`, `pnpm test`
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Key Files to Modify
|
|
446
|
+
|
|
447
|
+
| File | Changes |
|
|
448
|
+
|------|---------|
|
|
449
|
+
| `rust/src/crypto.rs` | Replace NK with IK pattern, update initiator signature |
|
|
450
|
+
| `rust/src/client_registry.rs` | **NEW** — client registry module |
|
|
451
|
+
| `rust/src/acl.rs` | **NEW** — ACL evaluation module |
|
|
452
|
+
| `rust/src/server.rs` | Registry integration, IK auth in handshake, ACL in packet loop |
|
|
453
|
+
| `rust/src/client.rs` | Required `client_private_key`, IK initiator |
|
|
454
|
+
| `rust/src/management.rs` | 10 new IPC command handlers |
|
|
455
|
+
| `rust/src/lib.rs` | Register new modules |
|
|
456
|
+
| `ts/smartvpn.interfaces.ts` | `IClientEntry`, `IClientConfigBundle`, updated configs & commands |
|
|
457
|
+
| `ts/smartvpn.classes.vpnserver.ts` | New hub methods |
|
|
458
|
+
| `ts/smartvpn.classes.vpnconfig.ts` | Updated validation rules |
|
|
459
|
+
| `ts/smartvpn.classes.wgconfig.ts` | Config generation from client entries |
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -51,6 +51,15 @@ export class VpnConfig {
|
|
|
51
51
|
if (!config.serverPublicKey) {
|
|
52
52
|
throw new Error('VpnConfig: serverPublicKey is required');
|
|
53
53
|
}
|
|
54
|
+
// Noise IK requires client keypair
|
|
55
|
+
if (!config.clientPrivateKey) {
|
|
56
|
+
throw new Error('VpnConfig: clientPrivateKey is required for Noise IK authentication');
|
|
57
|
+
}
|
|
58
|
+
VpnConfig.validateBase64Key(config.clientPrivateKey, 'clientPrivateKey');
|
|
59
|
+
if (!config.clientPublicKey) {
|
|
60
|
+
throw new Error('VpnConfig: clientPublicKey is required for Noise IK authentication');
|
|
61
|
+
}
|
|
62
|
+
VpnConfig.validateBase64Key(config.clientPublicKey, 'clientPublicKey');
|
|
54
63
|
}
|
|
55
64
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
|
56
65
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
@@ -116,6 +125,18 @@ export class VpnConfig {
|
|
|
116
125
|
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
|
117
126
|
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
|
118
127
|
}
|
|
128
|
+
// Validate client entries if provided
|
|
129
|
+
if (config.clients) {
|
|
130
|
+
for (const client of config.clients) {
|
|
131
|
+
if (!client.clientId) {
|
|
132
|
+
throw new Error('VpnConfig: client entry must have a clientId');
|
|
133
|
+
}
|
|
134
|
+
if (!client.publicKey) {
|
|
135
|
+
throw new Error(`VpnConfig: client '${client.clientId}' must have a publicKey`);
|
|
136
|
+
}
|
|
137
|
+
VpnConfig.validateBase64Key(client.publicKey, `client '${client.clientId}' publicKey`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
119
140
|
}
|
|
120
141
|
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
|
121
142
|
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
|
@@ -10,6 +10,8 @@ import type {
|
|
|
10
10
|
IVpnClientTelemetry,
|
|
11
11
|
IWgPeerConfig,
|
|
12
12
|
IWgPeerInfo,
|
|
13
|
+
IClientEntry,
|
|
14
|
+
IClientConfigBundle,
|
|
13
15
|
TVpnServerCommands,
|
|
14
16
|
} from './smartvpn.interfaces.js';
|
|
15
17
|
|
|
@@ -152,6 +154,81 @@ export class VpnServer extends plugins.events.EventEmitter {
|
|
|
152
154
|
return result.peers;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
// ── Client Registry (Hub) Methods ─────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a new client. Generates keypairs, assigns IP, returns full config bundle.
|
|
161
|
+
* The secrets (private keys) are only returned at creation time.
|
|
162
|
+
*/
|
|
163
|
+
public async createClient(opts: Partial<IClientEntry>): Promise<IClientConfigBundle> {
|
|
164
|
+
return this.bridge.sendCommand('createClient', { client: opts });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Remove a registered client (also disconnects if connected).
|
|
169
|
+
*/
|
|
170
|
+
public async removeClient(clientId: string): Promise<void> {
|
|
171
|
+
await this.bridge.sendCommand('removeClient', { clientId });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a registered client by ID.
|
|
176
|
+
*/
|
|
177
|
+
public async getClient(clientId: string): Promise<IClientEntry> {
|
|
178
|
+
return this.bridge.sendCommand('getClient', { clientId });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* List all registered clients.
|
|
183
|
+
*/
|
|
184
|
+
public async listRegisteredClients(): Promise<IClientEntry[]> {
|
|
185
|
+
const result = await this.bridge.sendCommand('listRegisteredClients', {} as Record<string, never>);
|
|
186
|
+
return result.clients;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update a registered client's fields (ACLs, tags, description, etc.).
|
|
191
|
+
*/
|
|
192
|
+
public async updateClient(clientId: string, update: Partial<IClientEntry>): Promise<void> {
|
|
193
|
+
await this.bridge.sendCommand('updateClient', { clientId, update });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Enable a previously disabled client.
|
|
198
|
+
*/
|
|
199
|
+
public async enableClient(clientId: string): Promise<void> {
|
|
200
|
+
await this.bridge.sendCommand('enableClient', { clientId });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Disable a client (also disconnects if connected).
|
|
205
|
+
*/
|
|
206
|
+
public async disableClient(clientId: string): Promise<void> {
|
|
207
|
+
await this.bridge.sendCommand('disableClient', { clientId });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Rotate a client's keys. Returns a new config bundle with fresh keypairs.
|
|
212
|
+
*/
|
|
213
|
+
public async rotateClientKey(clientId: string): Promise<IClientConfigBundle> {
|
|
214
|
+
return this.bridge.sendCommand('rotateClientKey', { clientId });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Export a client config (without secrets) in the specified format.
|
|
219
|
+
*/
|
|
220
|
+
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
|
|
221
|
+
const result = await this.bridge.sendCommand('exportClientConfig', { clientId, format });
|
|
222
|
+
return result.config;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generate a standalone Noise IK keypair (not tied to a client).
|
|
227
|
+
*/
|
|
228
|
+
public async generateClientKeypair(): Promise<IVpnKeypair> {
|
|
229
|
+
return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
|
|
230
|
+
}
|
|
231
|
+
|
|
155
232
|
/**
|
|
156
233
|
* Stop the daemon bridge.
|
|
157
234
|
*/
|