feat(auth,client-registry): add Noise IK client authentication with managed client registry and per-client ACL controls

This commit is contained in:
2026-03-29 17:04:27 +00:00
parent 187a69028b
commit 01a0d8b9f4
20 changed files with 1930 additions and 897 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartvpn',
version: '1.7.0',
version: '1.8.0',
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
}

View File

@@ -51,6 +51,15 @@ export class VpnConfig {
if (!config.serverPublicKey) {
throw new Error('VpnConfig: serverPublicKey is required');
}
// Noise IK requires client keypair
if (!config.clientPrivateKey) {
throw new Error('VpnConfig: clientPrivateKey is required for Noise IK authentication');
}
VpnConfig.validateBase64Key(config.clientPrivateKey, 'clientPrivateKey');
if (!config.clientPublicKey) {
throw new Error('VpnConfig: clientPublicKey is required for Noise IK authentication');
}
VpnConfig.validateBase64Key(config.clientPublicKey, 'clientPublicKey');
}
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
throw new Error('VpnConfig: mtu must be between 576 and 65535');
@@ -116,6 +125,18 @@ export class VpnConfig {
if (!VpnConfig.isValidSubnet(config.subnet)) {
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
}
// Validate client entries if provided
if (config.clients) {
for (const client of config.clients) {
if (!client.clientId) {
throw new Error('VpnConfig: client entry must have a clientId');
}
if (!client.publicKey) {
throw new Error(`VpnConfig: client '${client.clientId}' must have a publicKey`);
}
VpnConfig.validateBase64Key(client.publicKey, `client '${client.clientId}' publicKey`);
}
}
}
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
throw new Error('VpnConfig: mtu must be between 576 and 65535');

View File

@@ -10,6 +10,8 @@ import type {
IVpnClientTelemetry,
IWgPeerConfig,
IWgPeerInfo,
IClientEntry,
IClientConfigBundle,
TVpnServerCommands,
} from './smartvpn.interfaces.js';
@@ -152,6 +154,81 @@ export class VpnServer extends plugins.events.EventEmitter {
return result.peers;
}
// ── Client Registry (Hub) Methods ─────────────────────────────────────
/**
* Create a new client. Generates keypairs, assigns IP, returns full config bundle.
* The secrets (private keys) are only returned at creation time.
*/
public async createClient(opts: Partial<IClientEntry>): Promise<IClientConfigBundle> {
return this.bridge.sendCommand('createClient', { client: opts });
}
/**
* Remove a registered client (also disconnects if connected).
*/
public async removeClient(clientId: string): Promise<void> {
await this.bridge.sendCommand('removeClient', { clientId });
}
/**
* Get a registered client by ID.
*/
public async getClient(clientId: string): Promise<IClientEntry> {
return this.bridge.sendCommand('getClient', { clientId });
}
/**
* List all registered clients.
*/
public async listRegisteredClients(): Promise<IClientEntry[]> {
const result = await this.bridge.sendCommand('listRegisteredClients', {} as Record<string, never>);
return result.clients;
}
/**
* Update a registered client's fields (ACLs, tags, description, etc.).
*/
public async updateClient(clientId: string, update: Partial<IClientEntry>): Promise<void> {
await this.bridge.sendCommand('updateClient', { clientId, update });
}
/**
* Enable a previously disabled client.
*/
public async enableClient(clientId: string): Promise<void> {
await this.bridge.sendCommand('enableClient', { clientId });
}
/**
* Disable a client (also disconnects if connected).
*/
public async disableClient(clientId: string): Promise<void> {
await this.bridge.sendCommand('disableClient', { clientId });
}
/**
* Rotate a client's keys. Returns a new config bundle with fresh keypairs.
*/
public async rotateClientKey(clientId: string): Promise<IClientConfigBundle> {
return this.bridge.sendCommand('rotateClientKey', { clientId });
}
/**
* Export a client config (without secrets) in the specified format.
*/
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
const result = await this.bridge.sendCommand('exportClientConfig', { clientId, format });
return result.config;
}
/**
* Generate a standalone Noise IK keypair (not tied to a client).
*/
public async generateClientKeypair(): Promise<IVpnKeypair> {
return this.bridge.sendCommand('generateClientKeypair', {} as Record<string, never>);
}
/**
* Stop the daemon bridge.
*/

