import * as plugins from '../../plugins.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import { isRoutedOptions } from './models/interfaces.js'; import type { IRouteConfig, IRouteAction } from './models/route-types.js'; import { ConnectionManager } from './connection-manager.js'; import { SecurityManager } from './security-manager.js'; import { TlsManager } from './tls-manager.js'; import { NetworkProxyBridge } from './network-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; constructor( settings: ISmartProxyOptions, private connectionManager: ConnectionManager, private securityManager: SecurityManager, private tlsManager: TlsManager, private networkProxyBridge: NetworkProxyBridge, private timeoutManager: TimeoutManager, private routeManager: RouteManager ) { this.settings = settings; } /** * 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) { console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`); 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) { console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`); } } } } if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + `Active connections: ${this.connectionManager.getConnectionCount()}` ); } else { console.log( `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}` ); } // 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) { console.log( `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}` ); // Add a grace period setTimeout(() => { if (!initialDataReceived) { console.log(`[${connectionId}] Final initial data timeout after grace period`); 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) { console.log( `[${connectionId}] Non-TLS connection detected on port 443. ` + `Terminating connection - only TLS traffic is allowed on standard HTTPS port.` ); 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) { console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`); 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) { console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`); } } } // 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) { console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`); // No matching route, use default/fallback handling console.log(`[${connectionId}] Using default route handling for connection`); // Check default security settings const defaultSecuritySettings = this.settings.defaults?.security; if (defaultSecuritySettings) { if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) { const isAllowed = this.securityManager.isIPAuthorized( remoteIP, defaultSecuritySettings.allowedIps, defaultSecuritySettings.blockedIps || [] ); if (!isAllowed) { console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`); 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, undefined, serverName, initialChunk, undefined, targetHost, targetPort ); } else { // No default target available, terminate the connection console.log(`[${connectionId}] No default target configured. Closing connection.`); socket.end(); this.connectionManager.cleanupConnection(record, 'no_default_target'); return; } } // A matching route was found const route = routeMatch.route; if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${serverName || 'connection'} on port ${localPort}` ); } // Handle the route based on its action type switch (route.action.type) { case 'forward': return this.handleForwardAction(socket, record, route, initialChunk); case 'redirect': return this.handleRedirectAction(socket, record, route); case 'block': return this.handleBlockAction(socket, record, route); default: console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`); 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; // We should have a target configuration for forwarding if (!action.target) { console.log(`[${connectionId}] Forward action missing target configuration`); socket.end(); this.connectionManager.cleanupConnection(record, 'missing_target'); return; } // 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) { console.log(`[${connectionId}] Using TLS passthrough to ${action.target.host}`); } // Allow for array of hosts const targetHost = Array.isArray(action.target.host) ? action.target.host[Math.floor(Math.random() * action.target.host.length)] : action.target.host; // Determine target port - either target port or preserve incoming port const targetPort = action.target.preservePort ? record.localPort : action.target.port; return this.setupDirectConnection( socket, record, undefined, record.lockedDomain, initialChunk, undefined, targetHost, targetPort ); case 'terminate': case 'terminate-and-reencrypt': // For TLS termination, use NetworkProxy if (this.networkProxyBridge.getNetworkProxy()) { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}` ); } // If we have an initial chunk with TLS data, start processing it if (initialChunk && record.isTLS) { return this.networkProxyBridge.forwardToNetworkProxy( connectionId, socket, record, initialChunk, this.settings.networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason) ); } // This shouldn't normally happen - we should have TLS data at this point console.log(`[${connectionId}] TLS termination route without TLS data`); socket.end(); this.connectionManager.cleanupConnection(record, 'tls_error'); return; } else { console.log(`[${connectionId}] NetworkProxy not available for TLS termination`); socket.end(); this.connectionManager.cleanupConnection(record, 'no_network_proxy'); return; } } } else { // No TLS settings - basic forwarding if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`); } // Allow for array of hosts const targetHost = Array.isArray(action.target.host) ? action.target.host[Math.floor(Math.random() * action.target.host.length)] : action.target.host; // Determine target port - either target port or preserve incoming port const targetPort = action.target.preservePort ? record.localPort : action.target.port; return this.setupDirectConnection( socket, record, undefined, record.lockedDomain, initialChunk, undefined, targetHost, targetPort ); } } /** * Handle a redirect action for a route */ private handleRedirectAction( socket: plugins.net.Socket, record: IConnectionRecord, route: IRouteConfig ): void { const connectionId = record.id; const action = route.action; // We should have a redirect configuration if (!action.redirect) { console.log(`[${connectionId}] Redirect action missing redirect configuration`); socket.end(); this.connectionManager.cleanupConnection(record, 'missing_redirect'); return; } // For TLS connections, we can't do redirects at the TCP level if (record.isTLS) { console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`); socket.end(); this.connectionManager.cleanupConnection(record, 'tls_redirect_error'); return; } // Wait for the first HTTP request to perform the redirect const dataListeners: ((chunk: Buffer) => void)[] = []; const httpDataHandler = (chunk: Buffer) => { // Remove all data listeners to avoid duplicated processing for (const listener of dataListeners) { socket.removeListener('data', listener); } // Parse HTTP request to get path try { const headersEnd = chunk.indexOf('\r\n\r\n'); if (headersEnd === -1) { // Not a complete HTTP request, need more data socket.once('data', httpDataHandler); dataListeners.push(httpDataHandler); return; } const httpHeaders = chunk.slice(0, headersEnd).toString(); const requestLine = httpHeaders.split('\r\n')[0]; const [method, path] = requestLine.split(' '); // Extract Host header const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i); const host = hostMatch ? hostMatch[1].trim() : record.lockedDomain || ''; // Process the redirect URL with template variables let redirectUrl = action.redirect.to; redirectUrl = redirectUrl.replace(/\{domain\}/g, host); redirectUrl = redirectUrl.replace(/\{path\}/g, path || ''); redirectUrl = redirectUrl.replace(/\{port\}/g, record.localPort.toString()); // Prepare the HTTP redirect response const redirectResponse = [ `HTTP/1.1 ${action.redirect.status} Moved`, `Location: ${redirectUrl}`, 'Connection: close', 'Content-Length: 0', '', '' ].join('\r\n'); if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`); } // Send the redirect response socket.end(redirectResponse); this.connectionManager.initiateCleanupOnce(record, 'redirect_complete'); } catch (err) { console.log(`[${connectionId}] Error processing HTTP redirect: ${err}`); socket.end(); this.connectionManager.initiateCleanupOnce(record, 'redirect_error'); } }; // Setup the HTTP data handler socket.once('data', httpDataHandler); dataListeners.push(httpDataHandler); } /** * Handle a block action for a route */ private handleBlockAction( socket: plugins.net.Socket, record: IConnectionRecord, route: IRouteConfig ): void { const connectionId = record.id; if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`); } // Simply close the connection socket.end(); this.connectionManager.initiateCleanupOnce(record, 'route_blocked'); } /** * Legacy connection handling has been removed in favor of pure route-based approach */ /** * Sets up a direct connection to the target */ private setupDirectConnection( socket: plugins.net.Socket, record: IConnectionRecord, _unused?: any, // kept for backward compatibility 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 || (this.settings.defaults?.target?.host || 'localhost'); // Determine target port const finalTargetPort = targetPort || (overridePort !== undefined ? overridePort : (this.settings.defaults?.target?.port || 443)); // 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:', ''); } // Create a safe queue for incoming data const dataQueue: Buffer[] = []; let queueSize = 0; let processingQueue = false; let drainPending = false; let pipingEstablished = false; // Pause the incoming socket to prevent buffer overflows socket.pause(); // Function to safely process the data queue without losing events const processDataQueue = () => { if (processingQueue || dataQueue.length === 0 || pipingEstablished) return; processingQueue = true; try { // Process all queued chunks with the current active handler while (dataQueue.length > 0) { const chunk = dataQueue.shift()!; queueSize -= chunk.length; // Once piping is established, we shouldn't get here, // but just in case, pass to the outgoing socket directly if (pipingEstablished && record.outgoing) { record.outgoing.write(chunk); continue; } // Track bytes received record.bytesReceived += chunk.length; // Check for TLS handshake if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) { record.isTLS = true; if (this.settings.enableTlsDebugLogging) { console.log( `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes` ); } } // Check if adding this chunk would exceed the buffer limit const newSize = record.pendingDataSize + chunk.length; if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) { console.log( `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes` ); socket.end(); // Gracefully close the socket this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded'); return; } // Buffer the chunk and update the size counter record.pendingData.push(Buffer.from(chunk)); record.pendingDataSize = newSize; this.timeoutManager.updateActivity(record); } } finally { processingQueue = false; // If there's a pending drain and we've processed everything, // signal we're ready for more data if we haven't established piping yet if (drainPending && dataQueue.length === 0 && !pipingEstablished) { drainPending = false; socket.resume(); } } }; // Unified data handler that safely queues incoming data const safeDataHandler = (chunk: Buffer) => { // If piping is already established, just let the pipe handle it if (pipingEstablished) return; // Add to our queue for orderly processing dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe queueSize += chunk.length; // If queue is getting large, pause socket until we catch up if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) { socket.pause(); drainPending = true; } // Process the queue processDataQueue(); }; // Add our safe data handler socket.on('data', safeDataHandler); // Add initial chunk to pending data if present if (initialChunk) { record.bytesReceived += initialChunk.length; record.pendingData.push(Buffer.from(initialChunk)); record.pendingDataSize = initialChunk.length; } // Create the target socket but don't set up piping immediately 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 to the outgoing connection as well 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) { console.log( `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}` ); } } } } // Setup specific error handler for connection phase targetSocket.once('error', (err) => { // This handler runs only once during the initial connection phase const code = (err as any).code; console.log( `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})` ); // Resume the incoming socket to prevent it from hanging socket.resume(); if (code === 'ECONNREFUSED') { console.log( `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection` ); } else if (code === 'ETIMEDOUT') { console.log( `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} timed out` ); } else if (code === 'ECONNRESET') { console.log( `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} was reset` ); } else if (code === 'EHOSTUNREACH') { console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`); } // 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'); } // If we have a forwarding handler for this domain, let it handle the error if (domainConfig) { try { const forwardingHandler = this.domainConfigManager.getForwardingHandler(domainConfig); forwardingHandler.emit('connection_error', { socket, error: err, connectionId }); } catch (handlerErr) { // If getting the handler fails, just log and continue with normal cleanup console.log(`Error getting forwarding handler for error handling: ${handlerErr}`); } } // Clean up the connection this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`); }); // Setup close handler targetSocket.on('close', this.connectionManager.handleClose('outgoing', record)); socket.on('close', this.connectionManager.handleClose('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) { console.log( `[${connectionId}] Timeout event on incoming keep-alive connection from ${ record.remoteIP } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); return; } // For non-keep-alive connections, proceed with normal cleanup console.log( `[${connectionId}] Timeout on incoming side from ${ record.remoteIP } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); 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) { console.log( `[${connectionId}] Timeout event on outgoing keep-alive connection from ${ record.remoteIP } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); return; } // For non-keep-alive connections, proceed with normal cleanup console.log( `[${connectionId}] Timeout on outgoing side from ${ record.remoteIP } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); 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', () => { // 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)); // Process any remaining data in the queue before switching to piping processDataQueue(); // Set up piping immediately pipingEstablished = true; // Flush all 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) { console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`); return this.connectionManager.initiateCleanupOnce(record, 'write_error'); } }); // Clear the buffer now that we've processed it record.pendingData = []; record.pendingDataSize = 0; } // Setup piping in both directions without any delays socket.pipe(targetSocket); targetSocket.pipe(socket); // Resume the socket to ensure data flows socket.resume(); // Process any data that might be queued in the interim if (dataQueue.length > 0) { // Write any remaining queued data directly to the target socket for (const chunk of dataQueue) { targetSocket.write(chunk); } // Clear the queue dataQueue.length = 0; queueSize = 0; } if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` + `${ serverName ? ` (SNI: ${serverName})` : domainConfig ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ record.hasKeepAlive ? 'Yes' : 'No' }` ); } else { console.log( `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` + `${ serverName ? ` (SNI: ${serverName})` : domainConfig ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` ); } // Add the renegotiation handler for SNI validation 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) { console.log( `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}` ); if (this.settings.allowSessionTicket === false) { console.log( `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.` ); } } } // Set connection timeout record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => { console.log( `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.` ); this.connectionManager.initiateCleanupOnce(record, reason); }); // Mark TLS handshake as complete for TLS connections if (record.isTLS) { record.tlsHandshakeComplete = true; if (this.settings.enableTlsDebugLogging) { console.log( `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}` ); } } }); } }