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:
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user