fix(PortProxy): Adjust default timeout settings and enhance keep-alive connection handling in PortProxy.
This commit is contained in:
		| @@ -1,5 +1,12 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-03-07 - 3.28.6 - fix(PortProxy) | ||||||
|  | Adjust default timeout settings and enhance keep-alive connection handling in PortProxy. | ||||||
|  |  | ||||||
|  | - Updated default value for maxConnectionLifetime to 24 hours and inactivityTimeout to 4 hours. | ||||||
|  | - Introduced enhanced settings for treating keep-alive connections as 'extended' or 'immortal'. | ||||||
|  | - Modified logic to avoid closing keep-alive connections unnecessarily by adding inactivity warnings and grace periods. | ||||||
|  |  | ||||||
| ## 2025-03-07 - 3.28.5 - fix(core) | ## 2025-03-07 - 3.28.5 - fix(core) | ||||||
| Ensure proper resource cleanup during server shutdown. | Ensure proper resource cleanup during server shutdown. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   name: '@push.rocks/smartproxy', | ||||||
|   version: '3.28.5', |   version: '3.28.6', | ||||||
|   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.' |   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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { | |||||||
|   initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) |   initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) | ||||||
|   socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) |   socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) | ||||||
|   inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) |   inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) | ||||||
|   maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h) |   maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h) | ||||||
|   inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h) |   inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) | ||||||
|  |  | ||||||
|   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown |   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown | ||||||
|   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges |   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges | ||||||
| @@ -49,6 +49,11 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { | |||||||
|   // Rate limiting and security |   // Rate limiting and security | ||||||
|   maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP |   maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP | ||||||
|   connectionRateLimitPerMinute?: number; // Max new connections per minute 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) | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -77,6 +82,12 @@ interface IConnectionRecord { | |||||||
|   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete |   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete | ||||||
|   hasReceivedInitialData: boolean; // Whether initial data has been received |   hasReceivedInitialData: boolean; // Whether initial data has been received | ||||||
|   domainConfig?: IDomainConfig; // Associated domain config for this connection |   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 | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -332,11 +343,11 @@ export class PortProxy { | |||||||
|       ...settingsArg, |       ...settingsArg, | ||||||
|       targetIP: settingsArg.targetIP || 'localhost', |       targetIP: settingsArg.targetIP || 'localhost', | ||||||
|  |  | ||||||
|       // Timeout settings with safe maximum values |       // Timeout settings with reasonable defaults | ||||||
|       initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake |       initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake | ||||||
|       socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 2147483647), // Maximum safe value (~24.8 days) |       socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout | ||||||
|       inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval |       inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval | ||||||
|       maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 2147483647), // Maximum safe value (~24.8 days) |       maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default | ||||||
|       inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout |       inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout | ||||||
|  |  | ||||||
|       gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds |       gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds | ||||||
| @@ -344,19 +355,25 @@ export class PortProxy { | |||||||
|       // Socket optimization settings |       // Socket optimization settings | ||||||
|       noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, |       noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, | ||||||
|       keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, |       keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, | ||||||
|       keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds |       keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness) | ||||||
|       maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes |       maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes | ||||||
|  |  | ||||||
|       // Feature flags |       // Feature flags | ||||||
|       disableInactivityCheck: settingsArg.disableInactivityCheck || false, |       disableInactivityCheck: settingsArg.disableInactivityCheck || false, | ||||||
|       enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false, |       enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined  | ||||||
|  |         ? settingsArg.enableKeepAliveProbes : true, // Enable by default | ||||||
|       enableDetailedLogging: settingsArg.enableDetailedLogging || false, |       enableDetailedLogging: settingsArg.enableDetailedLogging || false, | ||||||
|       enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, |       enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, | ||||||
|       enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true, |       enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default | ||||||
|  |  | ||||||
|       // Rate limiting defaults |       // Rate limiting defaults | ||||||
|       maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP |       maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP | ||||||
|       connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute |       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 | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -418,25 +435,6 @@ export class PortProxy { | |||||||
|     this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1; |     this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Get connection timeout based on domain config or default settings |  | ||||||
|    */ |  | ||||||
|   private getConnectionTimeout(record: IConnectionRecord): number { |  | ||||||
|     // If the connection has a domain-specific timeout, use that with safety check |  | ||||||
|     if (record.domainConfig?.connectionTimeout) { |  | ||||||
|       return ensureSafeTimeout(record.domainConfig.connectionTimeout); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Use default timeout, potentially randomized with safety check |  | ||||||
|     const baseTimeout = this.settings.maxConnectionLifetime!; |  | ||||||
|  |  | ||||||
|     if (this.settings.enableRandomizedTimeouts) { |  | ||||||
|       return randomizeTimeout(baseTimeout); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return ensureSafeTimeout(baseTimeout); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Cleans up a connection record. |    * Cleans up a connection record. | ||||||
|    * Destroys both incoming and outgoing sockets, clears timers, and removes the record. |    * Destroys both incoming and outgoing sockets, clears timers, and removes the record. | ||||||
| @@ -534,7 +532,7 @@ export class PortProxy { | |||||||
|             ` Duration: ${plugins.prettyMs( |             ` Duration: ${plugins.prettyMs( | ||||||
|               duration |               duration | ||||||
|             )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + |             )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + | ||||||
|             `TLS: ${record.isTLS ? 'Yes' : 'No'}` |             `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         console.log( |         console.log( | ||||||
| @@ -549,6 +547,11 @@ export class PortProxy { | |||||||
|    */ |    */ | ||||||
|   private updateActivity(record: IConnectionRecord): void { |   private updateActivity(record: IConnectionRecord): void { | ||||||
|     record.lastActivity = Date.now(); |     record.lastActivity = Date.now(); | ||||||
|  |      | ||||||
|  |     // Clear any inactivity warning | ||||||
|  |     if (record.inactivityWarningIssued) { | ||||||
|  |       record.inactivityWarningIssued = false; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -564,6 +567,22 @@ export class PortProxy { | |||||||
|     return this.settings.targetIP!; |     return this.settings.targetIP!; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Initiates cleanup once for a connection | ||||||
|  |    */ | ||||||
|  |   private initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void { | ||||||
|  |     if (this.settings.enableDetailedLogging) { | ||||||
|  |       console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) { | ||||||
|  |       record.incomingTerminationReason = reason; | ||||||
|  |       this.incrementTerminationStat('incoming', reason); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     this.cleanupConnection(record, reason); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Main method to start the proxy |    * Main method to start the proxy | ||||||
|    */ |    */ | ||||||
| @@ -609,24 +628,6 @@ export class PortProxy { | |||||||
|  |  | ||||||
|       // Apply socket optimizations |       // Apply socket optimizations | ||||||
|       socket.setNoDelay(this.settings.noDelay); |       socket.setNoDelay(this.settings.noDelay); | ||||||
|       if (this.settings.keepAlive) { |  | ||||||
|         socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Apply enhanced TCP options if available |  | ||||||
|       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 |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |        | ||||||
|       // Create a unique connection ID and record |       // Create a unique connection ID and record | ||||||
|       const connectionId = generateConnectionId(); |       const connectionId = generateConnectionId(); | ||||||
| @@ -648,15 +649,44 @@ export class PortProxy { | |||||||
|         isTLS: false, |         isTLS: false, | ||||||
|         tlsHandshakeComplete: false, |         tlsHandshakeComplete: false, | ||||||
|         hasReceivedInitialData: false, |         hasReceivedInitialData: false, | ||||||
|  |         hasKeepAlive: false, // Will set to true if keep-alive is applied | ||||||
|  |         incomingTerminationReason: null, | ||||||
|  |         outgoingTerminationReason: null | ||||||
|       }; |       }; | ||||||
|        |        | ||||||
|  |       // 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 { | ||||||
|  |             // These are platform-specific and may not be available | ||||||
|  |             if ('setKeepAliveProbes' in socket) { | ||||||
|  |               (socket as any).setKeepAliveProbes(10); // More aggressive probing | ||||||
|  |             } | ||||||
|  |             if ('setKeepAliveInterval' in socket) { | ||||||
|  |               (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes | ||||||
|  |             } | ||||||
|  |           } catch (err) { | ||||||
|  |             // Ignore errors - these are optional enhancements | ||||||
|  |             if (this.settings.enableDetailedLogging) { | ||||||
|  |               console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|       // Track connection by IP |       // Track connection by IP | ||||||
|       this.trackConnectionByIP(remoteIP, connectionId); |       this.trackConnectionByIP(remoteIP, connectionId); | ||||||
|       this.connectionRecords.set(connectionId, connectionRecord); |       this.connectionRecords.set(connectionId, connectionRecord); | ||||||
|  |  | ||||||
|       if (this.settings.enableDetailedLogging) { |       if (this.settings.enableDetailedLogging) { | ||||||
|         console.log( |         console.log( | ||||||
|           `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}` |           `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + | ||||||
|  |           `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + | ||||||
|  |           `Active connections: ${this.connectionRecords.size}` | ||||||
|         ); |         ); | ||||||
|       } else { |       } else { | ||||||
|         console.log( |         console.log( | ||||||
| @@ -668,11 +698,6 @@ export class PortProxy { | |||||||
|       let incomingTerminationReason: string | null = null; |       let incomingTerminationReason: string | null = null; | ||||||
|       let outgoingTerminationReason: string | null = null; |       let outgoingTerminationReason: string | null = null; | ||||||
|  |  | ||||||
|       // Local function for cleanupOnce |  | ||||||
|       const cleanupOnce = () => { |  | ||||||
|         this.cleanupConnection(connectionRecord); |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       // Define initiateCleanupOnce for compatibility |       // Define initiateCleanupOnce for compatibility | ||||||
|       const initiateCleanupOnce = (reason: string = 'normal') => { |       const initiateCleanupOnce = (reason: string = 'normal') => { | ||||||
|         if (this.settings.enableDetailedLogging) { |         if (this.settings.enableDetailedLogging) { | ||||||
| @@ -680,9 +705,10 @@ export class PortProxy { | |||||||
|         } |         } | ||||||
|         if (incomingTerminationReason === null) { |         if (incomingTerminationReason === null) { | ||||||
|           incomingTerminationReason = reason; |           incomingTerminationReason = reason; | ||||||
|  |           connectionRecord.incomingTerminationReason = reason; | ||||||
|           this.incrementTerminationStat('incoming', reason); |           this.incrementTerminationStat('incoming', reason); | ||||||
|         } |         } | ||||||
|         cleanupOnce(); |         this.cleanupConnection(connectionRecord, reason); | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       // Helper to reject an incoming connection |       // Helper to reject an incoming connection | ||||||
| @@ -691,9 +717,10 @@ export class PortProxy { | |||||||
|         socket.end(); |         socket.end(); | ||||||
|         if (incomingTerminationReason === null) { |         if (incomingTerminationReason === null) { | ||||||
|           incomingTerminationReason = reason; |           incomingTerminationReason = reason; | ||||||
|  |           connectionRecord.incomingTerminationReason = reason; | ||||||
|           this.incrementTerminationStat('incoming', reason); |           this.incrementTerminationStat('incoming', reason); | ||||||
|         } |         } | ||||||
|         cleanupOnce(); |         this.cleanupConnection(connectionRecord, reason); | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       // Set an initial timeout for SNI data if needed |       // Set an initial timeout for SNI data if needed | ||||||
| @@ -706,10 +733,11 @@ export class PortProxy { | |||||||
|             ); |             ); | ||||||
|             if (incomingTerminationReason === null) { |             if (incomingTerminationReason === null) { | ||||||
|               incomingTerminationReason = 'initial_timeout'; |               incomingTerminationReason = 'initial_timeout'; | ||||||
|  |               connectionRecord.incomingTerminationReason = 'initial_timeout'; | ||||||
|               this.incrementTerminationStat('incoming', 'initial_timeout'); |               this.incrementTerminationStat('incoming', 'initial_timeout'); | ||||||
|             } |             } | ||||||
|             socket.end(); |             socket.end(); | ||||||
|             cleanupOnce(); |             this.cleanupConnection(connectionRecord, 'initial_timeout'); | ||||||
|           } |           } | ||||||
|         }, this.settings.initialDataTimeout!); |         }, this.settings.initialDataTimeout!); | ||||||
|  |  | ||||||
| @@ -783,9 +811,11 @@ export class PortProxy { | |||||||
|  |  | ||||||
|         if (side === 'incoming' && incomingTerminationReason === null) { |         if (side === 'incoming' && incomingTerminationReason === null) { | ||||||
|           incomingTerminationReason = reason; |           incomingTerminationReason = reason; | ||||||
|  |           connectionRecord.incomingTerminationReason = reason; | ||||||
|           this.incrementTerminationStat('incoming', reason); |           this.incrementTerminationStat('incoming', reason); | ||||||
|         } else if (side === 'outgoing' && outgoingTerminationReason === null) { |         } else if (side === 'outgoing' && outgoingTerminationReason === null) { | ||||||
|           outgoingTerminationReason = reason; |           outgoingTerminationReason = reason; | ||||||
|  |           connectionRecord.outgoingTerminationReason = reason; | ||||||
|           this.incrementTerminationStat('outgoing', reason); |           this.incrementTerminationStat('outgoing', reason); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -799,9 +829,11 @@ export class PortProxy { | |||||||
|  |  | ||||||
|         if (side === 'incoming' && incomingTerminationReason === null) { |         if (side === 'incoming' && incomingTerminationReason === null) { | ||||||
|           incomingTerminationReason = 'normal'; |           incomingTerminationReason = 'normal'; | ||||||
|  |           connectionRecord.incomingTerminationReason = 'normal'; | ||||||
|           this.incrementTerminationStat('incoming', 'normal'); |           this.incrementTerminationStat('incoming', 'normal'); | ||||||
|         } else if (side === 'outgoing' && outgoingTerminationReason === null) { |         } else if (side === 'outgoing' && outgoingTerminationReason === null) { | ||||||
|           outgoingTerminationReason = 'normal'; |           outgoingTerminationReason = 'normal'; | ||||||
|  |           connectionRecord.outgoingTerminationReason = 'normal'; | ||||||
|           this.incrementTerminationStat('outgoing', 'normal'); |           this.incrementTerminationStat('outgoing', 'normal'); | ||||||
|           // Record the time when outgoing socket closed. |           // Record the time when outgoing socket closed. | ||||||
|           connectionRecord.outgoingClosedTime = Date.now(); |           connectionRecord.outgoingClosedTime = Date.now(); | ||||||
| @@ -956,11 +988,12 @@ export class PortProxy { | |||||||
|  |  | ||||||
|         // Apply socket optimizations |         // Apply socket optimizations | ||||||
|         targetSocket.setNoDelay(this.settings.noDelay); |         targetSocket.setNoDelay(this.settings.noDelay); | ||||||
|  |          | ||||||
|  |         // Apply keep-alive settings to the outgoing connection as well | ||||||
|         if (this.settings.keepAlive) { |         if (this.settings.keepAlive) { | ||||||
|           targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); |           targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); | ||||||
|         } |  | ||||||
|            |            | ||||||
|         // Apply enhanced TCP options if available |           // Apply enhanced TCP keep-alive options if enabled | ||||||
|           if (this.settings.enableKeepAliveProbes) { |           if (this.settings.enableKeepAliveProbes) { | ||||||
|             try { |             try { | ||||||
|               if ('setKeepAliveProbes' in targetSocket) { |               if ('setKeepAliveProbes' in targetSocket) { | ||||||
| @@ -971,6 +1004,10 @@ export class PortProxy { | |||||||
|               } |               } | ||||||
|             } catch (err) { |             } catch (err) { | ||||||
|               // Ignore errors - these are optional enhancements |               // Ignore errors - these are optional enhancements | ||||||
|  |               if (this.settings.enableDetailedLogging) { | ||||||
|  |                 console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -1009,6 +1046,7 @@ export class PortProxy { | |||||||
|  |  | ||||||
|           if (outgoingTerminationReason === null) { |           if (outgoingTerminationReason === null) { | ||||||
|             outgoingTerminationReason = 'connection_failed'; |             outgoingTerminationReason = 'connection_failed'; | ||||||
|  |             connectionRecord.outgoingTerminationReason = 'connection_failed'; | ||||||
|             this.incrementTerminationStat('outgoing', 'connection_failed'); |             this.incrementTerminationStat('outgoing', 'connection_failed'); | ||||||
|           } |           } | ||||||
|  |  | ||||||
| @@ -1020,8 +1058,20 @@ export class PortProxy { | |||||||
|         targetSocket.on('close', handleClose('outgoing')); |         targetSocket.on('close', handleClose('outgoing')); | ||||||
|         socket.on('close', handleClose('incoming')); |         socket.on('close', handleClose('incoming')); | ||||||
|  |  | ||||||
|         // Handle timeouts |         // Handle timeouts with keep-alive awareness | ||||||
|         socket.on('timeout', () => { |         socket.on('timeout', () => { | ||||||
|  |           // For keep-alive connections, just log a warning instead of closing | ||||||
|  |           if (connectionRecord.hasKeepAlive) { | ||||||
|  |             console.log( | ||||||
|  |               `[${connectionId}] Timeout event on incoming keep-alive connection from ${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( |           console.log( | ||||||
|             `[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs( |             `[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs( | ||||||
|               this.settings.socketTimeout || 3600000 |               this.settings.socketTimeout || 3600000 | ||||||
| @@ -1029,12 +1079,25 @@ export class PortProxy { | |||||||
|           ); |           ); | ||||||
|           if (incomingTerminationReason === null) { |           if (incomingTerminationReason === null) { | ||||||
|             incomingTerminationReason = 'timeout'; |             incomingTerminationReason = 'timeout'; | ||||||
|  |             connectionRecord.incomingTerminationReason = 'timeout'; | ||||||
|             this.incrementTerminationStat('incoming', 'timeout'); |             this.incrementTerminationStat('incoming', 'timeout'); | ||||||
|           } |           } | ||||||
|           initiateCleanupOnce('timeout_incoming'); |           initiateCleanupOnce('timeout_incoming'); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         targetSocket.on('timeout', () => { |         targetSocket.on('timeout', () => { | ||||||
|  |           // For keep-alive connections, just log a warning instead of closing | ||||||
|  |           if (connectionRecord.hasKeepAlive) { | ||||||
|  |             console.log( | ||||||
|  |               `[${connectionId}] Timeout event on outgoing keep-alive connection from ${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( |           console.log( | ||||||
|             `[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs( |             `[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs( | ||||||
|               this.settings.socketTimeout || 3600000 |               this.settings.socketTimeout || 3600000 | ||||||
| @@ -1042,14 +1105,26 @@ export class PortProxy { | |||||||
|           ); |           ); | ||||||
|           if (outgoingTerminationReason === null) { |           if (outgoingTerminationReason === null) { | ||||||
|             outgoingTerminationReason = 'timeout'; |             outgoingTerminationReason = 'timeout'; | ||||||
|  |             connectionRecord.outgoingTerminationReason = 'timeout'; | ||||||
|             this.incrementTerminationStat('outgoing', 'timeout'); |             this.incrementTerminationStat('outgoing', 'timeout'); | ||||||
|           } |           } | ||||||
|           initiateCleanupOnce('timeout_outgoing'); |           initiateCleanupOnce('timeout_outgoing'); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         // Set appropriate timeouts using the configured value with safety |         // Set appropriate timeouts, or disable for immortal keep-alive connections | ||||||
|  |         if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { | ||||||
|  |           // 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`); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           // Set normal timeouts for other connections | ||||||
|           socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000)); |           socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000)); | ||||||
|           targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000)); |           targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         // Track outgoing data for bytes counting |         // Track outgoing data for bytes counting | ||||||
|         targetSocket.on('data', (chunk: Buffer) => { |         targetSocket.on('data', (chunk: Buffer) => { | ||||||
| @@ -1094,7 +1169,7 @@ export class PortProxy { | |||||||
|                         ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` |                         ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` | ||||||
|                         : '' |                         : '' | ||||||
|                     }` + |                     }` + | ||||||
|                     ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` |                     ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}` | ||||||
|                 ); |                 ); | ||||||
|               } else { |               } else { | ||||||
|                 console.log( |                 console.log( | ||||||
| @@ -1125,7 +1200,7 @@ export class PortProxy { | |||||||
|                       ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` |                       ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` | ||||||
|                       : '' |                       : '' | ||||||
|                   }` + |                   }` + | ||||||
|                   ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` |                   ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}` | ||||||
|               ); |               ); | ||||||
|             } else { |             } else { | ||||||
|               console.log( |               console.log( | ||||||
| @@ -1171,14 +1246,46 @@ export class PortProxy { | |||||||
|             }); |             }); | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           // Set connection timeout |           // Set connection timeout with simpler logic | ||||||
|           if (connectionRecord.cleanupTimer) { |           if (connectionRecord.cleanupTimer) { | ||||||
|             clearTimeout(connectionRecord.cleanupTimer); |             clearTimeout(connectionRecord.cleanupTimer); | ||||||
|           } |           } | ||||||
|            |            | ||||||
|           // Set timeout based on domain config or default with safety check |           // For immortal keep-alive connections, skip setting a timeout completely | ||||||
|           const connectionTimeout = this.getConnectionTimeout(connectionRecord); |           if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { | ||||||
|           const safeTimeout = ensureSafeTimeout(connectionTimeout); // Ensure timeout is safe |             if (this.settings.enableDetailedLogging) { | ||||||
|  |               console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`); | ||||||
|  |             } | ||||||
|  |             // No cleanup timer for immortal connections | ||||||
|  |           }  | ||||||
|  |           // For extended keep-alive connections, use extended timeout | ||||||
|  |           else if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { | ||||||
|  |             const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days | ||||||
|  |             const safeTimeout = ensureSafeTimeout(extendedTimeout); | ||||||
|  |              | ||||||
|  |             connectionRecord.cleanupTimer = setTimeout(() => { | ||||||
|  |               console.log( | ||||||
|  |                 `[${connectionId}] Keep-alive connection from ${remoteIP} exceeded extended lifetime (${plugins.prettyMs( | ||||||
|  |                   extendedTimeout | ||||||
|  |                 )}), forcing cleanup.` | ||||||
|  |               ); | ||||||
|  |               initiateCleanupOnce('extended_lifetime'); | ||||||
|  |             }, safeTimeout); | ||||||
|  |              | ||||||
|  |             // Make sure timeout doesn't keep the process alive | ||||||
|  |             if (connectionRecord.cleanupTimer.unref) { | ||||||
|  |               connectionRecord.cleanupTimer.unref(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if (this.settings.enableDetailedLogging) { | ||||||
|  |               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 = connectionRecord.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!; | ||||||
|  |             const safeTimeout = ensureSafeTimeout(connectionTimeout); | ||||||
|              |              | ||||||
|             connectionRecord.cleanupTimer = setTimeout(() => { |             connectionRecord.cleanupTimer = setTimeout(() => { | ||||||
|               console.log( |               console.log( | ||||||
| @@ -1193,6 +1300,7 @@ export class PortProxy { | |||||||
|             if (connectionRecord.cleanupTimer.unref) { |             if (connectionRecord.cleanupTimer.unref) { | ||||||
|               connectionRecord.cleanupTimer.unref(); |               connectionRecord.cleanupTimer.unref(); | ||||||
|             } |             } | ||||||
|  |           } | ||||||
|  |  | ||||||
|           // Mark TLS handshake as complete for TLS connections |           // Mark TLS handshake as complete for TLS connections | ||||||
|           if (connectionRecord.isTLS) { |           if (connectionRecord.isTLS) { | ||||||
| @@ -1385,6 +1493,7 @@ export class PortProxy { | |||||||
|       let nonTlsConnections = 0; |       let nonTlsConnections = 0; | ||||||
|       let completedTlsHandshakes = 0; |       let completedTlsHandshakes = 0; | ||||||
|       let pendingTlsHandshakes = 0; |       let pendingTlsHandshakes = 0; | ||||||
|  |       let keepAliveConnections = 0; | ||||||
|  |  | ||||||
|       // Create a copy of the keys to avoid modification during iteration |       // Create a copy of the keys to avoid modification during iteration | ||||||
|       const connectionIds = [...this.connectionRecords.keys()]; |       const connectionIds = [...this.connectionRecords.keys()]; | ||||||
| @@ -1405,6 +1514,10 @@ export class PortProxy { | |||||||
|           nonTlsConnections++; |           nonTlsConnections++; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  |         if (record.hasKeepAlive) { | ||||||
|  |           keepAliveConnections++; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); |         maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); | ||||||
|         if (record.outgoingStartTime) { |         if (record.outgoingStartTime) { | ||||||
|           maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); |           maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); | ||||||
| @@ -1440,27 +1553,67 @@ export class PortProxy { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Skip inactivity check if disabled |         // Skip inactivity check if disabled or for immortal keep-alive connections | ||||||
|         if (!this.settings.disableInactivityCheck) { |         if (!this.settings.disableInactivityCheck &&  | ||||||
|           // Inactivity check with configurable timeout |             !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) { | ||||||
|           const inactivityThreshold = this.settings.inactivityTimeout!; |  | ||||||
|            |            | ||||||
|           const inactivityTime = now - record.lastActivity; |           const inactivityTime = now - record.lastActivity; | ||||||
|           if (inactivityTime > inactivityThreshold && !record.connectionClosed) { |            | ||||||
|  |           // 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( |               console.log( | ||||||
|               `[${id}] Inactivity check: No activity on connection from ${ |                 `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + | ||||||
|                 record.remoteIP |                 `Will close in 10 minutes if no activity.` | ||||||
|               } for ${plugins.prettyMs(inactivityTime)}.` |               ); | ||||||
|  |                | ||||||
|  |               // 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`); | ||||||
|  |                   } | ||||||
|  |                 } catch (err) { | ||||||
|  |                   console.log(`[${id}] Error sending probe packet: ${err}`); | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } 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'); |               this.cleanupConnection(record, 'inactivity'); | ||||||
|             } |             } | ||||||
|  |           } 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`); | ||||||
|  |             } | ||||||
|  |             record.inactivityWarningIssued = false; | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Log detailed stats periodically |       // Log detailed stats periodically | ||||||
|       console.log( |       console.log( | ||||||
|         `Active connections: ${this.connectionRecords.size}. ` + |         `Active connections: ${this.connectionRecords.size}. ` + | ||||||
|           `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` + |           `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + | ||||||
|  |           `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}. ` + | ||||||
|           `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs( |           `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs( | ||||||
|             maxOutgoing |             maxOutgoing | ||||||
|           )}. ` + |           )}. ` + | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user