# 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 |