import * as plugins from '../../plugins.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import { logger } from '../../core/utils/logger.js'; // Route checking functions have been removed import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-types.js'; import { ConnectionManager } from './connection-manager.js'; import { SecurityManager } from './security-manager.js'; import { TlsManager } from './tls-manager.js'; import { HttpProxyBridge } from './http-proxy-bridge.js'; import { TimeoutManager } from './timeout-manager.js'; import { RouteManager } from './route-manager.js'; import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; /** * Handles new connection processing and setup logic with support for route-based configuration */ export class RouteConnectionHandler { private settings: ISmartProxyOptions; // Cache for route contexts to avoid recreation private routeContextCache: Map = new Map(); constructor( settings: ISmartProxyOptions, private connectionManager: ConnectionManager, private securityManager: SecurityManager, private tlsManager: TlsManager, private httpProxyBridge: HttpProxyBridge, private timeoutManager: TimeoutManager, private routeManager: RouteManager ) { this.settings = settings; } /** * Create a route context object for port and host mapping functions */ private createRouteContext(options: { connectionId: string; port: number; domain?: string; clientIp: string; serverIp: string; isTls: boolean; tlsVersion?: string; routeName?: string; routeId?: string; path?: string; query?: string; headers?: Record; }): IRouteContext { return { // Connection information port: options.port, domain: options.domain, clientIp: options.clientIp, serverIp: options.serverIp, path: options.path, query: options.query, headers: options.headers, // TLS information isTls: options.isTls, tlsVersion: options.tlsVersion, // Route information routeName: options.routeName, routeId: options.routeId, // Additional properties timestamp: Date.now(), connectionId: options.connectionId, }; } /** * Handle a new incoming connection */ public handleConnection(socket: plugins.net.Socket): void { const remoteIP = socket.remoteAddress || ''; const localPort = socket.localPort || 0; // Validate IP against rate limits and connection limits const ipValidation = this.securityManager.validateIP(remoteIP); if (!ipValidation.allowed) { logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' }); socket.end(); socket.destroy(); return; } // Create a new connection record const record = this.connectionManager.createConnection(socket); const connectionId = record.id; // Apply socket optimizations socket.setNoDelay(this.settings.noDelay); // Apply keep-alive settings if enabled if (this.settings.keepAlive) { socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); record.hasKeepAlive = true; // Apply enhanced TCP keep-alive options if enabled if (this.settings.enableKeepAliveProbes) { try { // These are platform-specific and may not be available if ('setKeepAliveProbes' in socket) { (socket as any).setKeepAliveProbes(10); } if ('setKeepAliveInterval' in socket) { (socket as any).setKeepAliveInterval(1000); } } catch (err) { // Ignore errors - these are optional enhancements if (this.settings.enableDetailedLogging) { logger.log('warn', `Enhanced TCP keep-alive settings not supported`, { connectionId, error: err, component: 'route-handler' }); } } } } if (this.settings.enableDetailedLogging) { logger.log('info', `New connection from ${remoteIP} on port ${localPort}. ` + `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + `Active connections: ${this.connectionManager.getConnectionCount()}`, { connectionId, remoteIP, localPort, keepAlive: record.hasKeepAlive ? 'Enabled' : 'Disabled', activeConnections: this.connectionManager.getConnectionCount(), component: 'route-handler' } ); } else { logger.log('info', `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`, { remoteIP, localPort, activeConnections: this.connectionManager.getConnectionCount(), component: 'route-handler' } ); } // Start TLS SNI handling this.handleTlsConnection(socket, record); } /** * Handle a connection and wait for TLS handshake for SNI extraction if needed */ private handleTlsConnection(socket: plugins.net.Socket, record: IConnectionRecord): void { const connectionId = record.id; const localPort = record.localPort; let initialDataReceived = false; // Set an initial timeout for handshake data let initialTimeout: NodeJS.Timeout | null = setTimeout(() => { if (!initialDataReceived) { logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.settings.initialDataTimeout}ms for connection ${connectionId}`, { connectionId, timeout: this.settings.initialDataTimeout, remoteIP: record.remoteIP, component: 'route-handler' }); // Add a grace period setTimeout(() => { if (!initialDataReceived) { logger.log('warn', `Final initial data timeout after grace period for connection ${connectionId}`, { connectionId, component: 'route-handler' }); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'initial_timeout'; this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout'); } socket.end(); this.connectionManager.cleanupConnection(record, 'initial_timeout'); } }, 30000); } }, this.settings.initialDataTimeout!); // Make sure timeout doesn't keep the process alive if (initialTimeout.unref) { initialTimeout.unref(); } // Set up error handler socket.on('error', this.connectionManager.handleError('incoming', record)); // First data handler to capture initial TLS handshake socket.once('data', (chunk: Buffer) => { // Clear the initial timeout since we've received data if (initialTimeout) { clearTimeout(initialTimeout); initialTimeout = null; } initialDataReceived = true; record.hasReceivedInitialData = true; // Block non-TLS connections on port 443 if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) { logger.log('warn', `Non-TLS connection ${connectionId} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, { connectionId, message: 'Terminating connection - only TLS traffic is allowed on standard HTTPS port.', component: 'route-handler' }); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'non_tls_blocked'; this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked'); } socket.end(); this.connectionManager.cleanupConnection(record, 'non_tls_blocked'); return; } // Check if this looks like a TLS handshake let serverName = ''; if (this.tlsManager.isTlsHandshake(chunk)) { record.isTLS = true; // Check for ClientHello to extract SNI if (this.tlsManager.isClientHello(chunk)) { // Create connection info for SNI extraction const connInfo = { sourceIp: record.remoteIP, sourcePort: socket.remotePort || 0, destIp: socket.localAddress || '', destPort: socket.localPort || 0, }; // Extract SNI serverName = this.tlsManager.extractSNI(chunk, connInfo) || ''; // Lock the connection to the negotiated SNI record.lockedDomain = serverName; // Check if we should reject connections without SNI if (!serverName && this.settings.allowSessionTicket === false) { logger.log('warn', `No SNI detected in TLS ClientHello for connection ${connectionId}; sending TLS alert`, { connectionId, component: 'route-handler' }); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; this.connectionManager.incrementTerminationStat( 'incoming', 'session_ticket_blocked_no_sni' ); } const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]); try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); } catch { socket.end(); } this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); return; } if (this.settings.enableDetailedLogging) { logger.log('info', `TLS connection with SNI`, { connectionId, serverName: serverName || '(empty)', component: 'route-handler' }); } } } // Find the appropriate route for this connection this.routeConnection(socket, record, serverName, chunk); }); } /** * Route the connection based on match criteria */ private routeConnection( socket: plugins.net.Socket, record: IConnectionRecord, serverName: string, initialChunk?: Buffer ): void { const connectionId = record.id; const localPort = record.localPort; const remoteIP = record.remoteIP; // Find matching route const routeMatch = this.routeManager.findMatchingRoute({ port: localPort, domain: serverName, clientIp: remoteIP, path: undefined, // We don't have path info at this point tlsVersion: undefined, // We don't extract TLS version yet }); if (!routeMatch) { logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, { connectionId, serverName: serverName || 'connection', localPort, component: 'route-handler' }); // No matching route, use default/fallback handling logger.log('info', `Using default route handling for connection ${connectionId}`, { connectionId, component: 'route-handler' }); // Check default security settings const defaultSecuritySettings = this.settings.defaults?.security; if (defaultSecuritySettings) { if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) { const isAllowed = this.securityManager.isIPAuthorized( remoteIP, defaultSecuritySettings.ipAllowList, defaultSecuritySettings.ipBlockList || [] ); if (!isAllowed) { logger.log('warn', `IP ${remoteIP} not in default allowed list for connection ${connectionId}`, { connectionId, remoteIP, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'ip_blocked'); return; } } } // Setup direct connection with default settings if (this.settings.defaults?.target) { // Use defaults from configuration const targetHost = this.settings.defaults.target.host; const targetPort = this.settings.defaults.target.port; return this.setupDirectConnection( socket, record, serverName, initialChunk, undefined, targetHost, targetPort ); } else { // No default target available, terminate the connection logger.log('warn', `No default target configured for connection ${connectionId}. Closing connection`, { connectionId, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'no_default_target'); return; } } // A matching route was found const route = routeMatch.route; if (this.settings.enableDetailedLogging) { logger.log('info', `Route matched`, { connectionId, routeName: route.name || 'unnamed', serverName: serverName || 'connection', localPort, component: 'route-handler' }); } // Handle the route based on its action type switch (route.action.type) { case 'forward': return this.handleForwardAction(socket, record, route, initialChunk); case 'socket-handler': logger.log('info', `Handling socket-handler action for route ${route.name}`, { connectionId, routeName: route.name, component: 'route-handler' }); this.handleSocketHandlerAction(socket, record, route, initialChunk); return; default: logger.log('error', `Unknown action type '${(route.action as any).type}' for connection ${connectionId}`, { connectionId, actionType: (route.action as any).type, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'unknown_action'); } } /** * Handle a forward action for a route */ private handleForwardAction( socket: plugins.net.Socket, record: IConnectionRecord, route: IRouteConfig, initialChunk?: Buffer ): void { const connectionId = record.id; const action = route.action as IRouteAction; // Check if this route uses NFTables for forwarding if (action.forwardingEngine === 'nftables') { // NFTables handles packet forwarding at the kernel level // The application should NOT interfere with these connections // Log the connection for monitoring purposes if (this.settings.enableDetailedLogging) { logger.log('info', `NFTables forwarding (kernel-level)`, { connectionId: record.id, source: `${record.remoteIP}:${socket.remotePort}`, destination: `${socket.localAddress}:${record.localPort}`, routeName: route.name || 'unnamed', domain: record.lockedDomain || 'n/a', component: 'route-handler' }); } else { logger.log('info', `NFTables forwarding`, { connectionId: record.id, remoteIP: record.remoteIP, localPort: record.localPort, routeName: route.name || 'unnamed', component: 'route-handler' }); } // Additional NFTables-specific logging if configured if (action.nftables) { const nftConfig = action.nftables; if (this.settings.enableDetailedLogging) { logger.log('info', `NFTables config`, { connectionId: record.id, protocol: nftConfig.protocol || 'tcp', preserveSourceIP: nftConfig.preserveSourceIP || false, priority: nftConfig.priority || 'default', maxRate: nftConfig.maxRate || 'unlimited', component: 'route-handler' }); } } // For NFTables routes, we should still track the connection but not interfere // Mark the connection as using network proxy so it's cleaned up properly record.usingNetworkProxy = true; // We don't close the socket - just let it remain open // The kernel-level NFTables rules will handle the actual forwarding return; } // We should have a target configuration for forwarding if (!action.target) { logger.log('error', `Forward action missing target configuration for connection ${connectionId}`, { connectionId, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'missing_target'); return; } // Create the routing context for this connection const routeContext = this.createRouteContext({ connectionId: record.id, port: record.localPort, domain: record.lockedDomain, clientIp: record.remoteIP, serverIp: socket.localAddress || '', isTls: record.isTLS || false, tlsVersion: record.tlsVersion, routeName: route.name, routeId: route.id, }); // Cache the context for potential reuse this.routeContextCache.set(connectionId, routeContext); // Determine host using function or static value let targetHost: string | string[]; if (typeof action.target.host === 'function') { try { targetHost = action.target.host(routeContext); if (this.settings.enableDetailedLogging) { logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, { connectionId, targetHost: Array.isArray(targetHost) ? targetHost.join(', ') : targetHost, component: 'route-handler' }); } } catch (err) { logger.log('error', `Error in host mapping function for connection ${connectionId}: ${err}`, { connectionId, error: err, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'host_mapping_error'); return; } } else { targetHost = action.target.host; } // If an array of hosts, select one randomly for load balancing const selectedHost = Array.isArray(targetHost) ? targetHost[Math.floor(Math.random() * targetHost.length)] : targetHost; // Determine port using function or static value let targetPort: number; if (typeof action.target.port === 'function') { try { targetPort = action.target.port(routeContext); if (this.settings.enableDetailedLogging) { logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, { connectionId, sourcePort: record.localPort, targetPort, component: 'route-handler' }); } // Store the resolved target port in the context for potential future use routeContext.targetPort = targetPort; } catch (err) { logger.log('error', `Error in port mapping function for connection ${connectionId}: ${err}`, { connectionId, error: err, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'port_mapping_error'); return; } } else if (action.target.port === 'preserve') { // Use incoming port if port is 'preserve' targetPort = record.localPort; } else { // Use static port from configuration targetPort = action.target.port; } // Store the resolved host in the context routeContext.targetHost = selectedHost; // Determine if this needs TLS handling if (action.tls) { switch (action.tls.mode) { case 'passthrough': // For TLS passthrough, just forward directly if (this.settings.enableDetailedLogging) { logger.log('info', `Using TLS passthrough to ${selectedHost}:${targetPort} for connection ${connectionId}`, { connectionId, targetHost: selectedHost, targetPort, component: 'route-handler' }); } return this.setupDirectConnection( socket, record, record.lockedDomain, initialChunk, undefined, selectedHost, targetPort ); case 'terminate': case 'terminate-and-reencrypt': // For TLS termination, use HttpProxy if (this.httpProxyBridge.getHttpProxy()) { if (this.settings.enableDetailedLogging) { logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, { connectionId, targetHost: action.target.host, component: 'route-handler' }); } // If we have an initial chunk with TLS data, start processing it if (initialChunk && record.isTLS) { this.httpProxyBridge.forwardToHttpProxy( connectionId, socket, record, initialChunk, this.settings.httpProxyPort || 8443, (reason) => this.connectionManager.initiateCleanupOnce(record, reason) ); return; } // This shouldn't normally happen - we should have TLS data at this point logger.log('error', `TLS termination route without TLS data for connection ${connectionId}`, { connectionId, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'tls_error'); return; } else { logger.log('error', `HttpProxy not available for TLS termination for connection ${connectionId}`, { connectionId, component: 'route-handler' }); socket.end(); this.connectionManager.cleanupConnection(record, 'no_http_proxy'); return; } } } else { // No TLS settings - check if this port should use HttpProxy const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort); if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) { // Forward non-TLS connections to HttpProxy if configured if (this.settings.enableDetailedLogging) { logger.log('info', `Using HttpProxy for non-TLS connection ${connectionId} on port ${record.localPort}`, { connectionId, port: record.localPort, component: 'route-handler' }); } this.httpProxyBridge.forwardToHttpProxy( connectionId, socket, record, initialChunk, this.settings.httpProxyPort || 8443, (reason) => this.connectionManager.initiateCleanupOnce(record, reason) ); return; } else { // Basic forwarding if (this.settings.enableDetailedLogging) { logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, { connectionId, targetHost: action.target.host, targetPort: action.target.port, component: 'route-handler' }); } // Get the appropriate host value let targetHost: string; if (typeof action.target.host === 'function') { // For function-based host, use the same routeContext created earlier const hostResult = action.target.host(routeContext); targetHost = Array.isArray(hostResult) ? hostResult[Math.floor(Math.random() * hostResult.length)] : hostResult; } else { // For static host value targetHost = Array.isArray(action.target.host) ? action.target.host[Math.floor(Math.random() * action.target.host.length)] : action.target.host; } // Determine port - either function-based, static, or preserve incoming port let targetPort: number; if (typeof action.target.port === 'function') { targetPort = action.target.port(routeContext); } else if (action.target.port === 'preserve') { targetPort = record.localPort; } else { targetPort = action.target.port; } // Update the connection record and context with resolved values record.targetHost = targetHost; record.targetPort = targetPort; return this.setupDirectConnection( socket, record, record.lockedDomain, initialChunk, undefined, targetHost, targetPort ); } } } /** * Handle a socket-handler action for a route */ private async handleSocketHandlerAction( socket: plugins.net.Socket, record: IConnectionRecord, route: IRouteConfig, initialChunk?: Buffer ): Promise { const connectionId = record.id; if (!route.action.socketHandler) { logger.log('error', 'socket-handler action missing socketHandler function', { connectionId, routeName: route.name, component: 'route-handler' }); socket.destroy(); this.connectionManager.cleanupConnection(record, 'missing_handler'); return; } // Create route context for the handler const routeContext = this.createRouteContext({ connectionId: record.id, port: record.localPort, domain: record.lockedDomain, clientIp: record.remoteIP, serverIp: socket.localAddress || '', isTls: record.isTLS || false, tlsVersion: record.tlsVersion, routeName: route.name, routeId: route.id, }); try { // Call the handler with socket AND context const result = route.action.socketHandler(socket, routeContext); // Handle async handlers properly if (result instanceof Promise) { result .then(() => { // Emit initial chunk after async handler completes if (initialChunk && initialChunk.length > 0) { socket.emit('data', initialChunk); } }) .catch(error => { logger.log('error', 'Socket handler error', { connectionId, routeName: route.name, error: error.message, component: 'route-handler' }); if (!socket.destroyed) { socket.destroy(); } this.connectionManager.cleanupConnection(record, 'handler_error'); }); } else { // For sync handlers, emit on next tick if (initialChunk && initialChunk.length > 0) { process.nextTick(() => { socket.emit('data', initialChunk); }); } } } catch (error) { logger.log('error', 'Socket handler error', { connectionId, routeName: route.name, error: error.message, component: 'route-handler' }); if (!socket.destroyed) { socket.destroy(); } this.connectionManager.cleanupConnection(record, 'handler_error'); } } /** * Setup improved error handling for the outgoing connection */ private setupOutgoingErrorHandler( connectionId: string, targetSocket: plugins.net.Socket, record: IConnectionRecord, socket: plugins.net.Socket, finalTargetHost: string, finalTargetPort: number ): void { targetSocket.once('error', (err) => { // This handler runs only once during the initial connection phase const code = (err as any).code; logger.log('error', `Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, errorMessage: err.message, errorCode: code, component: 'route-handler' } ); // Resume the incoming socket to prevent it from hanging socket.resume(); // Log specific error types for easier debugging if (code === 'ECONNREFUSED') { logger.log('error', `Connection ${connectionId}: Target ${finalTargetHost}:${finalTargetPort} refused connection. Check if the target service is running and listening on that port.`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, recommendation: 'Check if the target service is running and listening on that port.', component: 'route-handler' } ); } else if (code === 'ETIMEDOUT') { logger.log('error', `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} timed out. Check network conditions, firewall rules, or if the target is too far away.`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, recommendation: 'Check network conditions, firewall rules, or if the target is too far away.', component: 'route-handler' } ); } else if (code === 'ECONNRESET') { logger.log('error', `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} was reset. The target might have closed the connection abruptly.`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, recommendation: 'The target might have closed the connection abruptly.', component: 'route-handler' } ); } else if (code === 'EHOSTUNREACH') { logger.log('error', `Connection ${connectionId}: Host ${finalTargetHost} is unreachable. Check DNS settings, network routing, or firewall rules.`, { connectionId, targetHost: finalTargetHost, recommendation: 'Check DNS settings, network routing, or firewall rules.', component: 'route-handler' } ); } else if (code === 'ENOTFOUND') { logger.log('error', `Connection ${connectionId}: DNS lookup failed for ${finalTargetHost}. Check your DNS settings or if the hostname is correct.`, { connectionId, targetHost: finalTargetHost, recommendation: 'Check your DNS settings or if the hostname is correct.', component: 'route-handler' } ); } // Clear any existing error handler after connection phase targetSocket.removeAllListeners('error'); // Re-add the normal error handler for established connections targetSocket.on('error', this.connectionManager.handleError('outgoing', record)); if (record.outgoingTerminationReason === null) { record.outgoingTerminationReason = 'connection_failed'; this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed'); } // Clean up the connection this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`); }); } /** * Sets up a direct connection to the target */ private setupDirectConnection( socket: plugins.net.Socket, record: IConnectionRecord, serverName?: string, initialChunk?: Buffer, overridePort?: number, targetHost?: string, targetPort?: number ): void { const connectionId = record.id; // Determine target host and port if not provided const finalTargetHost = targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost'; // Determine target port const finalTargetPort = targetPort || record.targetPort || (overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443); // Update record with final target information record.targetHost = finalTargetHost; record.targetPort = finalTargetPort; if (this.settings.enableDetailedLogging) { logger.log('info', `Setting up direct connection ${connectionId} to ${finalTargetHost}:${finalTargetPort}`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, component: 'route-handler' }); } // Setup connection options const connectionOptions: plugins.net.NetConnectOpts = { host: finalTargetHost, port: finalTargetPort, }; // Preserve source IP if configured if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) { connectionOptions.localAddress = record.remoteIP.replace('::ffff:', ''); } // Store initial data if provided if (initialChunk) { record.bytesReceived += initialChunk.length; record.pendingData.push(Buffer.from(initialChunk)); record.pendingDataSize = initialChunk.length; } // Create the target socket const targetSocket = plugins.net.connect(connectionOptions); record.outgoing = targetSocket; record.outgoingStartTime = Date.now(); // Apply socket optimizations targetSocket.setNoDelay(this.settings.noDelay); // Apply keep-alive settings if enabled if (this.settings.keepAlive) { targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); // Apply enhanced TCP keep-alive options if enabled if (this.settings.enableKeepAliveProbes) { try { if ('setKeepAliveProbes' in targetSocket) { (targetSocket as any).setKeepAliveProbes(10); } if ('setKeepAliveInterval' in targetSocket) { (targetSocket as any).setKeepAliveInterval(1000); } } catch (err) { // Ignore errors - these are optional enhancements if (this.settings.enableDetailedLogging) { logger.log('warn', `Enhanced TCP keep-alive not supported for outgoing socket on connection ${connectionId}: ${err}`, { connectionId, error: err, component: 'route-handler' }); } } } } // Setup improved error handling for outgoing connection this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort); // Setup close handlers targetSocket.on('close', this.connectionManager.handleClose('outgoing', record)); socket.on('close', this.connectionManager.handleClose('incoming', record)); // Setup error handlers for incoming socket socket.on('error', this.connectionManager.handleError('incoming', record)); // Handle timeouts with keep-alive awareness socket.on('timeout', () => { // For keep-alive connections, just log a warning instead of closing if (record.hasKeepAlive) { logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, { connectionId, remoteIP: record.remoteIP, timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000), status: 'Connection preserved', component: 'route-handler' }); return; } // For non-keep-alive connections, proceed with normal cleanup logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, { connectionId, remoteIP: record.remoteIP, timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000), component: 'route-handler' }); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'timeout'; this.connectionManager.incrementTerminationStat('incoming', 'timeout'); } this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming'); }); targetSocket.on('timeout', () => { // For keep-alive connections, just log a warning instead of closing if (record.hasKeepAlive) { logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, { connectionId, remoteIP: record.remoteIP, timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000), status: 'Connection preserved', component: 'route-handler' }); return; } // For non-keep-alive connections, proceed with normal cleanup logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, { connectionId, remoteIP: record.remoteIP, timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000), component: 'route-handler' }); if (record.outgoingTerminationReason === null) { record.outgoingTerminationReason = 'timeout'; this.connectionManager.incrementTerminationStat('outgoing', 'timeout'); } this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing'); }); // Apply socket timeouts this.timeoutManager.applySocketTimeouts(record); // Track outgoing data for bytes counting targetSocket.on('data', (chunk: Buffer) => { record.bytesSent += chunk.length; this.timeoutManager.updateActivity(record); }); // Wait for the outgoing connection to be ready before setting up piping targetSocket.once('connect', () => { if (this.settings.enableDetailedLogging) { logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, { connectionId, targetHost: finalTargetHost, targetPort: finalTargetPort, component: 'route-handler' }); } // Clear the initial connection error handler targetSocket.removeAllListeners('error'); // Add the normal error handler for established connections targetSocket.on('error', this.connectionManager.handleError('outgoing', record)); // Flush any pending data to target if (record.pendingData.length > 0) { const combinedData = Buffer.concat(record.pendingData); if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target` ); } // Write pending data immediately targetSocket.write(combinedData, (err) => { if (err) { logger.log('error', `Error writing pending data to target for connection ${connectionId}: ${err.message}`, { connectionId, error: err.message, component: 'route-handler' }); return this.connectionManager.initiateCleanupOnce(record, 'write_error'); } }); // Clear the buffer now that we've processed it record.pendingData = []; record.pendingDataSize = 0; } // Immediately setup bidirectional piping - much simpler than manual data management socket.pipe(targetSocket); targetSocket.pipe(socket); // Track incoming data for bytes counting - do this after piping is set up socket.on('data', (chunk: Buffer) => { record.bytesReceived += chunk.length; this.timeoutManager.updateActivity(record); }); // Log successful connection logger.log('info', `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` + `${serverName ? ` (SNI: ${serverName})` : record.lockedDomain ? ` (Domain: ${record.lockedDomain})` : ''}`, { remoteIP: record.remoteIP, targetHost: finalTargetHost, targetPort: finalTargetPort, sni: serverName || undefined, domain: !serverName && record.lockedDomain ? record.lockedDomain : undefined, component: 'route-handler' } ); // Add TLS renegotiation handler if needed if (serverName) { // Create connection info object for the existing connection const connInfo = { sourceIp: record.remoteIP, sourcePort: record.incoming.remotePort || 0, destIp: record.incoming.localAddress || '', destPort: record.incoming.localPort || 0, }; // Create a renegotiation handler function const renegotiationHandler = this.tlsManager.createRenegotiationHandler( connectionId, serverName, connInfo, (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason) ); // Store the handler in the connection record so we can remove it during cleanup record.renegotiationHandler = renegotiationHandler; // Add the handler to the socket socket.on('data', renegotiationHandler); if (this.settings.enableDetailedLogging) { logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, { connectionId, serverName, component: 'route-handler' }); } } // Set connection timeout record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => { logger.log('warn', `Connection ${connectionId} from ${record.remoteIP} exceeded max lifetime, forcing cleanup`, { connectionId, remoteIP: record.remoteIP, component: 'route-handler' }); this.connectionManager.initiateCleanupOnce(record, reason); }); // Mark TLS handshake as complete for TLS connections if (record.isTLS) { record.tlsHandshakeComplete = true; } }); } }