enterprise readiness step 1
This commit is contained in:
417
readme.plan.md
Normal file
417
readme.plan.md
Normal file
@@ -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<IClientEntry> }` | `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<IClientEntry> }` | `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<String>,
|
||||
pub allowed_from: Option<Vec<String>>,
|
||||
pub allowed_to: Option<Vec<String>>,
|
||||
pub not_allowed_from: Option<Vec<String>>,
|
||||
pub not_allowed_to: Option<Vec<String>>,
|
||||
pub rate_limit_bytes_per_sec: Option<u64>,
|
||||
pub burst_bytes: Option<u64>,
|
||||
pub priority: Option<u32>,
|
||||
pub enabled: Option<bool>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub description: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub assigned_ip: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ClientRegistry {
|
||||
entries: HashMap<String, ClientEntry>, // keyed by clientId
|
||||
key_index: HashMap<String, String>, // 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<Vec<ClientEntry>>` to `ServerConfig`
|
||||
- Add `client_registry: RwLock<ClientRegistry>` 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 |
|
||||
Reference in New Issue
Block a user