diff --git a/changelog.md b/changelog.md index 5e01976..953899f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-12 - 3.41.8 - fix(portproxy) +Improve TLS handshake timeout handling and connection piping in PortProxy + +- Increase the default initial handshake timeout from 60 seconds to 120 seconds +- Add a 30-second grace period before terminating connections waiting for initial TLS data +- Refactor piping logic by removing redundant callback and establishing piping immediately after flushing buffered data +- Enhance debug logging during TLS ClientHello processing for improved SNI extraction insights + ## 2025-03-12 - 3.41.7 - fix(core) Refactor PortProxy and SniHandler: improve configuration handling, logging, and whitespace consistency diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 9d90f8c..08a5ec0 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '3.41.7', + version: '3.41.8', description: '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.' } diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts index 9f182d9..e9c3968 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -216,7 +216,7 @@ export class PortProxy { targetIP: settingsArg.targetIP || 'localhost', // Timeout settings with reasonable defaults - initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake + initialDataTimeout: settingsArg.initialDataTimeout || 120000, // 120 seconds for initial handshake socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default @@ -853,78 +853,76 @@ export class PortProxy { // Process any remaining data in the queue before switching to piping processDataQueue(); - - // Setup function to establish piping - we'll use this after flushing data - const setupPiping = () => { - // Mark that we're switching to piping mode - pipingEstablished = true; - - // Setup piping in both directions - 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(', ')})` - : '' - }` - ); - } - }; - + + // Set up piping immediately - don't delay this crucial step + 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.initiateCleanupOnce(record, 'write_error'); } - - // Establish piping now that we've flushed the buffered data - setupPiping(); }); + + // 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 - CRITICAL! + 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 { - // No pending data, just establish piping immediately - setupPiping(); + console.log( + `Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` + + `${ + serverName + ? ` (SNI: ${serverName})` + : domainConfig + ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` + : '' + }` + ); } - // Clear the buffer now that we've processed it - record.pendingData = []; - record.pendingDataSize = 0; // Add the renegotiation handler for SNI validation with strict domain enforcement // This will be called after we've established piping @@ -1575,15 +1573,20 @@ export class PortProxy { // Set an initial timeout for handshake data let initialTimeout: NodeJS.Timeout | null = setTimeout(() => { if (!initialDataReceived) { - console.log( - `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}` - ); - if (connectionRecord.incomingTerminationReason === null) { - connectionRecord.incomingTerminationReason = 'initial_timeout'; - this.incrementTerminationStat('incoming', 'initial_timeout'); - } - socket.end(); - this.cleanupConnection(connectionRecord, 'initial_timeout'); + console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`); + + // Add a grace period instead of immediate termination + setTimeout(() => { + if (!initialDataReceived) { + console.log(`[${connectionId}] Final initial data timeout after grace period`); + if (connectionRecord.incomingTerminationReason === null) { + connectionRecord.incomingTerminationReason = 'initial_timeout'; + this.incrementTerminationStat('incoming', 'initial_timeout'); + } + socket.end(); + this.cleanupConnection(connectionRecord, 'initial_timeout'); + } + }, 30000); // 30 second grace period } }, this.settings.initialDataTimeout!); @@ -1764,15 +1767,20 @@ export class PortProxy { if (this.settings.sniEnabled) { initialTimeout = setTimeout(() => { if (!initialDataReceived) { - console.log( - `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}` - ); - if (connectionRecord.incomingTerminationReason === null) { - connectionRecord.incomingTerminationReason = 'initial_timeout'; - this.incrementTerminationStat('incoming', 'initial_timeout'); - } - socket.end(); - this.cleanupConnection(connectionRecord, 'initial_timeout'); + console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`); + + // Add a grace period instead of immediate termination + setTimeout(() => { + if (!initialDataReceived) { + console.log(`[${connectionId}] Final initial data timeout after grace period`); + if (connectionRecord.incomingTerminationReason === null) { + connectionRecord.incomingTerminationReason = 'initial_timeout'; + this.incrementTerminationStat('incoming', 'initial_timeout'); + } + socket.end(); + this.cleanupConnection(connectionRecord, 'initial_timeout'); + } + }, 30000); // 30 second grace period } }, this.settings.initialDataTimeout!); @@ -2024,30 +2032,29 @@ export class PortProxy { initialDataReceived = false; socket.once('data', (chunk: Buffer) => { + // Clear timeout immediately if (initialTimeout) { clearTimeout(initialTimeout); initialTimeout = null; } - + initialDataReceived = true; - - // ADD THE DEBUGGING CODE RIGHT HERE, BEFORE ANY OTHER PROCESSING - if (SniHandler.isClientHello(chunk)) { - // Log more details to understand session resumption - const resumptionInfo = SniHandler.hasSessionResumption(chunk, true); - console.log( - `[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}` - ); - - // Try both extraction methods - const standardSNI = SniHandler.extractSNI(chunk, true); - const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true); - - console.log( - `[${connectionId}] SNI extraction results: standardSNI=${ - standardSNI || 'none' - }, pskSNI=${pskSNI || 'none'}` - ); + + // Add debugging ONLY if detailed logging is enabled - avoid heavy processing + if (this.settings.enableTlsDebugLogging && SniHandler.isClientHello(chunk)) { + // Move heavy debug logging to a separate async task to not block the flow + setImmediate(() => { + try { + const resumptionInfo = SniHandler.hasSessionResumption(chunk, true); + const standardSNI = SniHandler.extractSNI(chunk, true); + const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true); + + console.log(`[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}`); + console.log(`[${connectionId}] SNI extraction results: standardSNI=${standardSNI || 'none'}, pskSNI=${pskSNI || 'none'}`); + } catch (err) { + console.log(`[${connectionId}] Error in debug logging: ${err}`); + } + }); } // Block non-TLS connections on port 443