View File

@@ -24,8 +24,12 @@ export type TVpnTransportOptions = IVpnTransportStdio | IVpnTransportSocket;
export interface IVpnClientConfig {
/** Server WebSocket URL, e.g. wss://vpn.example.com/tunnel */
serverUrl: string;
/** Server's static public key (base64) for Noise NK handshake */
/** Server's static public key (base64) for Noise IK handshake */
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;
/** Optional DNS servers to use while connected */
dns?: string[];
/** Optional MTU for the TUN device */
@@ -96,6 +100,8 @@ export interface IVpnServerConfig {
wgListenPort?: number;
/** WireGuard: configured peers */
wgPeers?: IWgPeerConfig[];
/** Pre-registered clients for Noise IK authentication */
clients?: IClientEntry[];
}
export interface IVpnServerOptions {
@@ -146,6 +152,10 @@ export interface IVpnClientInfo {
keepalivesReceived: number;
rateLimitBytesPerSec?: number;
burstBytes?: number;
/** Client's authenticated Noise IK public key (base64) */
authenticatedKey: string;
/** Registered client ID from the client registry */
registeredClientId: string;
}
export interface IVpnServerStatistics extends IVpnStatistics {
@@ -205,6 +215,84 @@ export interface IVpnClientTelemetry {
burstBytes?: number;
}
// ============================================================================
// Client Registry (Hub) types — aligned with SmartProxy IRouteSecurity pattern
// ============================================================================
/** Per-client rate limiting. */
export interface IClientRateLimit {
/** Max throughput in bytes/sec */
bytesPerSec: number;
/** Burst allowance in bytes */
burstBytes: number;
}
/**
* Per-client security settings.
* Mirrors SmartProxy's IRouteSecurity: ipAllowList/ipBlockList naming + deny-overrides-allow.
* Adds VPN-specific destination filtering.
*/
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). */
ipAllowList?: string[];
/** Source IPs blocked — overrides ipAllowList (deny wins). */
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;
}
/**
* Server-side client definition — the central config object for the Hub.
* Naming and structure aligned with SmartProxy's IRouteConfig / IRouteSecurity.
*/
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 settings (ACLs, rate limits) */
security?: IClientSecurity;
/** Traffic priority (lower = higher priority, default: 100) */
priority?: number;
/** 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;
/** Assigned VPN IP address (set by server) */
assignedIp?: string;
}
/**
* Complete client config bundle — returned by createClient() and rotateClientKey().
* Contains everything the client needs to connect.
*/
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;
};
}
// ============================================================================
// WireGuard-specific types
// ============================================================================
@@ -262,6 +350,17 @@ export type TVpnServerCommands = {
addWgPeer: { params: { peer: IWgPeerConfig }; result: void };
removeWgPeer: { params: { publicKey: string }; result: void };
listWgPeers: { params: Record<string, never>; result: { peers: IWgPeerInfo[] } };
// Client Registry (Hub) commands
createClient: { params: { client: Partial<IClientEntry> }; result: IClientConfigBundle };
removeClient: { params: { clientId: string }; result: void };
getClient: { params: { clientId: string }; result: IClientEntry };
listRegisteredClients: { params: Record<string, never>; result: { clients: IClientEntry[] } };
updateClient: { params: { clientId: string; update: Partial<IClientEntry> }; result: void };
enableClient: { params: { clientId: string }; result: void };
disableClient: { params: { clientId: string }; result: void };
rotateClientKey: { params: { clientId: string }; result: IClientConfigBundle };
exportClientConfig: { params: { clientId: string; format: 'smartvpn' | 'wireguard' }; result: { config: string } };
generateClientKeypair: { params: Record<string, never>; result: IVpnKeypair };
};
// ============================================================================