This commit is contained in:
2026-02-27 10:18:23 +00:00
commit 3f63d19173
36 changed files with 14285 additions and 0 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitance info data
*/
export const commitinfo = {
name: '@push.rocks/smartvpn',
version: '1.0.0',
description: 'A VPN solution with TypeScript control plane and Rust data plane daemon',
};

6
ts/index.ts Normal file
View File

@@ -0,0 +1,6 @@
export * from './smartvpn.interfaces.js';
export { VpnBridge } from './smartvpn.classes.vpnbridge.js';
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';

View File

@@ -0,0 +1,152 @@
import * as plugins from './smartvpn.plugins.js';
import * as paths from './smartvpn.paths.js';
import type {
TVpnTransportOptions,
IVpnTransportSocket,
} from './smartvpn.interfaces.js';
import type { TCommandMap } from '@push.rocks/smartrust';
/**
* Get the package root directory.
*/
function getPackageRoot(): string {
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
return plugins.path.resolve(thisDir, '..');
}
/**
* Map Node.js platform/arch to tsrust's platform suffix.
*/
function getTsrustPlatformSuffix(): string | null {
const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
const osMap: Record<string, string> = { linux: 'linux', darwin: 'macos' };
const os = osMap[process.platform];
const arch = archMap[process.arch];
if (os && arch) {
return `${os}_${arch}`;
}
return null;
}
/**
* Build local search paths for the smartvpn daemon binary.
*/
function buildLocalPaths(binaryName: string): string[] {
const packageRoot = getPackageRoot();
const suffix = getTsrustPlatformSuffix();
const paths: string[] = [];
// dist_rust/ (tsrust cross-compiled output)
if (suffix) {
paths.push(plugins.path.join(packageRoot, 'dist_rust', `${binaryName}_${suffix}`));
}
paths.push(plugins.path.join(packageRoot, 'dist_rust', binaryName));
// Local dev build paths
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', binaryName));
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', binaryName));
return paths;
}
/**
* Shared bridge wrapper around smartrust RustBridge.
* Supports stdio mode (dev: spawn child process) and socket mode (production: connect to running daemon).
*/
export class VpnBridge<TCommands extends TCommandMap> extends plugins.events.EventEmitter {
private bridge: plugins.smartrust.RustBridge<TCommands>;
private transportOptions: TVpnTransportOptions;
private mode: 'client' | 'server';
constructor(options: {
transport: TVpnTransportOptions;
mode: 'client' | 'server';
binaryName?: string;
}) {
super();
const binaryName = options.binaryName || 'smartvpn_daemon';
this.transportOptions = options.transport;
this.mode = options.mode;
this.bridge = new plugins.smartrust.RustBridge<TCommands>({
binaryName,
envVarName: 'SMARTVPN_RUST_BINARY',
platformPackagePrefix: '@push.rocks/smartvpn',
localPaths: buildLocalPaths(binaryName),
cliArgs: ['--management', '--mode', this.mode],
maxPayloadSize: 10 * 1024 * 1024, // 10 MB
});
// Forward events from inner bridge
this.bridge.on('exit', (code: number | null, signal: string | null) => {
this.emit('exit', code, signal);
});
this.bridge.on('reconnected', () => {
this.emit('reconnected');
});
// Forward management events from the daemon
// smartrust emits 'management:<eventName>' for unsolicited events
this.bridge.on('management:status', (data: any) => {
this.emit('status', data);
});
this.bridge.on('management:error', (data: any) => {
this.emit('error', data);
});
this.bridge.on('management:client-connected', (data: any) => {
this.emit('client-connected', data);
});
this.bridge.on('management:client-disconnected', (data: any) => {
this.emit('client-disconnected', data);
});
this.bridge.on('management:started', (data: any) => {
this.emit('started', data);
});
this.bridge.on('management:stopped', (data: any) => {
this.emit('stopped', data);
});
}
/**
* Start the bridge: spawn in stdio mode or connect in socket mode.
*/
public async start(): Promise<boolean> {
if (this.transportOptions.transport === 'socket') {
const socketOpts = this.transportOptions as IVpnTransportSocket;
return this.bridge.connect(socketOpts.socketPath, {
autoReconnect: socketOpts.autoReconnect,
reconnectBaseDelayMs: socketOpts.reconnectBaseDelayMs,
reconnectMaxDelayMs: socketOpts.reconnectMaxDelayMs,
maxReconnectAttempts: socketOpts.maxReconnectAttempts,
});
} else {
return this.bridge.spawn();
}
}
/**
* Stop the bridge. In socket mode, closes the socket (daemon stays alive).
* In stdio mode, kills the child process.
*/
public stop(): void {
this.bridge.kill();
}
/**
* Send a typed command to the daemon.
*/
public async sendCommand<K extends string & keyof TCommands>(
method: K,
params: TCommands[K]['params'],
): Promise<TCommands[K]['result']> {
return this.bridge.sendCommand(method, params);
}
/**
* Whether the bridge is currently running/connected.
*/
public get running(): boolean {
return this.bridge.running;
}
}

