From 9d7ed21cbac1a8bf0df94010db0c5ab16e2f6310 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sat, 8 Mar 2025 12:40:55 +0000 Subject: [PATCH] feat(PortProxy): Add advanced TLS keep-alive handling and system sleep detection --- changelog.md | 7 ++ ts/00_commitinfo_data.ts | 2 +- ts/classes.portproxy.ts | 193 ++++++++++++++++++++++++++++++++++----- 3 files changed, 180 insertions(+), 22 deletions(-) diff --git a/changelog.md b/changelog.md index d882682..1a73c76 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-03-08 - 3.30.0 - feat(PortProxy) +Add advanced TLS keep-alive handling and system sleep detection + +- Implemented system sleep detection to maintain keep-alive connections. +- Enhanced TLS keep-alive connections with extended timeout and sleep detection mechanisms. +- Introduced automatic TLS state refresh after system wake-up to prevent connection drops. + ## 2025-03-07 - 3.29.3 - fix(core) Fix functional errors in the proxy setup and enhance pnpm configuration diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 7d50961..917d85e 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.29.3', + version: '3.30.0', 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 d179f93..2d0361e 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -100,6 +100,10 @@ interface IConnectionRecord { // 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 } /** @@ -481,7 +485,21 @@ export class PortProxy { }); // Update activity on data transfer - socket.on('data', () => this.updateActivity(record)); + 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 (this.settings.enableDetailedLogging) { + 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) { @@ -835,6 +853,31 @@ export class PortProxy { } // 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( + 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)}`); + } + } // 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 @@ -950,6 +993,74 @@ export class PortProxy { this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1; } + /** + * Update connection activity timestamp with sleep detection + */ + 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.` + ); + } + + // For keep-alive connections after sleep, we should refresh the TLS state if needed + if (record.isTLS && record.tlsHandshakeComplete) { + 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; + } + } + + /** + * Refresh TLS state after sleep detection + */ + private refreshTlsStateAfterSleep(record: IConnectionRecord): void { + // Skip if we're using a NetworkProxy as it handles its own TLS state + 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 + 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 + } + } + /** * Cleans up a connection record. * Destroys both incoming and outgoing sockets, clears timers, and removes the record. @@ -1058,18 +1169,6 @@ export class PortProxy { } } - /** - * Update connection activity timestamp - */ - private updateActivity(record: IConnectionRecord): void { - record.lastActivity = Date.now(); - - // Clear any inactivity warning - if (record.inactivityWarningIssued) { - record.inactivityWarningIssued = false; - } - } - /** * Get target IP with round-robin support */ @@ -1245,7 +1344,10 @@ export class PortProxy { outgoingTerminationReason: null, // Initialize NetworkProxy tracking fields - usingNetworkProxy: false + usingNetworkProxy: false, + + // Initialize sleep detection fields + possibleSystemSleep: false }; // Apply keep-alive settings if enabled @@ -1711,6 +1813,42 @@ export class PortProxy { const inactivityTime = now - record.lastActivity; + // Special handling for TLS keep-alive connections + 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.` + ); + + // 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); + + // 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`); + } + } catch (err) { + 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') { @@ -1743,13 +1881,26 @@ export class PortProxy { } } } 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.' : '') - ); - this.cleanupConnection(record, 'inactivity'); + // MODIFIED: For TLS connections, be more lenient before closing + 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); + } 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.' : '') + ); + this.cleanupConnection(record, 'inactivity'); + } } } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) { // If activity detected after warning, clear the warning