import * as plugins from '../../plugins.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.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 { 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; // 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 networkProxyBridge: NetworkProxyBridge, 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) { 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.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) { const isAllowed = this.securityManager.isIPAuthorized( remoteIP, defaultSecuritySettings.ipAllowList, defaultSecuritySettings.ipBlockList || [] ); 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, 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}` ); } // Check if this route uses NFTables for forwarding if (route.action.forwardingEngine === 'nftables') { // For NFTables routes, we don't need to do anything at the application level // The packet is forwarded at the kernel level // Log the connection console.log( `[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}` ); // Just close the socket in our application since it's handled at kernel level socket.end(); this.connectionManager.cleanupConnection(record, 'nftables_handled'); return; } // 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); case 'static': this.handleStaticAction(socket, record, route); return; 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; // Check if this route uses NFTables for forwarding if (action.forwardingEngine === 'nftables') { // Log detailed information about NFTables-handled connection if (this.settings.enableDetailedLogging) { console.log( `[${record.id}] Connection forwarded by NFTables (kernel-level): ` + `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})` ); } else { console.log( `[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${ record.localPort } (Route: "${route.name || 'unnamed'}")` ); } // Additional NFTables-specific logging if configured if (action.nftables) { const nftConfig = action.nftables; if (this.settings.enableDetailedLogging) { console.log( `[${record.id}] NFTables config: ` + `protocol=${nftConfig.protocol || 'tcp'}, ` + `preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` + `priority=${nftConfig.priority || 'default'}, ` + `maxRate=${nftConfig.maxRate || 'unlimited'}` ); } } // This connection is handled at the kernel level, no need to process at application level // Close the socket gracefully in our application layer socket.end(); // Mark the connection as handled by NFTables for proper cleanup record.nftablesHandled = true; this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); return; } // 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; } // 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) { console.log( `[${connectionId}] Dynamic host resolved to: ${ Array.isArray(targetHost) ? targetHost.join(', ') : targetHost }` ); } } catch (err) { console.log(`[${connectionId}] Error in host mapping function: ${err}`); 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) { console.log( `[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}` ); } // Store the resolved target port in the context for potential future use routeContext.targetPort = targetPort; } catch (err) { console.log(`[${connectionId}] Error in port mapping function: ${err}`); 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) { console.log(`[${connectionId}] Using TLS passthrough to ${selectedHost}:${targetPort}`); } return this.setupDirectConnection( socket, record, record.lockedDomain, initialChunk, undefined, selectedHost, 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) { this.networkProxyBridge.forwardToNetworkProxy( connectionId, socket, record, initialChunk, this.settings.networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason) ); return; } // 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}` ); } // 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 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'); } /** * Handle a static action for a route */ private async handleStaticAction( socket: plugins.net.Socket, record: IConnectionRecord, route: IRouteConfig ): Promise { const connectionId = record.id; if (!route.action.handler) { console.error(`[${connectionId}] Static route '${route.name}' has no handler`); socket.end(); this.connectionManager.cleanupConnection(record, 'no_handler'); return; } let buffer = Buffer.alloc(0); let processingData = false; const handleHttpData = async (chunk: Buffer) => { // Accumulate the data buffer = Buffer.concat([buffer, chunk]); // Prevent concurrent processing of the same buffer if (processingData) return; processingData = true; try { // Process data until we have a complete request or need more data await processBuffer(); } finally { processingData = false; } }; const processBuffer = async () => { // Look for end of HTTP headers const headerEndIndex = buffer.indexOf('\r\n\r\n'); if (headerEndIndex === -1) { // Need more data if (buffer.length > 8192) { // Prevent excessive buffering console.error(`[${connectionId}] HTTP headers too large`); socket.end(); this.connectionManager.cleanupConnection(record, 'headers_too_large'); } return; // Wait for more data to arrive } // Parse the HTTP request const headerBuffer = buffer.slice(0, headerEndIndex); const headers = headerBuffer.toString(); const lines = headers.split('\r\n'); if (lines.length === 0) { console.error(`[${connectionId}] Invalid HTTP request`); socket.end(); this.connectionManager.cleanupConnection(record, 'invalid_request'); return; } // Parse request line const requestLine = lines[0]; const requestParts = requestLine.split(' '); if (requestParts.length < 3) { console.error(`[${connectionId}] Invalid HTTP request line`); socket.end(); this.connectionManager.cleanupConnection(record, 'invalid_request_line'); return; } const [method, path, httpVersion] = requestParts; // Parse headers const headersMap: Record = {}; for (let i = 1; i < lines.length; i++) { const colonIndex = lines[i].indexOf(':'); if (colonIndex > 0) { const key = lines[i].slice(0, colonIndex).trim().toLowerCase(); const value = lines[i].slice(colonIndex + 1).trim(); headersMap[key] = value; } } // Check for Content-Length to handle request body const requestBodyLength = parseInt(headersMap['content-length'] || '0', 10); const bodyStartIndex = headerEndIndex + 4; // Skip the \r\n\r\n // If there's a body, ensure we have the full body if (requestBodyLength > 0) { const totalExpectedLength = bodyStartIndex + requestBodyLength; // If we don't have the complete body yet, wait for more data if (buffer.length < totalExpectedLength) { // Implement a reasonable body size limit to prevent memory issues if (requestBodyLength > 1024 * 1024) { // 1MB limit console.error(`[${connectionId}] Request body too large`); socket.end(); this.connectionManager.cleanupConnection(record, 'body_too_large'); return; } return; // Wait for more data } } // Extract query string if present let pathname = path; let query: string | undefined; const queryIndex = path.indexOf('?'); if (queryIndex !== -1) { pathname = path.slice(0, queryIndex); query = path.slice(queryIndex + 1); } try { // Get request body if present let requestBody: Buffer | undefined; if (requestBodyLength > 0) { requestBody = buffer.slice(bodyStartIndex, bodyStartIndex + requestBodyLength); } // Pause socket to prevent data loss during async processing socket.pause(); // Remove the data listener since we're handling the request socket.removeListener('data', handleHttpData); // Build route context with parsed HTTP information const context: IRouteContext = { port: record.localPort, domain: record.lockedDomain || headersMap['host']?.split(':')[0], clientIp: record.remoteIP, serverIp: socket.localAddress!, path: pathname, query: query, headers: headersMap, method: method, isTls: record.isTLS, tlsVersion: record.tlsVersion, routeName: route.name, routeId: route.id, timestamp: Date.now(), connectionId, }; // Since IRouteContext doesn't have a body property, // we need an alternative approach to handle the body let response; if (requestBody) { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Processing request with body (${requestBody.length} bytes)` ); } // Pass the body as an additional parameter by extending the context object // This is not type-safe, but it allows handlers that expect a body to work const extendedContext = { ...context, // Provide both raw buffer and string representation requestBody: requestBody, requestBodyText: requestBody.toString(), }; // Call the handler with the extended context // The handler needs to know to look for the non-standard properties response = await route.action.handler(extendedContext as any); } else { // Call the handler with the standard context response = await route.action.handler(context); } // Prepare the HTTP response const responseHeaders = response.headers || {}; const contentLength = Buffer.byteLength(response.body || ''); responseHeaders['Content-Length'] = contentLength.toString(); if (!responseHeaders['Content-Type']) { responseHeaders['Content-Type'] = 'text/plain'; } // Build the response let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`; for (const [key, value] of Object.entries(responseHeaders)) { httpResponse += `${key}: ${value}\r\n`; } httpResponse += '\r\n'; // Send response socket.write(httpResponse); if (response.body) { socket.write(response.body); } socket.end(); this.connectionManager.cleanupConnection(record, 'completed'); } catch (error) { console.error(`[${connectionId}] Error in static handler: ${error}`); // Send error response const errorResponse = 'HTTP/1.1 500 Internal Server Error\r\n' + 'Content-Type: text/plain\r\n' + 'Content-Length: 21\r\n' + '\r\n' + 'Internal Server Error'; socket.write(errorResponse); socket.end(); this.connectionManager.cleanupConnection(record, 'handler_error'); } }; // Listen for data socket.on('data', handleHttpData); // Ensure cleanup on socket close socket.once('close', () => { socket.removeListener('data', handleHttpData); }); } /** * 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) { console.log( `[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}` ); } // 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'); } // Route-based configuration doesn't use domain handlers // 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})` : record.lockedDomain ? ` (Domain: ${record.lockedDomain})` : '' }` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ record.hasKeepAlive ? 'Yes' : 'No' }` ); } else { console.log( `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` + `${ serverName ? ` (SNI: ${serverName})` : record.lockedDomain ? ` (Domain: ${record.lockedDomain})` : '' }` ); } // 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}` ); } } }); } } // Helper function for status text function getStatusText(status: number): string { const statusTexts: Record = { 200: 'OK', 404: 'Not Found', 500: 'Internal Server Error', }; return statusTexts[status] || 'Unknown'; }