diff --git a/changelog.md b/changelog.md index 1a73c76..36557df 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2025-03-10 - 3.30.1 - fix(PortProxy) +Improve TLS keep-alive management and fix whitespace formatting + +- Implemented better handling for TLS keep-alive connections after sleep or long inactivity. +- Reformatted whitespace for better readability and consistency. + ## 2025-03-08 - 3.30.0 - feat(PortProxy) Add advanced TLS keep-alive handling and system sleep detection diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 917d85e..1286389 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.30.0', + version: '3.30.1', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.' } diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts index 2d0361e..828efb2 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -10,7 +10,7 @@ export interface IDomainConfig { portRanges?: Array<{ from: number; to: number }>; // Optional port ranges // Allow domain-specific timeout override connectionTimeout?: number; // Connection timeout override (ms) - + // New properties for NetworkProxy integration useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0) @@ -54,12 +54,12 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { // Rate limiting and security maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP - + // Enhanced keep-alive settings keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) - + // New property for NetworkProxy integration networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination } @@ -90,17 +90,17 @@ interface IConnectionRecord { tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete hasReceivedInitialData: boolean; // Whether initial data has been received domainConfig?: IDomainConfig; // Associated domain config for this connection - + // Keep-alive tracking hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued incomingTerminationReason?: string | null; // Reason for incoming termination outgoingTerminationReason?: string | null; // Reason for outgoing termination - + // New field for NetworkProxy tracking usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy networkProxyIndex?: number; // Which NetworkProxy instance is being used - + // Sleep detection fields possibleSystemSleep?: boolean; // Flag to indicate a possible system sleep was detected lastSleepDetection?: number; // Timestamp of the last sleep detection @@ -352,7 +352,7 @@ export class PortProxy { // Connection tracking by IP for rate limiting private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); - + // New property to store NetworkProxy instances private networkProxies: NetworkProxy[] = []; @@ -379,8 +379,8 @@ export class PortProxy { // Feature flags disableInactivityCheck: settingsArg.disableInactivityCheck || false, - enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined - ? settingsArg.enableKeepAliveProbes : true, // Enable by default + enableKeepAliveProbes: + settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default @@ -388,13 +388,13 @@ export class PortProxy { // Rate limiting defaults maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute - + // Enhanced keep-alive settings keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days }; - + // Store NetworkProxy instances if provided this.networkProxies = settingsArg.networkProxies || []; } @@ -417,58 +417,66 @@ export class PortProxy { serverName?: string ): void { // Determine which NetworkProxy to use - const proxyIndex = domainConfig.networkProxyIndex !== undefined - ? domainConfig.networkProxyIndex - : 0; - + const proxyIndex = + domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0; + // Validate the NetworkProxy index if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) { - console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`); + console.log( + `[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.` + ); // Fall back to direct connection - return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData); + return this.setupDirectConnection( + connectionId, + socket, + record, + domainConfig, + serverName, + initialData + ); } - + const networkProxy = this.networkProxies[proxyIndex]; const proxyPort = networkProxy.getListeningPort(); const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}` ); } - + // Create a connection to the NetworkProxy const proxySocket = plugins.net.connect({ host: proxyHost, - port: proxyPort + port: proxyPort, }); - + // Store the outgoing socket in the record record.outgoing = proxySocket; record.outgoingStartTime = Date.now(); record.usingNetworkProxy = true; record.networkProxyIndex = proxyIndex; - + // Set up error handlers proxySocket.on('error', (err) => { console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`); this.cleanupConnection(record, 'network_proxy_connect_error'); }); - + // Handle connection to NetworkProxy proxySocket.on('connect', () => { if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`); } - + // First send the initial data that contains the TLS ClientHello proxySocket.write(initialData); - + // Now set up bidirectional piping between client and NetworkProxy socket.pipe(proxySocket); proxySocket.pipe(socket); - + // Setup cleanup handlers proxySocket.on('close', () => { if (this.settings.enableDetailedLogging) { @@ -476,32 +484,37 @@ export class PortProxy { } this.cleanupConnection(record, 'network_proxy_closed'); }); - + socket.on('close', () => { if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`); + console.log( + `[${connectionId}] Client connection closed after forwarding to NetworkProxy` + ); } this.cleanupConnection(record, 'client_closed'); }); - + // Update activity on data transfer socket.on('data', (chunk: Buffer) => { this.updateActivity(record); - + // Check for potential TLS renegotiation or reconnection packets - if (chunk.length > 0 && chunk[0] === 22) { // ContentType.handshake + if (chunk.length > 0 && chunk[0] === 22) { + // ContentType.handshake if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Detected potential TLS handshake data while connected to NetworkProxy`); + console.log( + `[${connectionId}] Detected potential TLS handshake data while connected to NetworkProxy` + ); } - + // Let the NetworkProxy handle the TLS renegotiation // Just update the activity timestamp to prevent timeouts record.lastActivity = Date.now(); } }); - + proxySocket.on('data', () => this.updateActivity(record)); - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]` @@ -509,7 +522,7 @@ export class PortProxy { } }); } - + /** * Sets up a direct connection to the target (original behavior) * This is used when NetworkProxy isn't configured or as a fallback @@ -586,11 +599,11 @@ export class PortProxy { // 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 { @@ -603,7 +616,9 @@ export class PortProxy { } 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}`); + console.log( + `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}` + ); } } } @@ -660,19 +675,21 @@ export class PortProxy { // 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( + `[${connectionId}] Timeout event on incoming keep-alive connection from ${ + record.remoteIP + } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); // Don't close the connection - just log 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 - )}` + `[${connectionId}] Timeout on incoming side from ${ + record.remoteIP + } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'timeout'; @@ -685,19 +702,21 @@ export class PortProxy { // 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( + `[${connectionId}] Timeout event on outgoing keep-alive connection from ${ + record.remoteIP + } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); // Don't close the connection - just log 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 - )}` + `[${connectionId}] Timeout on outgoing side from ${ + record.remoteIP + } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); if (record.outgoingTerminationReason === null) { record.outgoingTerminationReason = 'timeout'; @@ -711,9 +730,11 @@ export class PortProxy { // Disable timeouts completely for immortal connections socket.setTimeout(0); targetSocket.setTimeout(0); - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`); + console.log( + `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection` + ); } } else { // Set normal timeouts for other connections @@ -743,9 +764,7 @@ export class PortProxy { const combinedData = Buffer.concat(record.pendingData); targetSocket.write(combinedData, (err) => { if (err) { - console.log( - `[${connectionId}] Error writing pending data to target: ${err.message}` - ); + console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`); return this.initiateCleanupOnce(record, 'write_error'); } @@ -764,7 +783,9 @@ export class PortProxy { ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` + - ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` ); } else { console.log( @@ -795,7 +816,9 @@ export class PortProxy { ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` + - ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` ); } else { console.log( @@ -845,77 +868,90 @@ export class PortProxy { if (record.cleanupTimer) { clearTimeout(record.cleanupTimer); } - + // For immortal keep-alive connections, skip setting a timeout completely if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`); + console.log( + `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime` + ); } // No cleanup timer for immortal connections - } + } // For TLS keep-alive connections, use a very extended timeout else if (record.hasKeepAlive && record.isTLS) { // For TLS keep-alive connections, use a very extended timeout // This helps prevent certificate errors after sleep/wake cycles const tlsKeepAliveTimeout = 14 * 24 * 60 * 60 * 1000; // 14 days for TLS keep-alive const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout); - + record.cleanupTimer = setTimeout(() => { console.log( - `[${connectionId}] TLS keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs( + `[${connectionId}] TLS keep-alive connection from ${ + record.remoteIP + } exceeded extended lifetime (${plugins.prettyMs( tlsKeepAliveTimeout )}), forcing cleanup.` ); this.initiateCleanupOnce(record, 'tls_extended_lifetime'); }, safeTimeout); - + // Make sure timeout doesn't keep the process alive if (record.cleanupTimer.unref) { record.cleanupTimer.unref(); } - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] TLS keep-alive connection with enhanced protection, lifetime: ${plugins.prettyMs(tlsKeepAliveTimeout)}`); + console.log( + `[${connectionId}] TLS keep-alive connection with enhanced protection, lifetime: ${plugins.prettyMs( + tlsKeepAliveTimeout + )}` + ); } } // For extended keep-alive connections, use extended timeout else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days const safeTimeout = ensureSafeTimeout(extendedTimeout); - + record.cleanupTimer = setTimeout(() => { console.log( - `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs( - extendedTimeout - )}), forcing cleanup.` + `[${connectionId}] Keep-alive connection from ${ + record.remoteIP + } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.` ); this.initiateCleanupOnce(record, 'extended_lifetime'); }, safeTimeout); - + // Make sure timeout doesn't keep the process alive if (record.cleanupTimer.unref) { record.cleanupTimer.unref(); } - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`); + console.log( + `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs( + extendedTimeout + )}` + ); } } // For standard connections, use normal timeout else { // Use domain-specific timeout if available, otherwise use default - const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!; + const connectionTimeout = + record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!; const safeTimeout = ensureSafeTimeout(connectionTimeout); - + record.cleanupTimer = setTimeout(() => { console.log( - `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs( - connectionTimeout - )}), forcing cleanup.` + `[${connectionId}] Connection from ${ + record.remoteIP + } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.` ); this.initiateCleanupOnce(record, 'connection_timeout'); }, safeTimeout); - + // Make sure timeout doesn't keep the process alive if (record.cleanupTimer.unref) { record.cleanupTimer.unref(); @@ -999,35 +1035,46 @@ export class PortProxy { private updateActivity(record: IConnectionRecord): void { // Get the current time const now = Date.now(); - + // Check if there was a large time gap that suggests system sleep if (record.lastActivity > 0) { const timeDiff = now - record.lastActivity; - + // If time difference is very large (> 30 minutes) and this is a keep-alive connection, // this might indicate system sleep rather than just inactivity if (timeDiff > 30 * 60 * 1000 && record.hasKeepAlive) { if (this.settings.enableDetailedLogging) { console.log( `[${record.id}] Detected possible system sleep for ${plugins.prettyMs(timeDiff)}. ` + - `Preserving keep-alive connection.` + `Handling keep-alive connection after long inactivity.` ); } - - // For keep-alive connections after sleep, we should refresh the TLS state if needed + + // For TLS keep-alive connections after sleep/long inactivity, force close + // to make browser establish a new connection with fresh certificate context if (record.isTLS && record.tlsHandshakeComplete) { - this.refreshTlsStateAfterSleep(record); + if (timeDiff > 4 * 60 * 60 * 1000) { + // If inactive for more than 4 hours + console.log( + `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` + + `Closing to force new connection with fresh certificate.` + ); + return this.initiateCleanupOnce(record, 'certificate_refresh_needed'); + } else { + // For shorter inactivity periods, try to refresh the TLS state + this.refreshTlsStateAfterSleep(record); + } } - + // Mark that we detected sleep record.possibleSystemSleep = true; record.lastSleepDetection = now; } } - + // Update the activity timestamp record.lastActivity = now; - + // Clear any inactivity warning if (record.inactivityWarningIssued) { record.inactivityWarningIssued = false; @@ -1042,22 +1089,37 @@ export class PortProxy { if (record.usingNetworkProxy) { return; } - + try { // For outgoing connections that might need to be refreshed if (record.outgoing && !record.outgoing.destroyed) { - // Send a zero-byte packet to test the connection + // Check how long this connection has been established + const connectionAge = Date.now() - record.incomingStartTime; + const hourInMs = 60 * 60 * 1000; + + // For TLS browser connections that are very old, it's better to force a new connection + // rather than trying to refresh the state, to avoid certificate issues + if (record.isTLS && record.hasKeepAlive && connectionAge > 12 * hourInMs) { + console.log( + `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` + + `Closing to ensure proper certificate handling on browser reconnect.` + ); + return this.initiateCleanupOnce(record, 'certificate_context_refresh'); + } + + // For newer connections, try to send a refresh packet record.outgoing.write(Buffer.alloc(0)); - + if (this.settings.enableDetailedLogging) { console.log(`[${record.id}] Sent refresh packet after sleep detection`); } } } catch (err) { console.log(`[${record.id}] Error refreshing TLS state: ${err}`); - - // If we can't refresh, don't terminate - client will re-establish if needed - // Just log the issue but preserve the connection + + // If we hit an error, it's likely the connection is already broken + // Force cleanup to ensure browser reconnects cleanly + return this.initiateCleanupOnce(record, 'tls_refresh_error'); } } @@ -1158,7 +1220,9 @@ export class PortProxy { ` Duration: ${plugins.prettyMs( duration )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + - `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + + `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` + `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` ); } else { @@ -1181,7 +1245,7 @@ export class PortProxy { } return this.settings.targetIP!; } - + /** * Initiates cleanup once for a connection */ @@ -1189,12 +1253,15 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); } - - if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) { + + if ( + record.incomingTerminationReason === null || + record.incomingTerminationReason === undefined + ) { record.incomingTerminationReason = reason; this.incrementTerminationStat('incoming', reason); } - + this.cleanupConnection(record, reason); } @@ -1318,7 +1385,7 @@ export class PortProxy { // Apply socket optimizations socket.setNoDelay(this.settings.noDelay); - + // Create a unique connection ID and record const connectionId = generateConnectionId(); const connectionRecord: IConnectionRecord = { @@ -1342,19 +1409,19 @@ export class PortProxy { hasKeepAlive: false, // Will set to true if keep-alive is applied incomingTerminationReason: null, outgoingTerminationReason: null, - + // Initialize NetworkProxy tracking fields usingNetworkProxy: false, - + // Initialize sleep detection fields - possibleSystemSleep: false + possibleSystemSleep: false, }; - + // Apply keep-alive settings if enabled if (this.settings.keepAlive) { socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive - + // Apply enhanced TCP keep-alive options if enabled if (this.settings.enableKeepAliveProbes) { try { @@ -1368,7 +1435,9 @@ export class PortProxy { } catch (err) { // Ignore errors - these are optional enhancements if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`); + console.log( + `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}` + ); } } } @@ -1381,8 +1450,8 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + - `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + - `Active connections: ${this.connectionRecords.size}` + `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + + `Active connections: ${this.connectionRecords.size}` ); } else { console.log( @@ -1520,12 +1589,12 @@ export class PortProxy { )}` ); } - + // Check if we should forward this to a NetworkProxy if ( - isTlsHandshakeDetected && - domainConfig.useNetworkProxy === true && - initialChunk && + isTlsHandshakeDetected && + domainConfig.useNetworkProxy === true && + initialChunk && this.networkProxies.length > 0 ) { return this.forwardToNetworkProxy( @@ -1763,11 +1832,11 @@ export class PortProxy { } else { nonTlsConnections++; } - + if (record.hasKeepAlive) { keepAliveConnections++; } - + if (record.usingNetworkProxy) { networkProxyConnections++; } @@ -1808,34 +1877,41 @@ export class PortProxy { } // Skip inactivity check if disabled or for immortal keep-alive connections - if (!this.settings.disableInactivityCheck && - !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) { - + if ( + !this.settings.disableInactivityCheck && + !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') + ) { const inactivityTime = now - record.lastActivity; - + // Special handling for TLS keep-alive connections - if (record.hasKeepAlive && record.isTLS && inactivityTime > this.settings.inactivityTimeout! / 2) { + if ( + record.hasKeepAlive && + record.isTLS && + inactivityTime > this.settings.inactivityTimeout! / 2 + ) { // For TLS keep-alive connections that are getting stale, try to refresh before closing if (!record.inactivityWarningIssued) { console.log( - `[${id}] TLS keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + - `Attempting to preserve connection.` + `[${id}] TLS keep-alive connection from ${ + record.remoteIP + } inactive for ${plugins.prettyMs(inactivityTime)}. ` + + `Attempting to preserve connection.` ); - + // Set warning flag but give a much longer grace period for TLS connections record.inactivityWarningIssued = true; - + // For TLS connections, extend the last activity time considerably // This gives browsers more time to re-establish the connection properly - record.lastActivity = now - (this.settings.inactivityTimeout! / 3); - + record.lastActivity = now - this.settings.inactivityTimeout! / 3; + // Try to stimulate the connection with a probe packet if (record.outgoing && !record.outgoing.destroyed) { try { // For TLS connections, send a proper TLS heartbeat-like packet // This is just a small empty buffer that won't affect the TLS session record.outgoing.write(Buffer.alloc(0)); - + if (this.settings.enableDetailedLogging) { console.log(`[${id}] Sent TLS keep-alive probe packet`); } @@ -1843,36 +1919,38 @@ export class PortProxy { console.log(`[${id}] Error sending TLS probe packet: ${err}`); } } - + // Don't proceed to the normal inactivity check logic continue; } } - + // Use extended timeout for extended-treatment keep-alive connections let effectiveTimeout = this.settings.inactivityTimeout!; if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { const multiplier = this.settings.keepAliveInactivityMultiplier || 6; effectiveTimeout = effectiveTimeout * multiplier; } - + if (inactivityTime > effectiveTimeout && !record.connectionClosed) { // For keep-alive connections, issue a warning first if (record.hasKeepAlive && !record.inactivityWarningIssued) { console.log( - `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + - `Will close in 10 minutes if no activity.` + `[${id}] Warning: Keep-alive connection from ${ + record.remoteIP + } inactive for ${plugins.prettyMs(inactivityTime)}. ` + + `Will close in 10 minutes if no activity.` ); - + // Set warning flag and add grace period record.inactivityWarningIssued = true; record.lastActivity = now - (effectiveTimeout - 600000); - + // Try to stimulate activity with a probe packet if (record.outgoing && !record.outgoing.destroyed) { try { record.outgoing.write(Buffer.alloc(0)); - + if (this.settings.enableDetailedLogging) { console.log(`[${id}] Sent probe packet to test keep-alive connection`); } @@ -1882,22 +1960,37 @@ export class PortProxy { } } else { // MODIFIED: For TLS connections, be more lenient before closing + // For TLS browser connections, we need to handle certificate context properly if (record.isTLS && record.hasKeepAlive) { - // For TLS keep-alive connections, add additional grace period - // This helps with browsers reconnecting after sleep - console.log( - `[${id}] TLS keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + - `Adding extra grace period.` - ); - - // Give additional time for browsers to reconnect properly - record.lastActivity = now - (effectiveTimeout / 2); + // For very long inactivity, it's better to close the connection + // so the browser establishes a new one with a fresh certificate context + if (inactivityTime > 6 * 60 * 60 * 1000) { + // 6 hours + console.log( + `[${id}] TLS keep-alive connection from ${ + record.remoteIP + } inactive for ${plugins.prettyMs(inactivityTime)}. ` + + `Closing to ensure proper certificate handling on browser reconnect.` + ); + this.cleanupConnection(record, 'tls_certificate_refresh'); + } else { + // For shorter inactivity periods, add grace period + console.log( + `[${id}] TLS keep-alive connection from ${ + record.remoteIP + } inactive for ${plugins.prettyMs(inactivityTime)}. ` + + `Adding extra grace period.` + ); + + // Give additional time for browsers to reconnect properly + record.lastActivity = now - effectiveTimeout / 2; + } } else { // For non-keep-alive or after warning, close the connection console.log( `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` + - `for ${plugins.prettyMs(inactivityTime)}.` + - (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') + `for ${plugins.prettyMs(inactivityTime)}.` + + (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') ); this.cleanupConnection(record, 'inactivity'); } @@ -1905,7 +1998,9 @@ export class PortProxy { } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) { // If activity detected after warning, clear the warning if (this.settings.enableDetailedLogging) { - console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`); + console.log( + `[${id}] Connection activity detected after inactivity warning, resetting warning` + ); } record.inactivityWarningIssued = false; } @@ -2054,4 +2149,4 @@ export class PortProxy { console.log('PortProxy shutdown complete.'); } -} \ No newline at end of file +}