From 187a69028b98d251049feebeb98c5695b359fbbc Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 29 Mar 2026 15:54:39 +0000 Subject: [PATCH] enterprise readiness step 1 --- readme.plan.md | 417 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 readme.plan.md diff --git a/readme.plan.md b/readme.plan.md new file mode 100644 index 0000000..6c8e012 --- /dev/null +++ b/readme.plan.md @@ -0,0 +1,417 @@ +# Enterprise Auth & Client Management for SmartVPN + +## Context + +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: + +1. **Per-client cryptographic authentication** (Noise IK handshake) +2. **Rich client definitions** with ACLs, rate limits, and priority +3. **Hub-generated configs** — server generates typed SmartVPN client configs AND WireGuard .conf files from the same client definition +4. **Top-notch DX** — one `createClient()` call gives you everything + +**This is a breaking change.** No backward compatibility with the old NK anonymous mode. + +--- + +## Design Overview + +### The Hub Model + +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. + +``` +Hub (Server) + └── Client Registry + ├── "alice-laptop" → SmartVPN config OR WireGuard .conf + ├── "bob-phone" → SmartVPN config OR WireGuard .conf + └── "office-gw" → SmartVPN config OR WireGuard .conf +``` + +### Authentication: NK → IK (Breaking Change) + +**Old (removed):** `Noise_NK_25519_ChaChaPoly_BLAKE2s` — client is anonymous +**New (always):** `Noise_IK_25519_ChaChaPoly_BLAKE2s` — client presents its static key during handshake + +IK is a 2-message handshake (same count as NK), so **the frame protocol stays identical**. Changes: +- `create_initiator()` now requires `(client_private_key, server_public_key)` — always +- `create_responder()` remains `(server_private_key)` — but now uses IK pattern +- After handshake, server extracts client's public key via `get_remote_static()` and verifies against registry +- Old NK functions are replaced, not kept alongside + +**Every client must have a keypair. Every server must have a client registry.** + +--- + +## Core Interface: `IClientEntry` + +This is the server-side client definition — the central config object: + +```typescript +export interface IClientEntry { + /** Human-readable client ID (e.g. "alice-laptop") */ + clientId: string; + + /** Client's Noise IK public key (base64) — for SmartVPN native transport */ + publicKey: string; + /** Client's WireGuard public key (base64) — for WireGuard transport */ + wgPublicKey?: string; + + // ── Network ACLs ────────────────────────────────────────────────────── + + /** Source IPs/CIDRs the client may connect FROM (empty = any) */ + allowedFrom?: string[]; + /** Destination IPs/CIDRs the client is allowed to reach (empty = all) */ + allowedTo?: string[]; + /** Blocklist: source IPs denied — overrides allowedFrom */ + notAllowedFrom?: string[]; + /** Blocklist: destination IPs denied — overrides allowedTo */ + notAllowedTo?: string[]; + + // ── QoS ──────────────────────────────────────────────────────────────── + + /** Rate limit in bytes/sec (omit = server default or unlimited) */ + rateLimitBytesPerSec?: number; + /** Burst size in bytes */ + burstBytes?: number; + /** Traffic priority (lower = higher priority, default: 100) */ + priority?: number; + + // ── Metadata ─────────────────────────────────────────────────────────── + + /** Whether this client is enabled (default: true) */ + enabled?: boolean; + /** Tags for grouping (e.g. ["engineering", "office"]) */ + tags?: string[]; + /** Optional description */ + description?: string; + /** Optional expiry (ISO 8601 timestamp, omit = never expires) */ + expiresAt?: string; +} +``` + +### ACL Evaluation Order + +``` +1. Check notAllowedFrom / notAllowedTo first (explicit deny wins) +2. If denied, DROP +3. Check allowedFrom / allowedTo (explicit allow) +4. If allowedFrom is empty → allow any source +5. If allowedTo is empty → allow all destinations +``` + +--- + +## Hub Config Generation + +### `createClient()` — The One-Call DX + +When the hub creates a client, it: +1. Generates a Noise IK keypair for the client +2. Generates a WireGuard keypair for the client +3. Allocates a VPN IP address +4. Stores the `IClientEntry` in the registry +5. Returns a **complete config bundle** with everything the client needs + +```typescript +export interface IClientConfigBundle { + /** The server-side client entry */ + entry: IClientEntry; + /** Ready-to-use SmartVPN client config (typed object) */ + smartvpnConfig: IVpnClientConfig; + /** Ready-to-use WireGuard .conf file content (string) */ + wireguardConfig: string; + /** Client's private keys (ONLY returned at creation time, not stored server-side) */ + secrets: { + noisePrivateKey: string; + wgPrivateKey: string; + }; +} +``` + +The `secrets` are returned **only at creation time** — the server stores only public keys. + +### `exportClientConfig()` — Re-export (without secrets) + +```typescript +exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): IVpnClientConfig | string +``` + +--- + +## Updated `IVpnServerConfig` + +```typescript +export interface IVpnServerConfig { + listenAddr: string; + tlsCert?: string; + tlsKey?: string; + privateKey: string; // Server's Noise static private key (base64) + publicKey: string; // Server's Noise static public key (base64) + subnet: string; + dns?: string[]; + mtu?: number; + keepaliveIntervalSecs?: number; + enableNat?: boolean; + defaultRateLimitBytesPerSec?: number; + defaultBurstBytes?: number; + transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard'; + quicListenAddr?: string; + quicIdleTimeoutSecs?: number; + wgListenPort?: number; + wgPeers?: IWgPeerConfig[]; // Keep for raw WG mode + + /** Pre-registered clients — REQUIRED for SmartVPN native transport */ + clients: IClientEntry[]; +} +``` + +Note: `clients` is now **required** (not optional), and there is no `authMode` field — IK is always used. + +--- + +## Updated `IVpnClientConfig` + +```typescript +export interface IVpnClientConfig { + serverUrl: string; + serverPublicKey: string; + /** Client's Noise IK private key (base64) — REQUIRED for SmartVPN native transport */ + clientPrivateKey: string; + /** Client's Noise IK public key (base64) — for reference/display */ + clientPublicKey: string; + dns?: string[]; + mtu?: number; + keepaliveIntervalSecs?: number; + transport?: 'auto' | 'websocket' | 'quic' | 'wireguard'; + serverCertHash?: string; + // WireGuard fields unchanged... + wgPrivateKey?: string; + wgAddress?: string; + wgAddressPrefix?: number; + wgPresharedKey?: string; + wgPersistentKeepalive?: number; + wgEndpoint?: string; + wgAllowedIps?: string[]; +} +``` + +Note: `clientPrivateKey` and `clientPublicKey` are now **required** (not optional) for non-WireGuard transports. + +--- + +## New IPC Commands + +Added to `TVpnServerCommands`: + +| Command | Params | Result | Description | +|---------|--------|--------|-------------| +| `createClient` | `{ client: Partial }` | `IClientConfigBundle` | Create client, generate keypairs, assign IP, return full config bundle | +| `removeClient` | `{ clientId: string }` | `void` | Remove from registry + disconnect if connected | +| `getClient` | `{ clientId: string }` | `IClientEntry` | Get a single client entry | +| `listRegisteredClients` | `{}` | `{ clients: IClientEntry[] }` | List all registered clients | +| `updateClient` | `{ clientId: string, update: Partial }` | `void` | Update ACLs, rate limits, tags, etc. | +| `enableClient` | `{ clientId: string }` | `void` | Enable a disabled client | +| `disableClient` | `{ clientId: string }` | `void` | Disable (but don't delete) | +| `rotateClientKey` | `{ clientId: string }` | `IClientConfigBundle` | New keypairs, return fresh config bundle | +| `exportClientConfig` | `{ clientId: string, format: 'smartvpn' \| 'wireguard' }` | `{ config: string }` | Re-export config (without secrets) | +| `generateClientKeypair` | `{}` | `IVpnKeypair` | Generate a standalone Noise IK keypair | + +--- + +## Implementation Plan + +### Phase 1: Rust — Crypto (Replace NK with IK) + +**File: `rust/src/crypto.rs`** + +- Change `NOISE_PATTERN` from NK to IK: `"Noise_IK_25519_ChaChaPoly_BLAKE2s"` +- Replace `create_initiator(server_public_key)` → `create_initiator(client_private_key, server_public_key)` +- `create_responder(private_key)` stays the same signature (IK responder only needs its own key) +- After handshake, `get_remote_static()` on the responder returns the client's public key +- Update `perform_handshake()` to pass client keypair +- Update all tests + +### Phase 2: Rust — Client Registry module + +**New file: `rust/src/client_registry.rs`** +**Modify: `rust/src/lib.rs`** — add `pub mod client_registry;` + +```rust +pub struct ClientEntry { + pub client_id: String, + pub public_key: String, + pub wg_public_key: Option, + pub allowed_from: Option>, + pub allowed_to: Option>, + pub not_allowed_from: Option>, + pub not_allowed_to: Option>, + pub rate_limit_bytes_per_sec: Option, + pub burst_bytes: Option, + pub priority: Option, + pub enabled: Option, + pub tags: Option>, + pub description: Option, + pub expires_at: Option, + pub assigned_ip: Option, +} + +pub struct ClientRegistry { + entries: HashMap, // keyed by clientId + key_index: HashMap, // publicKey → clientId (fast lookup) +} +``` + +Methods: `add`, `remove`, `get_by_id`, `get_by_key`, `update`, `list`, `is_authorized` (enabled + not expired + key exists), `rotate_key`. + +### Phase 3: Rust — ACL enforcement module + +**New file: `rust/src/acl.rs`** +**Modify: `rust/src/lib.rs`** — add `pub mod acl;` + +```rust +pub fn check_acl(entry: &ClientEntry, src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> AclResult { + // 1. Check notAllowedFrom/notAllowedTo (deny overrides) + // 2. Check allowedFrom/allowedTo (explicit allow) + // 3. Empty list = allow all +} +``` + +Called in `server.rs` packet loop after decryption, before forwarding. + +### Phase 4: Rust — Server changes + +**File: `rust/src/server.rs`** + +- Add `clients: Option>` to `ServerConfig` +- Add `client_registry: RwLock` to `ServerState` (no `auth_mode` — always IK) +- Modify `handle_client_connection()`: + - Always use `create_responder()` (now IK pattern) + - Call `get_remote_static()` **before** `into_transport_mode()` to get client's public key + - Verify against registry — reject unauthorized clients with Disconnect frame + - Use registry entry for rate limits (overrides server defaults) + - In packet loop: call `acl::check_acl()` on decrypted packets +- Add `ClientInfo.authenticated_key: String` and `ClientInfo.registered_client_id: String` (no longer optional) +- Add methods: `create_client()`, `remove_client()`, `update_client()`, `list_registered_clients()`, `rotate_client_key()`, `export_client_config()` + +### Phase 5: Rust — Client changes + +**File: `rust/src/client.rs`** + +- Add `client_private_key: String` to `ClientConfig` (required, not optional) +- `connect()` always uses `create_initiator(client_private_key, server_public_key)` (IK) + +### Phase 6: Rust — Management IPC handlers + +**File: `rust/src/management.rs`** + +Add handlers for all 10 new IPC commands following existing patterns. + +### Phase 7: TypeScript — Interfaces + +**File: `ts/smartvpn.interfaces.ts`** + +- Add `IClientEntry` interface +- Add `IClientConfigBundle` interface +- Update `IVpnServerConfig`: add required `clients: IClientEntry[]` +- Update `IVpnClientConfig`: add required `clientPrivateKey: string`, `clientPublicKey: string` +- Update `IVpnClientInfo`: add `authenticatedKey: string`, `registeredClientId: string` +- Add new commands to `TVpnServerCommands` + +### Phase 8: TypeScript — VpnServer class methods + +**File: `ts/smartvpn.classes.vpnserver.ts`** + +Add methods: +- `createClient(opts)` → `IClientConfigBundle` +- `removeClient(clientId)` → `void` +- `getClient(clientId)` → `IClientEntry` +- `listRegisteredClients()` → `IClientEntry[]` +- `updateClient(clientId, update)` → `void` +- `enableClient(clientId)` / `disableClient(clientId)` +- `rotateClientKey(clientId)` → `IClientConfigBundle` +- `exportClientConfig(clientId, format)` → `string | IVpnClientConfig` + +### Phase 9: TypeScript — Config validation + +**File: `ts/smartvpn.classes.vpnconfig.ts`** + +- Server config: validate `clients` present, each entry has valid `clientId` + `publicKey` +- Client config: validate `clientPrivateKey` and `clientPublicKey` present for non-WG transports +- Validate CIDRs in ACL fields + +### Phase 10: TypeScript — Hub config generation + +**File: `ts/smartvpn.classes.wgconfig.ts`** (extend existing) + +Add `generateClientConfigFromEntry(entry, serverConfig)` — produces WireGuard .conf from `IClientEntry`. + +### Phase 11: Update existing tests + +All existing tests that use the old NK handshake or old config shapes need updating: +- Rust tests in `crypto.rs`, `server.rs`, `client.rs` +- TS tests in `test/test.vpnconfig.node.ts`, `test/test.flowcontrol.node.ts`, etc. +- Tests now must provide client keypairs and client registry entries + +--- + +## DX Highlights + +1. **One call to create a client:** + ```typescript + const bundle = await server.createClient({ clientId: 'alice-laptop', tags: ['engineering'] }); + // bundle.smartvpnConfig — typed SmartVPN client config + // bundle.wireguardConfig — standard WireGuard .conf string + // bundle.secrets — private keys, shown only at creation time + ``` + +2. **Typed config objects throughout** — no raw strings or JSON blobs + +3. **Dual transport from same definition** — register once, connect via SmartVPN or WireGuard + +4. **ACLs are deny-overrides-allow** — intuitive enterprise model + +5. **Hot management** — add/remove/update/disable clients at runtime + +6. **Key rotation** — `rotateClientKey()` generates new keys and returns a fresh config bundle + +--- + +## Verification Plan + +1. **Rust unit tests:** + - `crypto.rs`: IK handshake roundtrip, `get_remote_static()` returns correct key, wrong key fails + - `client_registry.rs`: CRUD, `is_authorized` with enabled/disabled/expired + - `acl.rs`: allow/deny logic, empty lists, deny-overrides-allow + +2. **Rust integration tests:** + - Server accepts authorized client + - Server rejects unknown client public key + - ACL filtering drops packets to blocked destinations + - Runtime `createClient` / `removeClient` works + - Disabled client rejected at handshake + +3. **TypeScript tests:** + - Config validation with required client fields + - `createClient()` returns valid bundle with both formats + - `exportClientConfig()` generates valid WireGuard .conf + - Full IPC roundtrip: create client → connect → traffic → disconnect + +4. **Build:** `pnpm build` (TS + Rust), `cargo test`, `pnpm test` + +--- + +## Key Files to Modify + +| File | Changes | +|------|---------| +| `rust/src/crypto.rs` | Replace NK with IK pattern, update initiator signature | +| `rust/src/client_registry.rs` | **NEW** — client registry module | +| `rust/src/acl.rs` | **NEW** — ACL evaluation module | +| `rust/src/server.rs` | Registry integration, IK auth in handshake, ACL in packet loop | +| `rust/src/client.rs` | Required `client_private_key`, IK initiator | +| `rust/src/management.rs` | 10 new IPC command handlers | +| `rust/src/lib.rs` | Register new modules | +| `ts/smartvpn.interfaces.ts` | `IClientEntry`, `IClientConfigBundle`, updated configs & commands | +| `ts/smartvpn.classes.vpnserver.ts` | New hub methods | +| `ts/smartvpn.classes.vpnconfig.ts` | Updated validation rules | +| `ts/smartvpn.classes.wgconfig.ts` | Config generation from client entries |