View File

@@ -0,0 +1,87 @@
import * as plugins from './smartvpn.plugins.js';
import { VpnBridge } from './smartvpn.classes.vpnbridge.js';
import type {
IVpnClientOptions,
IVpnClientConfig,
IVpnStatus,
IVpnStatistics,
TVpnClientCommands,
} from './smartvpn.interfaces.js';
/**
* VPN Client — manages a smartvpn daemon in client mode.
*/
export class VpnClient extends plugins.events.EventEmitter {
private bridge: VpnBridge<TVpnClientCommands>;
private options: IVpnClientOptions;
constructor(options: IVpnClientOptions) {
super();
this.options = options;
this.bridge = new VpnBridge<TVpnClientCommands>({
transport: options.transport,
mode: 'client',
});
// Forward bridge events
this.bridge.on('exit', (code: number | null, signal: string | null) => {
this.emit('exit', { code, signal });
});
this.bridge.on('reconnected', () => {
this.emit('reconnected');
});
}
/**
* Start the daemon bridge (spawn or connect).
*/
public async start(): Promise<boolean> {
return this.bridge.start();
}
/**
* Connect to the VPN server using the provided config.
*/
public async connect(config?: IVpnClientConfig): Promise<{ assignedIp: string }> {
const cfg = config || this.options.config;
if (!cfg) {
throw new Error('VpnClient.connect: no config provided');
}
return this.bridge.sendCommand('connect', { config: cfg });
}
/**
* Disconnect from the VPN server.
*/
public async disconnect(): Promise<void> {
await this.bridge.sendCommand('disconnect', {} as Record<string, never>);
}
/**
* Get current connection status.
*/
public async getStatus(): Promise<IVpnStatus> {
return this.bridge.sendCommand('getStatus', {} as Record<string, never>);
}
/**
* Get traffic statistics.
*/
public async getStatistics(): Promise<IVpnStatistics> {
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
}
/**
* Stop the daemon bridge.
*/
public stop(): void {
this.bridge.stop();
}
/**
* Whether the bridge is running.
*/
public get running(): boolean {
return this.bridge.running;
}
}

View File

