feat(smart-proxy): add socket-handler relay, fast-path port-only forwarding, metrics and bridge improvements, and various TS/Rust integration fixes

This commit is contained in:
2026-02-09 16:25:33 +00:00
parent 41efdb47f8
commit f7605e042e
17 changed files with 724 additions and 300 deletions

View File

@@ -15,6 +15,7 @@ export class SocketHandlerServer {
private server: plugins.net.Server | null = null;
private socketPath: string;
private preprocessor: RoutePreprocessor;
private activeSockets = new Set<plugins.net.Socket>();
constructor(preprocessor: RoutePreprocessor) {
this.preprocessor = preprocessor;
@@ -41,6 +42,8 @@ export class SocketHandlerServer {
return new Promise<void>((resolve, reject) => {
this.server = plugins.net.createServer((socket) => {
this.activeSockets.add(socket);
socket.on('close', () => this.activeSockets.delete(socket));
this.handleConnection(socket);
});
@@ -61,6 +64,12 @@ export class SocketHandlerServer {
* Stop the server and clean up.
*/
public async stop(): Promise<void> {
// Destroy all active connections first
for (const socket of this.activeSockets) {
socket.destroy();
}
this.activeSockets.clear();
if (this.server) {
return new Promise<void>((resolve) => {
this.server!.close(() => {
@@ -100,6 +109,7 @@ export class SocketHandlerServer {
metadataParsed = true;
socket.removeListener('data', onData);
socket.pause(); // Prevent data loss between handler removal and pipe setup
const metadataJson = metadataBuffer.slice(0, newlineIndex);
const remainingData = metadataBuffer.slice(newlineIndex + 1);
@@ -140,13 +150,6 @@ export class SocketHandlerServer {
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,
@@ -167,12 +170,110 @@ export class SocketHandlerServer {
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();
const handler = originalRoute.action.socketHandler;
if (handler) {
// Route has an explicit socket handler callback
try {
const result = handler(socket, context);
// If the handler is async, wait for it to finish setup before resuming.
// This prevents data loss when async handlers need to do work before
// attaching their `data` listeners.
if (result && typeof (result as any).then === 'function') {
(result as any).then(() => {
socket.resume();
}).catch((err: any) => {
logger.log('error', `Async socket handler rejected for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
});
} else {
// Synchronous handler — listeners are already attached, safe to resume.
socket.resume();
}
} catch (err: any) {
logger.log('error', `Socket handler threw for route ${routeKey}: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
}
return;
}
// Route has dynamic host/port functions - resolve and forward
if (originalRoute.action.targets && originalRoute.action.targets.length > 0) {
this.forwardDynamicRoute(socket, originalRoute, context);
return;
}
logger.log('error', `Route ${routeKey} has no socketHandler and no targets`, { component: 'socket-handler-server' });
socket.destroy();
}
/**
* Forward a connection to a dynamically resolved target.
* Used for routes with function-based host/port that Rust cannot handle.
*/
private forwardDynamicRoute(socket: plugins.net.Socket, route: IRouteConfig, context: IRouteContext): void {
const targets = route.action.targets!;
// Pick a target (round-robin would be ideal, but simple random for now)
const target = targets[Math.floor(Math.random() * targets.length)];
// Resolve host
let host: string;
if (typeof target.host === 'function') {
try {
const result = target.host(context);
host = Array.isArray(result) ? result[Math.floor(Math.random() * result.length)] : result;
} catch (err: any) {
logger.log('error', `Dynamic host function failed: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
return;
}
} else if (typeof target.host === 'string') {
host = target.host;
} else if (Array.isArray(target.host)) {
host = target.host[Math.floor(Math.random() * target.host.length)];
} else {
host = 'localhost';
}
// Resolve port
let port: number;
if (typeof target.port === 'function') {
try {
port = target.port(context);
} catch (err: any) {
logger.log('error', `Dynamic port function failed: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
return;
}
} else if (typeof target.port === 'number') {
port = target.port;
} else {
port = context.port;
}
logger.log('debug', `Dynamic forward: ${context.clientIp} -> ${host}:${port}`, { component: 'socket-handler-server' });
// Connect to the resolved target
const backend = plugins.net.connect(port, host, () => {
// Pipe bidirectionally
socket.pipe(backend);
backend.pipe(socket);
});
backend.on('error', (err) => {
logger.log('error', `Dynamic forward backend error: ${err.message}`, { component: 'socket-handler-server' });
socket.destroy();
});
socket.on('error', () => {
backend.destroy();
});
socket.on('close', () => {
backend.destroy();
});
backend.on('close', () => {
socket.destroy();
});
}
}