Files
smartvpn/readme.plan.md

17 KiB

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. Naming and structure are aligned with SmartProxy's IRouteConfig / IRouteSecurity patterns.

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;

  // ── Security (aligned with SmartProxy IRouteSecurity pattern) ─────────

  security?: IClientSecurity;

  // ── QoS ────────────────────────────────────────────────────────────────

  /** Traffic priority (lower = higher priority, default: 100) */
  priority?: number;

  // ── Metadata (aligned with SmartProxy IRouteConfig pattern) ────────────

  /** 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;
}

/**
 * Security settings per client — mirrors SmartProxy's IRouteSecurity structure.
 * Uses the same ipAllowList/ipBlockList naming convention.
 * Adds VPN-specific destination filtering (destinationAllowList/destinationBlockList).
 */
export interface IClientSecurity {
  /** Source IPs/CIDRs the client may connect FROM (empty = any).
   *  Supports: exact IP, CIDR, wildcard (192.168.1.*), ranges (1.1.1.1-1.1.1.5).
   *  Same format as SmartProxy's ipAllowList. */
  ipAllowList?: string[];
  /** Source IPs blocked — overrides ipAllowList (deny wins).
   *  Same format as SmartProxy's ipBlockList. */
  ipBlockList?: string[];
  /** Destination IPs/CIDRs the client may reach through the VPN (empty = all) */
  destinationAllowList?: string[];
  /** Destination IPs blocked — overrides destinationAllowList (deny wins) */
  destinationBlockList?: string[];
  /** Max concurrent connections from this client */
  maxConnections?: number;
  /** Per-client rate limiting */
  rateLimit?: IClientRateLimit;
}

export interface IClientRateLimit {
  /** Max throughput in bytes/sec */
  bytesPerSec: number;
  /** Burst allowance in bytes */
  burstBytes: number;
}

SmartProxy Alignment Notes

Pattern SmartProxy SmartVPN
ACL naming ipAllowList / ipBlockList Same — ipAllowList / ipBlockList
Security grouping security: IRouteSecurity sub-object Same — security: IClientSecurity sub-object
Rate limit structure rateLimit: IRouteRateLimit object Same pattern — rateLimit: IClientRateLimit object
IP format support Exact, CIDR, wildcard, ranges Same formats
Metadata fields priority, tags, enabled, description Same fields
ACL evaluation Block-first, then allow-list Same — deny overrides allow

ACL Evaluation Order

1. Check ipBlockList / destinationBlockList first (explicit deny wins)
2. If denied, DROP
3. Check ipAllowList / destinationAllowList (explicit allow)
4. If ipAllowList is empty → allow any source
5. If destinationAllowList 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
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)

exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): IVpnClientConfig | string

Updated IVpnServerConfig

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

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;

pub struct ClientEntry {
    pub client_id: String,
    pub public_key: String,
    pub wg_public_key: Option<String>,
    pub security: Option<ClientSecurity>,
    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>,
}

/// Mirrors IClientSecurity — aligned with SmartProxy's IRouteSecurity
pub struct ClientSecurity {
    pub ip_allow_list: Option<Vec<String>>,
    pub ip_block_list: Option<Vec<String>>,
    pub destination_allow_list: Option<Vec<String>>,
    pub destination_block_list: Option<Vec<String>>,
    pub max_connections: Option<u32>,
    pub rate_limit: Option<ClientRateLimit>,
}

pub struct ClientRateLimit {
    pub bytes_per_sec: u64,
    pub burst_bytes: u64,
}

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;

/// IP matching supports: exact, CIDR, wildcard, ranges — same as SmartProxy's IpMatcher
pub fn check_acl(security: &ClientSecurity, src_ip: Ipv4Addr, dst_ip: Ipv4Addr) -> AclResult {
    // 1. Check ip_block_list / destination_block_list (deny overrides)
    // 2. Check ip_allow_list / destination_allow_list (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:

    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 rotationrotateClientKey() 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