@@ -0,0 +1,104 @@
import * as plugins from './smartvpn.plugins.js';
import type {
IVpnClientConfig,
IVpnServerConfig,
} from './smartvpn.interfaces.js';
/**
* VPN configuration loader, saver, and validator.
*/
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');
}
if (!config.serverUrl.startsWith('wss://') && !config.serverUrl.startsWith('ws://')) {
throw new Error('VpnConfig: serverUrl must start with wss:// or ws://');
}
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');
}
if (config.keepaliveIntervalSecs !== undefined && config.keepaliveIntervalSecs < 1) {
throw new Error('VpnConfig: keepaliveIntervalSecs must be >= 1');
}
if (config.dns) {
for (const dns of config.dns) {
if (!VpnConfig.isValidIp(dns)) {
throw new Error(`VpnConfig: invalid DNS address: ${dns}`);
}
}
}
}
/**
* 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.mtu !== undefined && (config.mtu < 576 || config.mtu > 65535)) {
throw new Error('VpnConfig: mtu must be between 576 and 65535');
}
if (config.keepaliveIntervalSecs !== undefined && config.keepaliveIntervalSecs < 1) {
throw new Error('VpnConfig: keepaliveIntervalSecs must be >= 1');
}
}
/**
* Load a config from a JSON file.
*/
public static async loadFromFile<T>(filePath: string): Promise<T> {
const content = await plugins.fs.promises.readFile(filePath, 'utf-8');
return JSON.parse(content) as T;
}
/**
* Save a config to a JSON file.
*/
public static async saveToFile<T>(filePath: string, config: T): Promise<void> {
const content = JSON.stringify(config, null, 2);
await plugins.fs.promises.writeFile(filePath, content, 'utf-8');
}
/**
* Basic IP address validation.
*/
private static isValidIp(ip: string): boolean {
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
});
}
/**
* Basic subnet validation (CIDR notation).
*/
private static isValidSubnet(subnet: string): boolean {
const [ip, prefix] = subnet.split('/');
if (!ip || !prefix) return false;
if (!VpnConfig.isValidIp(ip)) return false;
const prefixNum = parseInt(prefix, 10);
return !isNaN(prefixNum) && prefixNum >= 0 && prefixNum <= 32;
}
}

View File

