UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.

754 lines 77.7 kB
import * as plugins from './plugins.js'; import { ConnectionManager } from './classes.pp.connectionmanager.js'; import { SecurityManager } from './classes.pp.securitymanager.js'; import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; import { TlsManager } from './classes.pp.tlsmanager.js'; import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; import { TimeoutManager } from './classes.pp.timeoutmanager.js'; import { PortRangeManager } from './classes.pp.portrangemanager.js'; /** * Handles new connection processing and setup logic */ export class ConnectionHandler { constructor(settings, connectionManager, securityManager, domainConfigManager, tlsManager, networkProxyBridge, timeoutManager, portRangeManager) { this.settings = settings; this.connectionManager = connectionManager; this.securityManager = securityManager; this.domainConfigManager = domainConfigManager; this.tlsManager = tlsManager; this.networkProxyBridge = networkProxyBridge; this.timeoutManager = timeoutManager; this.portRangeManager = portRangeManager; } /** * Handle a new incoming connection */ handleConnection(socket) { 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.setKeepAliveProbes(10); } if ('setKeepAliveInterval' in socket) { socket.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()}`); } // Check if this connection should be forwarded directly to NetworkProxy if (this.portRangeManager.shouldUseNetworkProxy(localPort)) { this.handleNetworkProxyConnection(socket, record); } else { // For non-NetworkProxy ports, proceed with normal processing this.handleStandardConnection(socket, record); } } /** * Handle a connection that should be forwarded to NetworkProxy */ handleNetworkProxyConnection(socket, record) { const connectionId = record.id; let initialDataReceived = false; // Set an initial timeout for handshake data let initialTimeout = setTimeout(() => { if (!initialDataReceived) { console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`); // Add a grace period instead of immediate termination 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); // 30 second grace period } }, 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 for NetworkProxy socket.once('data', (chunk) => { // 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 const localPort = record.localPort; 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 if (this.tlsManager.isTlsHandshake(chunk)) { record.isTLS = true; // Check for ClientHello to extract SNI - but don't enforce it for NetworkProxy 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 for domain-specific NetworkProxy handling if available const serverName = this.tlsManager.extractSNI(chunk, connInfo); // For NetworkProxy connections, we'll allow session tickets even without SNI // We'll only use the serverName if available to determine the specific NetworkProxy port if (serverName) { // Save domain config and SNI in connection record const domainConfig = this.domainConfigManager.findDomainConfig(serverName); record.domainConfig = domainConfig; record.lockedDomain = serverName; // Use domain-specific NetworkProxy port if configured if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) { const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig); if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`); } // Forward to NetworkProxy with domain-specific port this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, chunk, networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason)); return; } } else if (this.settings.allowSessionTicket === false && this.settings.enableDetailedLogging) { // Log that we're allowing a session resumption without SNI for NetworkProxy console.log(`[${connectionId}] Allowing session resumption without SNI for NetworkProxy forwarding`); } } // Forward directly to NetworkProxy without domain-specific settings this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, chunk, undefined, (reason) => this.connectionManager.initiateCleanupOnce(record, reason)); } else { // If not TLS, use normal direct connection console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`); this.setupDirectConnection(socket, record, undefined, undefined, chunk); } }); } /** * Handle a standard (non-NetworkProxy) connection */ handleStandardConnection(socket, record) { const connectionId = record.id; const localPort = record.localPort; // Define helpers for rejecting connections const rejectIncomingConnection = (reason, logMessage) => { console.log(`[${connectionId}] ${logMessage}`); socket.end(); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = reason; this.connectionManager.incrementTerminationStat('incoming', reason); } this.connectionManager.cleanupConnection(record, reason); }; let initialDataReceived = false; // Set an initial timeout for SNI data if needed let initialTimeout = null; if (this.settings.sniEnabled) { initialTimeout = setTimeout(() => { if (!initialDataReceived) { console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`); // Add a grace period instead of immediate termination 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); // 30 second grace period } }, this.settings.initialDataTimeout); // Make sure timeout doesn't keep the process alive if (initialTimeout.unref) { initialTimeout.unref(); } } else { initialDataReceived = true; record.hasReceivedInitialData = true; } socket.on('error', this.connectionManager.handleError('incoming', record)); // Track data for bytes counting socket.on('data', (chunk) => { record.bytesReceived += chunk.length; this.timeoutManager.updateActivity(record); // Check for TLS handshake if this is the first chunk if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) { record.isTLS = true; if (this.settings.enableTlsDebugLogging) { console.log(`[${connectionId}] TLS handshake detected from ${record.remoteIP}, ${chunk.length} bytes`); } } }); /** * Sets up the connection to the target host. */ const setupConnection = (serverName, initialChunk, forcedDomain, overridePort) => { // Clear the initial timeout since we've received data if (initialTimeout) { clearTimeout(initialTimeout); initialTimeout = null; } // Mark that we've received initial data initialDataReceived = true; record.hasReceivedInitialData = true; // Check if this looks like a TLS handshake if (initialChunk && this.tlsManager.isTlsHandshake(initialChunk)) { record.isTLS = true; if (this.settings.enableTlsDebugLogging) { console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`); } } // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup. const domainConfig = forcedDomain ? forcedDomain : serverName ? this.domainConfigManager.findDomainConfig(serverName) : undefined; // Save domain config in connection record record.domainConfig = domainConfig; // Check if this domain should use NetworkProxy (domain-specific setting) if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig) && this.networkProxyBridge.getNetworkProxy()) { if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`); } const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig); if (initialChunk && record.isTLS) { // For TLS connections with initial chunk, forward to NetworkProxy this.networkProxyBridge.forwardToNetworkProxy(connectionId, socket, record, initialChunk, networkProxyPort, (reason) => this.connectionManager.initiateCleanupOnce(record, reason)); return; // Skip normal connection setup } } // IP validation if (domainConfig) { const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig); // Skip IP validation if allowedIPs is empty if (domainConfig.allowedIPs.length > 0 && !this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) { return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`); } } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { if (!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) { return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed by default allowed list`); } } // Save the initial SNI if (serverName) { record.lockedDomain = serverName; } // Set up the direct connection this.setupDirectConnection(socket, record, domainConfig, serverName, initialChunk, overridePort); }; // --- PORT RANGE-BASED HANDLING --- // Only apply port-based rules if the incoming port is within one of the global port ranges. if (this.portRangeManager.isPortInGlobalRanges(localPort)) { if (this.portRangeManager.shouldUseGlobalForwarding(localPort)) { if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)) { console.log(`[${connectionId}] Connection from ${record.remoteIP} rejected: IP ${record.remoteIP} not allowed in global default allowed list.`); socket.end(); return; } if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`); } setupConnection('', undefined, { domains: ['global'], allowedIPs: this.settings.defaultAllowedIPs || [], blockedIPs: this.settings.defaultBlockedIPs || [], targetIPs: [this.settings.targetIP], portRanges: [], }, localPort); return; } else { // Attempt to find a matching forced domain config based on the local port. const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort); if (forcedDomain) { const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain); if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) { console.log(`[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`); socket.end(); return; } if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`); } setupConnection('', undefined, forcedDomain, localPort); return; } // Fall through to SNI/default handling if no forced domain config is found. } } // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) --- if (this.settings.sniEnabled) { initialDataReceived = false; socket.once('data', (chunk) => { // Clear timeout immediately if (initialTimeout) { clearTimeout(initialTimeout); initialTimeout = null; } initialDataReceived = 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 in SNI handler. ` + `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; } // Try to extract SNI let serverName = ''; if (this.tlsManager.isTlsHandshake(chunk)) { record.isTLS = true; if (this.settings.enableTlsDebugLogging) { console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`); } // Create connection info object 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) || ''; // If allowSessionTicket is false and this is a ClientHello with no SNI, terminate the connection if (this.settings.allowSessionTicket === false && this.tlsManager.isClientHello(chunk) && !serverName) { // Block ClientHello without SNI when allowSessionTicket is false console.log(`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` + `Sending warning unrecognized_name alert to encourage immediate retry with SNI.`); // Set the termination reason first if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'session_ticket_blocked_no_sni'; this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni'); } // Create a warning-level alert for unrecognized_name // This encourages Chrome to retry immediately with SNI const serverNameUnknownAlertData = Buffer.from([ 0x15, // Alert record type 0x03, 0x03, // TLS 1.2 version 0x00, 0x02, // Length 0x01, // Warning alert level (not fatal) 0x70, // unrecognized_name alert (code 112) ]); try { // Use cork/uncork to ensure the alert is sent as a single packet socket.cork(); const writeSuccessful = socket.write(serverNameUnknownAlertData); socket.uncork(); socket.end(); // Function to handle the clean socket termination - but more gradually const finishConnection = () => { this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); }; if (writeSuccessful) { // Wait longer before ending connection to ensure alert is processed by client setTimeout(finishConnection, 200); // Increased from 50ms to 200ms } else { // If the kernel buffer was full, wait for the drain event socket.once('drain', () => { // Wait longer after drain as well setTimeout(finishConnection, 200); }); // Safety timeout is increased too setTimeout(() => { socket.removeAllListeners('drain'); finishConnection(); }, 400); // Increased from 250ms to 400ms } } catch (err) { // If we can't send the alert, fall back to immediate termination console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`); socket.end(); this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni'); } return; } } // Lock the connection to the negotiated SNI. record.lockedDomain = serverName; if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Received connection from ${record.remoteIP} with SNI: ${serverName || '(empty)'}`); } setupConnection(serverName, chunk); }); } else { initialDataReceived = true; record.hasReceivedInitialData = true; if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)) { return rejectIncomingConnection('rejected', `Connection rejected: IP ${record.remoteIP} not allowed for non-SNI connection`); } setupConnection(''); } } /** * Sets up a direct connection to the target */ setupDirectConnection(socket, record, domainConfig, serverName, initialChunk, overridePort) { const connectionId = record.id; // Determine target host const targetHost = domainConfig ? this.domainConfigManager.getTargetIP(domainConfig) : this.settings.targetIP; // Determine target port const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort; // Setup connection options const connectionOptions = { host: targetHost, port: targetPort, }; // Preserve source IP if configured if (this.settings.preserveSourceIP) { connectionOptions.localAddress = record.remoteIP.replace('::ffff:', ''); } // Create a safe queue for incoming data const dataQueue = []; 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) => { // 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.setKeepAliveProbes(10); } if ('setKeepAliveInterval' in targetSocket) { targetSocket.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.code; console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`); // Resume the incoming socket to prevent it from hanging socket.resume(); if (code === 'ECONNREFUSED') { console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`); } else if (code === 'ETIMEDOUT') { console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`); } else if (code === 'ECONNRESET') { console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`); } else if (code === 'EHOSTUNREACH') { console.log(`[${connectionId}] Host ${targetHost} 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'); } // 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) => { 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} -> ${targetHost}:${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} -> ${targetHost}:${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}`); } } }); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5wcC5jb25uZWN0aW9uaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMucHAuY29ubmVjdGlvbmhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFNeEMsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sbUNBQW1DLENBQUM7QUFDdEUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ2xFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQzFFLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxvQ0FBb0MsQ0FBQztBQUN4RSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDaEUsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFFcEU7O0dBRUc7QUFDSCxNQUFNLE9BQU8saUJBQWlCO0lBQzVCLFlBQ1UsUUFBNEIsRUFDNUIsaUJBQW9DLEVBQ3BDLGVBQWdDLEVBQ2hDLG1CQUF3QyxFQUN4QyxVQUFzQixFQUN0QixrQkFBc0MsRUFDdEMsY0FBOEIsRUFDOUIsZ0JBQWtDO1FBUGxDLGFBQVEsR0FBUixRQUFRLENBQW9CO1FBQzVCLHNCQUFpQixHQUFqQixpQkFBaUIsQ0FBbUI7UUFDcEMsb0JBQWUsR0FBZixlQUFlLENBQWlCO1FBQ2hDLHdCQUFtQixHQUFuQixtQkFBbUIsQ0FBcUI7UUFDeEMsZUFBVSxHQUFWLFVBQVUsQ0FBWTtRQUN0Qix1QkFBa0IsR0FBbEIsa0JBQWtCLENBQW9CO1FBQ3RDLG1CQUFjLEdBQWQsY0FBYyxDQUFnQjtRQUM5QixxQkFBZ0IsR0FBaEIsZ0JBQWdCLENBQWtCO0lBQ3pDLENBQUM7SUFFSjs7T0FFRztJQUNJLGdCQUFnQixD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