feat(wireguard): add WireGuard transport support with management APIs and config generation
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartvpn',
|
||||
version: '1.4.1',
|
||||
version: '1.5.0',
|
||||
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon'
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export { VpnClient } from './smartvpn.classes.vpnclient.js';
|
||||
export { VpnServer } from './smartvpn.classes.vpnserver.js';
|
||||
export { VpnConfig } from './smartvpn.classes.vpnconfig.js';
|
||||
export { VpnInstaller } from './smartvpn.classes.vpninstaller.js';
|
||||
export { WgConfigGenerator } from './smartvpn.classes.wgconfig.js';
|
||||
|
||||
@@ -12,17 +12,45 @@ export class VpnConfig {
|
||||
* Validate a client config object. Throws on invalid config.
|
||||
*/
|
||||
public static validateClientConfig(config: IVpnClientConfig): void {
|
||||
if (!config.serverUrl) {
|
||||
throw new Error('VpnConfig: serverUrl is required');
|
||||
}
|
||||
// For QUIC-only transport, serverUrl is a host:port address; for WebSocket/auto it must be ws:// or wss://
|
||||
if (config.transport !== 'quic') {
|
||||
if (!config.serverUrl.startsWith('wss://') && !config.serverUrl.startsWith('ws://')) {
|
||||
throw new Error('VpnConfig: serverUrl must start with wss:// or ws:// (for WebSocket transport)');
|
||||
if (config.transport === 'wireguard') {
|
||||
// WireGuard-specific validation
|
||||
if (!config.wgPrivateKey) {
|
||||
throw new Error('VpnConfig: wgPrivateKey is required for WireGuard transport');
|
||||
}
|
||||
VpnConfig.validateBase64Key(config.wgPrivateKey, 'wgPrivateKey');
|
||||
if (!config.wgAddress) {
|
||||
throw new Error('VpnConfig: wgAddress is required for WireGuard transport');
|
||||
}
|
||||
if (!config.serverPublicKey) {
|
||||
throw new Error('VpnConfig: serverPublicKey is required for WireGuard transport');
|
||||
}
|
||||
VpnConfig.validateBase64Key(config.serverPublicKey, 'serverPublicKey');
|
||||
if (!config.wgEndpoint) {
|
||||
throw new Error('VpnConfig: wgEndpoint is required for WireGuard transport');
|
||||
}
|
||||
if (config.wgPresharedKey) {
|
||||
VpnConfig.validateBase64Key(config.wgPresharedKey, 'wgPresharedKey');
|
||||
}
|
||||
if (config.wgAllowedIps) {
|
||||
for (const cidr of config.wgAllowedIps) {
|
||||
if (!VpnConfig.isValidCidr(cidr)) {
|
||||
throw new Error(`VpnConfig: invalid allowedIp CIDR: ${cidr}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!config.serverUrl) {
|
||||
throw new Error('VpnConfig: serverUrl is required');
|
||||
}
|
||||
// For QUIC-only transport, serverUrl is a host:port address; for WebSocket/auto it must be ws:// or wss://
|
||||
if (config.transport !== 'quic') {
|
||||
if (!config.serverUrl.startsWith('wss://') && !config.serverUrl.startsWith('ws://')) {
|
||||
throw new Error('VpnConfig: serverUrl must start with wss:// or ws:// (for WebSocket transport)');
|
||||
}
|
||||
}
|
||||
if (!config.serverPublicKey) {
|
||||
throw new Error('VpnConfig: serverPublicKey is required');
|
||||
}
|
||||
}
|
||||
if (!config.serverPublicKey) {
|
||||
throw new Error('VpnConfig: serverPublicKey is required');
|
||||
}
|
||||
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
||||
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
||||
@@ -43,20 +71,51 @@ export class VpnConfig {
|
||||
* Validate a server config object. Throws on invalid config.
|
||||
*/
|
||||
public static validateServerConfig(config: IVpnServerConfig): void {
|
||||
if (!config.listenAddr) {
|
||||
throw new Error('VpnConfig: listenAddr is required');
|
||||
}
|
||||
if (!config.privateKey) {
|
||||
throw new Error('VpnConfig: privateKey is required');
|
||||
}
|
||||
if (!config.publicKey) {
|
||||
throw new Error('VpnConfig: publicKey is required');
|
||||
}
|
||||
if (!config.subnet) {
|
||||
throw new Error('VpnConfig: subnet is required');
|
||||
}
|
||||
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
||||
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
||||
if (config.transportMode === 'wireguard') {
|
||||
// WireGuard server validation
|
||||
if (!config.privateKey) {
|
||||
throw new Error('VpnConfig: privateKey is required');
|
||||
}
|
||||
VpnConfig.validateBase64Key(config.privateKey, 'privateKey');
|
||||
if (!config.wgPeers || config.wgPeers.length === 0) {
|
||||
throw new Error('VpnConfig: at least one wgPeers entry is required for WireGuard mode');
|
||||
}
|
||||
for (const peer of config.wgPeers) {
|
||||
if (!peer.publicKey) {
|
||||
throw new Error('VpnConfig: peer publicKey is required');
|
||||
}
|
||||
VpnConfig.validateBase64Key(peer.publicKey, 'peer.publicKey');
|
||||
if (!peer.allowedIps || peer.allowedIps.length === 0) {
|
||||
throw new Error('VpnConfig: peer allowedIps is required');
|
||||
}
|
||||
for (const cidr of peer.allowedIps) {
|
||||
if (!VpnConfig.isValidCidr(cidr)) {
|
||||
throw new Error(`VpnConfig: invalid peer allowedIp CIDR: ${cidr}`);
|
||||
}
|
||||
}
|
||||
if (peer.presharedKey) {
|
||||
VpnConfig.validateBase64Key(peer.presharedKey, 'peer.presharedKey');
|
||||
}
|
||||
}
|
||||
if (config.wgListenPort !== undefined && (config.wgListenPort < 1 || config.wgListenPort > 65535)) {
|
||||
throw new Error('VpnConfig: wgListenPort must be between 1 and 65535');
|
||||
}
|
||||
} else {
|
||||
if (!config.listenAddr) {
|
||||
throw new Error('VpnConfig: listenAddr is required');
|
||||
}
|
||||
if (!config.privateKey) {
|
||||
throw new Error('VpnConfig: privateKey is required');
|
||||
}
|
||||
if (!config.publicKey) {
|
||||
throw new Error('VpnConfig: publicKey is required');
|
||||
}
|
||||
if (!config.subnet) {
|
||||
throw new Error('VpnConfig: subnet is required');
|
||||
}
|
||||
if (!VpnConfig.isValidSubnet(config.subnet)) {
|
||||
throw new Error(`VpnConfig: invalid subnet: ${config.subnet}`);
|
||||
}
|
||||
}
|
||||
if (config.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
|
||||
throw new Error('VpnConfig: mtu must be between 576 and 65535');
|
||||
@@ -104,4 +163,41 @@ export class VpnConfig {
|
||||
const prefixNum = parseInt(prefix, 10);
|
||||
return !isNaN(prefixNum) && prefixNum >= 0 && prefixNum <= 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CIDR string (IPv4 or IPv6).
|
||||
*/
|
||||
private static isValidCidr(cidr: string): boolean {
|
||||
const parts = cidr.split('/');
|
||||
if (parts.length !== 2) return false;
|
||||
const prefixNum = parseInt(parts[1], 10);
|
||||
if (isNaN(prefixNum) || prefixNum < 0) return false;
|
||||
// IPv4
|
||||
if (VpnConfig.isValidIp(parts[0])) {
|
||||
return prefixNum <= 32;
|
||||
}
|
||||
// IPv6 (basic check)
|
||||
if (parts[0].includes(':')) {
|
||||
return prefixNum <= 128;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a base64-encoded 32-byte key (WireGuard X25519 format).
|
||||
*/
|
||||
private static validateBase64Key(key: string, fieldName: string): void {
|
||||
if (key.length !== 44) {
|
||||
throw new Error(`VpnConfig: ${fieldName} must be 44 characters (base64 of 32 bytes), got ${key.length}`);
|
||||
}
|
||||
try {
|
||||
const buf = Buffer.from(key, 'base64');
|
||||
if (buf.length !== 32) {
|
||||
throw new Error(`VpnConfig: ${fieldName} must decode to 32 bytes, got ${buf.length}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.startsWith('VpnConfig:')) throw e;
|
||||
throw new Error(`VpnConfig: ${fieldName} is not valid base64`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
IVpnClientInfo,
|
||||
IVpnKeypair,
|
||||
IVpnClientTelemetry,
|
||||
IWgPeerConfig,
|
||||
IWgPeerInfo,
|
||||
TVpnServerCommands,
|
||||
} from './smartvpn.interfaces.js';
|
||||
|
||||
@@ -121,6 +123,35 @@ export class VpnServer extends plugins.events.EventEmitter {
|
||||
return this.bridge.sendCommand('getClientTelemetry', { clientId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a WireGuard-compatible X25519 keypair.
|
||||
*/
|
||||
public async generateWgKeypair(): Promise<IVpnKeypair> {
|
||||
return this.bridge.sendCommand('generateWgKeypair', {} as Record<string, never>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WireGuard peer (server must be running in wireguard mode).
|
||||
*/
|
||||
public async addWgPeer(peer: IWgPeerConfig): Promise<void> {
|
||||
await this.bridge.sendCommand('addWgPeer', { peer });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a WireGuard peer by public key.
|
||||
*/
|
||||
public async removeWgPeer(publicKey: string): Promise<void> {
|
||||
await this.bridge.sendCommand('removeWgPeer', { publicKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* List WireGuard peers with stats.
|
||||
*/
|
||||
public async listWgPeers(): Promise<IWgPeerInfo[]> {
|
||||
const result = await this.bridge.sendCommand('listWgPeers', {} as Record<string, never>);
|
||||
return result.peers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the daemon bridge.
|
||||
*/
|
||||
|
||||
123
ts/smartvpn.classes.wgconfig.ts
Normal file
123
ts/smartvpn.classes.wgconfig.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { IWgPeerConfig } from './smartvpn.interfaces.js';
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard .conf file generator
|
||||
// ============================================================================
|
||||
|
||||
export interface IWgClientConfOptions {
|
||||
/** Client private key (base64) */
|
||||
privateKey: string;
|
||||
/** Client TUN address with prefix (e.g. 10.8.0.2/24) */
|
||||
address: string;
|
||||
/** DNS servers */
|
||||
dns?: string[];
|
||||
/** TUN MTU */
|
||||
mtu?: number;
|
||||
/** Server peer config */
|
||||
peer: {
|
||||
publicKey: string;
|
||||
presharedKey?: string;
|
||||
endpoint: string;
|
||||
allowedIps: string[];
|
||||
persistentKeepalive?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWgServerConfOptions {
|
||||
/** Server private key (base64) */
|
||||
privateKey: string;
|
||||
/** Server TUN address with prefix (e.g. 10.8.0.1/24) */
|
||||
address: string;
|
||||
/** UDP listen port */
|
||||
listenPort: number;
|
||||
/** DNS servers */
|
||||
dns?: string[];
|
||||
/** TUN MTU */
|
||||
mtu?: number;
|
||||
/** Enable NAT — adds PostUp/PostDown iptables rules */
|
||||
enableNat?: boolean;
|
||||
/** Network interface for NAT (e.g. eth0). Auto-detected if omitted. */
|
||||
natInterface?: string;
|
||||
/** Configured peers */
|
||||
peers: IWgPeerConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates standard WireGuard .conf files compatible with wg-quick,
|
||||
* WireGuard iOS/Android apps, and other standard WireGuard clients.
|
||||
*/
|
||||
export class WgConfigGenerator {
|
||||
/**
|
||||
* Generate a client .conf file content.
|
||||
*/
|
||||
public static generateClientConfig(opts: IWgClientConfOptions): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('[Interface]');
|
||||
lines.push(`PrivateKey = ${opts.privateKey}`);
|
||||
lines.push(`Address = ${opts.address}`);
|
||||
if (opts.dns && opts.dns.length > 0) {
|
||||
lines.push(`DNS = ${opts.dns.join(', ')}`);
|
||||
}
|
||||
if (opts.mtu) {
|
||||
lines.push(`MTU = ${opts.mtu}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('[Peer]');
|
||||
lines.push(`PublicKey = ${opts.peer.publicKey}`);
|
||||
if (opts.peer.presharedKey) {
|
||||
lines.push(`PresharedKey = ${opts.peer.presharedKey}`);
|
||||
}
|
||||
lines.push(`Endpoint = ${opts.peer.endpoint}`);
|
||||
lines.push(`AllowedIPs = ${opts.peer.allowedIps.join(', ')}`);
|
||||
if (opts.peer.persistentKeepalive) {
|
||||
lines.push(`PersistentKeepalive = ${opts.peer.persistentKeepalive}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a server .conf file content.
|
||||
*/
|
||||
public static generateServerConfig(opts: IWgServerConfOptions): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('[Interface]');
|
||||
lines.push(`PrivateKey = ${opts.privateKey}`);
|
||||
lines.push(`Address = ${opts.address}`);
|
||||
lines.push(`ListenPort = ${opts.listenPort}`);
|
||||
if (opts.dns && opts.dns.length > 0) {
|
||||
lines.push(`DNS = ${opts.dns.join(', ')}`);
|
||||
}
|
||||
if (opts.mtu) {
|
||||
lines.push(`MTU = ${opts.mtu}`);
|
||||
}
|
||||
if (opts.enableNat) {
|
||||
const iface = opts.natInterface || 'eth0';
|
||||
lines.push(`PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ${iface} -j MASQUERADE`);
|
||||
lines.push(`PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ${iface} -j MASQUERADE`);
|
||||
}
|
||||
|
||||
for (const peer of opts.peers) {
|
||||
lines.push('');
|
||||
lines.push('[Peer]');
|
||||
lines.push(`PublicKey = ${peer.publicKey}`);
|
||||
if (peer.presharedKey) {
|
||||
lines.push(`PresharedKey = ${peer.presharedKey}`);
|
||||
}
|
||||
lines.push(`AllowedIPs = ${peer.allowedIps.join(', ')}`);
|
||||
if (peer.endpoint) {
|
||||
lines.push(`Endpoint = ${peer.endpoint}`);
|
||||
}
|
||||
if (peer.persistentKeepalive) {
|
||||
lines.push(`PersistentKeepalive = ${peer.persistentKeepalive}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -32,10 +32,24 @@ export interface IVpnClientConfig {
|
||||
mtu?: number;
|
||||
/** Keepalive interval in seconds (default: 30) */
|
||||
keepaliveIntervalSecs?: number;
|
||||
/** Transport protocol: 'auto' (default, tries QUIC then WS), 'websocket', or 'quic' */
|
||||
transport?: 'auto' | 'websocket' | 'quic';
|
||||
/** Transport protocol: 'auto' (default, tries QUIC then WS), 'websocket', 'quic', or 'wireguard' */
|
||||
transport?: 'auto' | 'websocket' | 'quic' | 'wireguard';
|
||||
/** For QUIC: SHA-256 hash of server certificate (base64) for cert pinning */
|
||||
serverCertHash?: string;
|
||||
/** WireGuard: client private key (base64, X25519) */
|
||||
wgPrivateKey?: string;
|
||||
/** WireGuard: client TUN address (e.g. 10.8.0.2) */
|
||||
wgAddress?: string;
|
||||
/** WireGuard: client TUN address prefix length (default: 24) */
|
||||
wgAddressPrefix?: number;
|
||||
/** WireGuard: preshared key (base64, optional) */
|
||||
wgPresharedKey?: string;
|
||||
/** WireGuard: persistent keepalive interval in seconds */
|
||||
wgPersistentKeepalive?: number;
|
||||
/** WireGuard: server endpoint (host:port, e.g. vpn.example.com:51820) */
|
||||
wgEndpoint?: string;
|
||||
/** WireGuard: allowed IPs (CIDR strings, e.g. ['0.0.0.0/0']) */
|
||||
wgAllowedIps?: string[];
|
||||
}
|
||||
|
||||
export interface IVpnClientOptions {
|
||||
@@ -72,12 +86,16 @@ export interface IVpnServerConfig {
|
||||
defaultRateLimitBytesPerSec?: number;
|
||||
/** Default burst size for new clients (bytes). Omit for unlimited. */
|
||||
defaultBurstBytes?: number;
|
||||
/** Transport mode: 'both' (default, WS+QUIC), 'websocket', or 'quic' */
|
||||
transportMode?: 'websocket' | 'quic' | 'both';
|
||||
/** Transport mode: 'both' (default, WS+QUIC), 'websocket', 'quic', or 'wireguard' */
|
||||
transportMode?: 'websocket' | 'quic' | 'both' | 'wireguard';
|
||||
/** QUIC listen address (host:port). Defaults to listenAddr. */
|
||||
quicListenAddr?: string;
|
||||
/** QUIC idle timeout in seconds (default: 30) */
|
||||
quicIdleTimeoutSecs?: number;
|
||||
/** WireGuard: UDP listen port (default: 51820) */
|
||||
wgListenPort?: number;
|
||||
/** WireGuard: configured peers */
|
||||
wgPeers?: IWgPeerConfig[];
|
||||
}
|
||||
|
||||
export interface IVpnServerOptions {
|
||||
@@ -187,6 +205,35 @@ export interface IVpnClientTelemetry {
|
||||
burstBytes?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WireGuard-specific types
|
||||
// ============================================================================
|
||||
|
||||
export interface IWgPeerConfig {
|
||||
/** Peer's public key (base64, X25519) */
|
||||
publicKey: string;
|
||||
/** Optional preshared key (base64) */
|
||||
presharedKey?: string;
|
||||
/** Allowed IP ranges (CIDR strings) */
|
||||
allowedIps: string[];
|
||||
/** Peer endpoint (host:port) — optional for server peers, required for client */
|
||||
endpoint?: string;
|
||||
/** Persistent keepalive interval in seconds */
|
||||
persistentKeepalive?: number;
|
||||
}
|
||||
|
||||
export interface IWgPeerInfo {
|
||||
publicKey: string;
|
||||
allowedIps: string[];
|
||||
endpoint?: string;
|
||||
persistentKeepalive?: number;
|
||||
bytesSent: number;
|
||||
bytesReceived: number;
|
||||
packetsSent: number;
|
||||
packetsReceived: number;
|
||||
lastHandshakeTime?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IPC Command maps (used by smartrust RustBridge<TCommands>)
|
||||
// ============================================================================
|
||||
@@ -211,6 +258,10 @@ export type TVpnServerCommands = {
|
||||
setClientRateLimit: { params: { clientId: string; rateBytesPerSec: number; burstBytes: number }; result: void };
|
||||
removeClientRateLimit: { params: { clientId: string }; result: void };
|
||||
getClientTelemetry: { params: { clientId: string }; result: IVpnClientTelemetry };
|
||||
generateWgKeypair: { params: Record<string, never>; result: IVpnKeypair };
|
||||
addWgPeer: { params: { peer: IWgPeerConfig }; result: void };
|
||||
removeWgPeer: { params: { publicKey: string }; result: void };
|
||||
listWgPeers: { params: Record<string, never>; result: { peers: IWgPeerInfo[] } };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user