@@ -0,0 +1,126 @@
import * as plugins from './smartvpn.plugins.js';
import type { TVpnPlatform, IVpnServiceUnit } from './smartvpn.interfaces.js';
/**
* Install the smartvpn daemon as a system service.
*/
export class VpnInstaller {
/**
* Detect the current platform.
*/
public static detectPlatform(): TVpnPlatform {
switch (process.platform) {
case 'linux':
return 'linux';
case 'darwin':
return 'macos';
case 'win32':
return 'windows';
default:
return 'unknown';
}
}
/**
* Generate a systemd unit file for Linux.
*/
public static generateSystemdUnit(options: {
binaryPath: string;
socketPath: string;
mode: 'client' | 'server';
configPath?: string;
description?: string;
}): IVpnServiceUnit {
const desc = options.description || `SmartVPN ${options.mode} daemon`;
const content = `[Unit]
Description=${desc}
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=${options.binaryPath} --management-socket ${options.socketPath} --mode ${options.mode}
Restart=always
RestartSec=5
LimitNOFILE=65535
# Security hardening
NoNewPrivileges=no
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/run /dev/net/tun
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
`;
return {
platform: 'linux',
content,
installPath: `/etc/systemd/system/smartvpn-${options.mode}.service`,
};
}
/**
* Generate a launchd plist for macOS.
*/
public static generateLaunchdPlist(options: {
binaryPath: string;
socketPath: string;
mode: 'client' | 'server';
description?: string;
}): IVpnServiceUnit {
const label = `rocks.push.smartvpn.${options.mode}`;
const content = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${label}</string>
<key>ProgramArguments</key>
<array>
<string>${options.binaryPath}</string>
<string>--management-socket</string>
<string>${options.socketPath}</string>
<string>--mode</string>
<string>${options.mode}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/smartvpn-${options.mode}.err.log</string>
<key>StandardOutPath</key>
<string>/var/log/smartvpn-${options.mode}.out.log</string>
</dict>
</plist>
`;
return {
platform: 'macos',
content,
installPath: `/Library/LaunchDaemons/${label}.plist`,
};
}
/**
* Generate the appropriate service unit for the current platform.
*/
public static generateServiceUnit(options: {
binaryPath: string;
socketPath: string;
mode: 'client' | 'server';
}): IVpnServiceUnit {
const platform = VpnInstaller.detectPlatform();
switch (platform) {
case 'linux':
return VpnInstaller.generateSystemdUnit(options);
case 'macos':
return VpnInstaller.generateLaunchdPlist(options);
default:
throw new Error(`VpnInstaller: unsupported platform: ${platform}`);
}
}
}

View File

@@ -0,0 +1,107 @@
import * as plugins from './smartvpn.plugins.js';
import { VpnBridge } from './smartvpn.classes.vpnbridge.js';
import type {
IVpnServerOptions,
IVpnServerConfig,
IVpnStatus,
IVpnServerStatistics,
IVpnClientInfo,
IVpnKeypair,
TVpnServerCommands,
} from './smartvpn.interfaces.js';
/**
* VPN Server — manages a smartvpn daemon in server mode.
*/
export class VpnServer extends plugins.events.EventEmitter {
private bridge: VpnBridge<TVpnServerCommands>;
private options: IVpnServerOptions;
constructor(options: IVpnServerOptions) {
super();
this.options = options;
this.bridge = new VpnBridge<TVpnServerCommands>({
transport: options.transport,
mode: 'server',
});
// Forward bridge events
this.bridge.on('exit', (code: number | null, signal: string | null) => {
this.emit('exit', { code, signal });
});
this.bridge.on('reconnected', () => {
this.emit('reconnected');
});
}
/**
* Start the daemon bridge (spawn or connect).
*/
public async start(config?: IVpnServerConfig): Promise<void> {
const started = await this.bridge.start();
if (!started) {
throw new Error('VpnServer: failed to start daemon bridge');
}
const cfg = config || this.options.config;
if (cfg) {
await this.bridge.sendCommand('start', { config: cfg });
}
}
/**
* Stop the VPN server.
*/
public async stopServer(): Promise<void> {
await this.bridge.sendCommand('stop', {} as Record<string, never>);
}
/**
* Get server status.
*/
public async getStatus(): Promise<IVpnStatus> {
return this.bridge.sendCommand('getStatus', {} as Record<string, never>);
}
/**
* Get server statistics.
*/
public async getStatistics(): Promise<IVpnServerStatistics> {
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
}
/**
* List connected clients.
*/
public async listClients(): Promise<IVpnClientInfo[]> {
const result = await this.bridge.sendCommand('listClients', {} as Record<string, never>);
return result.clients;
}
/**
* Disconnect a specific client.
*/
public async disconnectClient(clientId: string): Promise<void> {
await this.bridge.sendCommand('disconnectClient', { clientId });
}
/**
* Generate a new Noise keypair.
*/
public async generateKeypair(): Promise<IVpnKeypair> {
return this.bridge.sendCommand('generateKeypair', {} as Record<string, never>);
}
/**
* Stop the daemon bridge.
*/
public stop(): void {
this.bridge.stop();
}
/**
* Whether the bridge is running.
*/
public get running(): boolean {
return this.bridge.running;
}
}

166
ts/smartvpn.interfaces.ts Normal file
View File

@@ -0,0 +1,166 @@
// ============================================================================
// Transport options
// ============================================================================
export interface IVpnTransportStdio {
transport: 'stdio';
}
export interface IVpnTransportSocket {
transport: 'socket';
socketPath: string;
autoReconnect?: boolean;
reconnectBaseDelayMs?: number;
reconnectMaxDelayMs?: number;
maxReconnectAttempts?: number;
}
export type TVpnTransportOptions = IVpnTransportStdio | IVpnTransportSocket;
// ============================================================================
// Client configuration
// ============================================================================
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 */
serverPublicKey: string;
/** Optional DNS servers to use while connected */
dns?: string[];
/** Optional MTU for the TUN device */
mtu?: number;
/** Keepalive interval in seconds (default: 30) */
keepaliveIntervalSecs?: number;
}
export interface IVpnClientOptions {
transport: TVpnTransportOptions;
config?: IVpnClientConfig;
}
// ============================================================================
// Server configuration
// ============================================================================
export interface IVpnServerConfig {
/** Listen address for WebSocket, e.g. 0.0.0.0:443 */
listenAddr: string;
/** TLS certificate PEM (optional — can be behind reverse proxy) */
tlsCert?: string;
/** TLS private key PEM */
tlsKey?: string;
/** Server's Noise static private key (base64) */
privateKey: string;
/** Server's Noise static public key (base64) */
publicKey: string;
/** IP subnet for VPN clients, e.g. 10.8.0.0/24 */
subnet: string;
/** DNS servers pushed to clients */
dns?: string[];
/** MTU for TUN device */
mtu?: number;
/** Keepalive interval in seconds (default: 30) */
keepaliveIntervalSecs?: number;
/** Enable NAT/masquerade for client traffic */
enableNat?: boolean;
}
export interface IVpnServerOptions {
transport: TVpnTransportOptions;
config?: IVpnServerConfig;
}
// ============================================================================
// Status and statistics
// ============================================================================
export type TVpnConnectionState =
| 'disconnected'
| 'connecting'
| 'handshaking'
| 'connected'
| 'reconnecting'
| 'error';
export interface IVpnStatus {
state: TVpnConnectionState;
assignedIp?: string;
serverAddr?: string;
connectedSince?: string;
lastError?: string;
}
export interface IVpnStatistics {
bytesSent: number;
bytesReceived: number;
packetsSent: number;
packetsReceived: number;
keepalivesSent: number;
keepalivesReceived: number;
uptimeSeconds: number;
}
export interface IVpnClientInfo {
clientId: string;
assignedIp: string;
connectedSince: string;
bytesSent: number;
bytesReceived: number;
}
export interface IVpnServerStatistics extends IVpnStatistics {
activeClients: number;
totalConnections: number;
}
export interface IVpnKeypair {
publicKey: string;
privateKey: string;
}
// ============================================================================
// IPC Command maps (used by smartrust RustBridge<TCommands>)
// ============================================================================
export type TVpnClientCommands = {
connect: { params: { config: IVpnClientConfig }; result: { assignedIp: string } };
disconnect: { params: Record<string, never>; result: void };
getStatus: { params: Record<string, never>; result: IVpnStatus };
getStatistics: { params: Record<string, never>; result: IVpnStatistics };
};
export type TVpnServerCommands = {
start: { params: { config: IVpnServerConfig }; result: void };
stop: { params: Record<string, never>; result: void };
getStatus: { params: Record<string, never>; result: IVpnStatus };
getStatistics: { params: Record<string, never>; result: IVpnServerStatistics };
listClients: { params: Record<string, never>; result: { clients: IVpnClientInfo[] } };
disconnectClient: { params: { clientId: string }; result: void };
generateKeypair: { params: Record<string, never>; result: IVpnKeypair };
};
// ============================================================================
// Installer
// ============================================================================
export type TVpnPlatform = 'linux' | 'macos' | 'windows' | 'unknown';
export interface IVpnServiceUnit {
platform: TVpnPlatform;
content: string;
installPath: string;
}
// ============================================================================
// Events emitted by VpnClient / VpnServer
// ============================================================================
export interface IVpnEventMap {
'status': IVpnStatus;
'error': { message: string; code?: string };
'client-connected': IVpnClientInfo;
'client-disconnected': { clientId: string; reason?: string };
'exit': { code: number | null; signal: string | null };
'reconnected': void;
}

6
ts/smartvpn.paths.ts Normal file
View File

@@ -0,0 +1,6 @@
import * as plugins from './smartvpn.plugins.js';
export const packageDir = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../',
);

14
ts/smartvpn.plugins.ts Normal file
View File

@@ -0,0 +1,14 @@
// node native
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as url from 'url';
import * as events from 'events';
export { path, fs, os, url, events };
// @push.rocks
import * as smartpath from '@push.rocks/smartpath';
import * as smartrust from '@push.rocks/smartrust';
export { smartpath, smartrust };