feat(rustproxy): introduce a Rust-powered proxy engine and workspace with core crates for proxy functionality, ACME/TLS support, passthrough and HTTP proxies, metrics, nftables integration, routing/security, management IPC, tests, and README updates

This commit is contained in:
2026-02-09 10:55:46 +00:00
parent a31fee41df
commit 1df3b7af4a
151 changed files with 16927 additions and 19432 deletions

View File

@@ -0,0 +1,178 @@
import * as plugins from '../../plugins.js';
import { logger } from '../../core/utils/logger.js';
import type { IRouteConfig, IRouteContext } from './models/route-types.js';
import type { RoutePreprocessor } from './route-preprocessor.js';
/**
* Unix domain socket server that receives relayed connections from the Rust proxy.
*
* When Rust encounters a route of type `socket-handler`, it connects to this
* Unix socket, sends a JSON metadata line, then proxies the raw TCP bytes.
* This server reads the metadata, finds the original JS handler, builds an
* IRouteContext, and hands the socket to the handler.
*/
export class SocketHandlerServer {
private server: plugins.net.Server | null = null;
private socketPath: string;
private preprocessor: RoutePreprocessor;
constructor(preprocessor: RoutePreprocessor) {
this.preprocessor = preprocessor;
this.socketPath = `/tmp/smartproxy-relay-${process.pid}.sock`;
}
/**
* The Unix socket path this server listens on.
*/
public getSocketPath(): string {
return this.socketPath;
}
/**
* Start listening for relayed connections from Rust.
*/
public async start(): Promise<void> {
// Clean up stale socket file
try {
await plugins.fs.promises.unlink(this.socketPath);
} catch {
// Ignore if doesn't exist
}
return new Promise<void>((resolve, reject) => {
this.server = plugins.net.createServer((socket) => {
this.handleConnection(socket);
});
this.server.on('error', (err) => {
logger.log('error', `SocketHandlerServer error: ${err.message}`, { component: 'socket-handler-server' });
});
this.server.listen(this.socketPath, () => {
logger.log('info', `SocketHandlerServer listening on ${this.socketPath}`, { component: 'socket-handler-server' });
resolve();
});
this.server.on('error', reject);
});
}
/**
* Stop the server and clean up.
*/
public async stop(): Promise<void> {
if (this.server) {
return new Promise<void>((resolve) => {
this.server!.close(() => {
this.server = null;
// Clean up socket file
plugins.fs.unlink(this.socketPath, () => resolve());
});
});
}
}
/**
* Handle an incoming relayed connection from Rust.
*
* Protocol: Rust sends a single JSON line with metadata, then raw bytes follow.
* JSON format: { "routeKey": "my-route", "remoteIP": "1.2.3.4", "remotePort": 12345,
* "localPort": 443, "isTLS": true, "domain": "example.com" }
*/
private handleConnection(socket: plugins.net.Socket): void {
let metadataBuffer = '';
let metadataParsed = false;
const onData = (chunk: Buffer) => {
if (metadataParsed) return;
metadataBuffer += chunk.toString('utf8');
const newlineIndex = metadataBuffer.indexOf('\n');
if (newlineIndex === -1) {
// Haven't received full metadata line yet
if (metadataBuffer.length > 8192) {
logger.log('error', 'Socket handler metadata too large, closing', { component: 'socket-handler-server' });
socket.destroy();
}
return;
}
metadataParsed = true;
socket.removeListener('data', onData);
const metadataJson = metadataBuffer.slice(0, newlineIndex);
const remainingData = metadataBuffer.slice(newlineIndex + 1);
let metadata: any;
try {
metadata = JSON.parse(metadataJson);
} catch {
logger.log('error', `Invalid socket handler metadata JSON: ${metadataJson.slice(0, 200)}`, { component: 'socket-handler-server' });
socket.destroy();
return;
}
this.dispatchToHandler(socket, metadata, remainingData);
};
socket.on('data', onData);
socket.on('error', (err) => {
logger.log('error', `Socket handler relay error: ${err.message}`, { component: 'socket-handler-server' });
});
}
/**
* Dispatch a relayed connection to the appropriate JS handler.
*/
private dispatchToHandler(socket: plugins.net.Socket, metadata: any, remainingData: string): void {
const routeKey = metadata.routeKey as string;
if (!routeKey) {
logger.log('error', 'Socket handler relay missing routeKey', { component: 'socket-handler-server' });
socket.destroy();
return;
}
const originalRoute = this.preprocessor.getOriginalRoute(routeKey);
if (!originalRoute) {
logger.log('error', `No handler found for route: ${routeKey}`, { component: 'socket-handler-server' });
socket.destroy();
return;
}
const handler = originalRoute.action.socketHandler;
if (!handler) {
logger.log('error', `Route ${routeKey} has no socketHandler`, { component: 'socket-handler-server' });
socket.destroy();
return;
}
// Build route context
const context: IRouteContext = {
port: metadata.localPort || 0,
domain: metadata.domain,
clientIp: metadata.remoteIP || 'unknown',
serverIp: '0.0.0.0',
path: metadata.path,
isTls: metadata.isTLS || false,
tlsVersion: metadata.tlsVersion,
routeName: originalRoute.name,
routeId: originalRoute.id,
timestamp: Date.now(),
connectionId: metadata.connectionId || `relay-${Date.now()}`,
};
// If there was remaining data after the metadata line, push it back
if (remainingData.length > 0) {
socket.unshift(Buffer.from(remainingData, 'utf8'));
}
// Call the handler
try {
handler(socket, context);
} catch (err: any) {
logger.log('error', `Socket handler threw for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
}
}
}