2026-02-09 10:55:46 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
|
import { logger } from '../../core/utils/logger.js';
|
|
|
|
|
import type { IRouteConfig } from './models/route-types.js';
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-10 09:43:40 +00:00
|
|
|
* Type-safe command definitions for the Rust proxy IPC protocol.
|
2026-02-09 10:55:46 +00:00
|
|
|
*/
|
2026-02-10 09:43:40 +00:00
|
|
|
type TSmartProxyCommands = {
|
|
|
|
|
start: { params: { config: any }; result: void };
|
|
|
|
|
stop: { params: Record<string, never>; result: void };
|
|
|
|
|
updateRoutes: { params: { routes: IRouteConfig[] }; result: void };
|
|
|
|
|
getMetrics: { params: Record<string, never>; result: any };
|
|
|
|
|
getStatistics: { params: Record<string, never>; result: any };
|
|
|
|
|
provisionCertificate: { params: { routeName: string }; result: void };
|
|
|
|
|
renewCertificate: { params: { routeName: string }; result: void };
|
|
|
|
|
getCertificateStatus: { params: { routeName: string }; result: any };
|
|
|
|
|
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
|
|
|
|
getNftablesStatus: { params: Record<string, never>; result: any };
|
|
|
|
|
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
|
|
|
|
addListeningPort: { params: { port: number }; result: void };
|
|
|
|
|
removeListeningPort: { params: { port: number }; result: void };
|
|
|
|
|
loadCertificate: { params: { domain: string; cert: string; key: string; ca?: string }; result: void };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the package root directory using import.meta.url.
|
|
|
|
|
* This file is at ts/proxies/smart-proxy/, so package root is 3 levels up.
|
|
|
|
|
*/
|
|
|
|
|
function getPackageRoot(): string {
|
|
|
|
|
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
|
|
|
|
return plugins.path.resolve(thisDir, '..', '..', '..');
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-10 09:43:40 +00:00
|
|
|
* Map Node.js process.platform/process.arch to tsrust's friendly name suffix.
|
|
|
|
|
* tsrust names cross-compiled binaries as: rustproxy_linux_amd64, rustproxy_linux_arm64, etc.
|
2026-02-09 10:55:46 +00:00
|
|
|
*/
|
2026-02-10 09:43:40 +00:00
|
|
|
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;
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-10 09:43:40 +00:00
|
|
|
* Build local search paths for the Rust binary, including dist_rust/ candidates
|
|
|
|
|
* (built by tsrust) and local development build paths.
|
2026-02-09 10:55:46 +00:00
|
|
|
*/
|
2026-02-10 09:43:40 +00:00
|
|
|
function buildLocalPaths(): string[] {
|
|
|
|
|
const packageRoot = getPackageRoot();
|
|
|
|
|
const suffix = getTsrustPlatformSuffix();
|
|
|
|
|
const paths: string[] = [];
|
|
|
|
|
|
|
|
|
|
// dist_rust/ candidates (tsrust cross-compiled output)
|
|
|
|
|
if (suffix) {
|
|
|
|
|
paths.push(plugins.path.join(packageRoot, 'dist_rust', `rustproxy_${suffix}`));
|
|
|
|
|
}
|
|
|
|
|
paths.push(plugins.path.join(packageRoot, 'dist_rust', 'rustproxy'));
|
|
|
|
|
|
|
|
|
|
// Local dev build paths
|
|
|
|
|
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', 'rustproxy'));
|
|
|
|
|
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', 'rustproxy'));
|
|
|
|
|
|
|
|
|
|
return paths;
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bridge between TypeScript SmartProxy and the Rust binary.
|
2026-02-10 09:43:40 +00:00
|
|
|
* Wraps @push.rocks/smartrust's RustBridge with type-safe command definitions.
|
2026-02-09 10:55:46 +00:00
|
|
|
*/
|
|
|
|
|
export class RustProxyBridge extends plugins.EventEmitter {
|
2026-02-10 09:43:40 +00:00
|
|
|
private bridge: plugins.smartrust.RustBridge<TSmartProxyCommands>;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
this.bridge = new plugins.smartrust.RustBridge<TSmartProxyCommands>({
|
|
|
|
|
binaryName: 'rustproxy',
|
|
|
|
|
envVarName: 'SMARTPROXY_RUST_BINARY',
|
|
|
|
|
platformPackagePrefix: '@push.rocks/smartproxy',
|
|
|
|
|
localPaths: buildLocalPaths(),
|
|
|
|
|
logger: {
|
|
|
|
|
log: (level: string, message: string, data?: Record<string, any>) => {
|
|
|
|
|
logger.log(level as any, message, data);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Forward events from the inner bridge
|
|
|
|
|
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
|
|
|
|
this.emit('exit', code, signal);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-09 10:55:46 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Spawn the Rust binary in management mode.
|
|
|
|
|
* Returns true if the binary was found and spawned successfully.
|
|
|
|
|
*/
|
|
|
|
|
public async spawn(): Promise<boolean> {
|
2026-02-10 09:43:40 +00:00
|
|
|
return this.bridge.spawn();
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-10 09:43:40 +00:00
|
|
|
* Kill the Rust process and clean up.
|
2026-02-09 10:55:46 +00:00
|
|
|
*/
|
2026-02-10 09:43:40 +00:00
|
|
|
public kill(): void {
|
|
|
|
|
this.bridge.kill();
|
|
|
|
|
}
|
2026-02-09 10:55:46 +00:00
|
|
|
|
2026-02-10 09:43:40 +00:00
|
|
|
/**
|
|
|
|
|
* Whether the bridge is currently running.
|
|
|
|
|
*/
|
|
|
|
|
public get running(): boolean {
|
|
|
|
|
return this.bridge.running;
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 09:43:40 +00:00
|
|
|
// --- Convenience methods for each management command ---
|
2026-02-09 10:55:46 +00:00
|
|
|
|
|
|
|
|
public async startProxy(config: any): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('start', { config });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async stopProxy(): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('stop', {} as Record<string, never>);
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('updateRoutes', { routes });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getMetrics(): Promise<any> {
|
2026-02-10 09:43:40 +00:00
|
|
|
return this.bridge.sendCommand('getMetrics', {} as Record<string, never>);
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getStatistics(): Promise<any> {
|
2026-02-10 09:43:40 +00:00
|
|
|
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async provisionCertificate(routeName: string): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('provisionCertificate', { routeName });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async renewCertificate(routeName: string): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('renewCertificate', { routeName });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getCertificateStatus(routeName: string): Promise<any> {
|
2026-02-10 09:43:40 +00:00
|
|
|
return this.bridge.sendCommand('getCertificateStatus', { routeName });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getListeningPorts(): Promise<number[]> {
|
2026-02-10 09:43:40 +00:00
|
|
|
const result = await this.bridge.sendCommand('getListeningPorts', {} as Record<string, never>);
|
2026-02-09 10:55:46 +00:00
|
|
|
return result?.ports ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getNftablesStatus(): Promise<any> {
|
2026-02-10 09:43:40 +00:00
|
|
|
return this.bridge.sendCommand('getNftablesStatus', {} as Record<string, never>);
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async setSocketHandlerRelay(socketPath: string): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async addListeningPort(port: number): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('addListeningPort', { port });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async removeListeningPort(port: number): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('removeListeningPort', { port });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async loadCertificate(domain: string, cert: string, key: string, ca?: string): Promise<void> {
|
2026-02-10 09:43:40 +00:00
|
|
|
await this.bridge.sendCommand('loadCertificate', { domain, cert, key, ca });
|
2026-02-09 10:55:46 +00:00
|
|
|
}
|
|
|
|
|
}
|