feat(PortProxy/TLS): Add allowSessionTicket option to control TLS session ticket handling
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-03-11 - 3.41.0 - feat(PortProxy/TLS) | ||||
| Add allowSessionTicket option to control TLS session ticket handling | ||||
|  | ||||
| - Introduce 'allowSessionTicket' flag (default true) in PortProxy settings to enable or disable TLS session resumption via session tickets. | ||||
| - Update SniHandler with a new hasSessionResumption method to detect session ticket and PSK extensions in ClientHello messages. | ||||
| - Force connection cleanup during renegotiation and initial handshake when allowSessionTicket is set to false and a session ticket is detected. | ||||
|  | ||||
| ## 2025-03-11 - 3.40.0 - feat(SniHandler) | ||||
| Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '3.40.0', | ||||
|   version: '3.41.0', | ||||
|   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.' | ||||
| } | ||||
|   | ||||
| @@ -51,6 +51,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { | ||||
|   enableDetailedLogging?: boolean; // Enable detailed connection logging | ||||
|   enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging | ||||
|   enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd | ||||
|   allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true) | ||||
|  | ||||
|   // Rate limiting and security | ||||
|   maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP | ||||
| @@ -236,6 +237,8 @@ export class PortProxy { | ||||
|       enableDetailedLogging: settingsArg.enableDetailedLogging || false, | ||||
|       enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, | ||||
|       enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, | ||||
|       allowSessionTicket: settingsArg.allowSessionTicket !== undefined  | ||||
|                          ? settingsArg.allowSessionTicket : true, | ||||
|  | ||||
|       // Rate limiting defaults | ||||
|       maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, | ||||
| @@ -935,6 +938,21 @@ export class PortProxy { | ||||
|                 destPort: record.incoming.localPort || 0 | ||||
|               }; | ||||
|                | ||||
|               // Check for session tickets if allowSessionTicket is disabled | ||||
|               if (this.settings.allowSessionTicket === false) { | ||||
|                 // Analyze for session resumption attempt (session ticket or PSK) | ||||
|                 const hasSessionTicket = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging); | ||||
|                  | ||||
|                 if (hasSessionTicket) { | ||||
|                   console.log( | ||||
|                     `[${connectionId}] Session ticket detected in renegotiation with allowSessionTicket=false. ` + | ||||
|                     `Terminating connection to force new TLS handshake.` | ||||
|                   ); | ||||
|                   this.initiateCleanupOnce(record, 'session_ticket_blocked'); | ||||
|                   return; | ||||
|                 } | ||||
|               } | ||||
|                | ||||
|               const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging); | ||||
|  | ||||
|               // Skip if no SNI was found | ||||
| @@ -970,6 +988,9 @@ export class PortProxy { | ||||
|          | ||||
|         if (this.settings.enableDetailedLogging) { | ||||
|           console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`); | ||||
|           if (this.settings.allowSessionTicket === false) { | ||||
|             console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @@ -1541,6 +1562,26 @@ export class PortProxy { | ||||
|           if (SniHandler.isTlsHandshake(chunk)) { | ||||
|             connectionRecord.isTLS = true; | ||||
|              | ||||
|             // Check for session tickets if allowSessionTicket is disabled | ||||
|             if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { | ||||
|               // Analyze for session resumption attempt | ||||
|               const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); | ||||
|                | ||||
|               if (hasSessionTicket) { | ||||
|                 console.log( | ||||
|                   `[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` + | ||||
|                   `Terminating connection to force new TLS handshake.` | ||||
|                 ); | ||||
|                 if (connectionRecord.incomingTerminationReason === null) { | ||||
|                   connectionRecord.incomingTerminationReason = 'session_ticket_blocked'; | ||||
|                   this.incrementTerminationStat('incoming', 'session_ticket_blocked'); | ||||
|                 } | ||||
|                 socket.end(); | ||||
|                 this.cleanupConnection(connectionRecord, 'session_ticket_blocked'); | ||||
|                 return; | ||||
|               } | ||||
|             } | ||||
|              | ||||
|             // Try to extract SNI for domain-specific NetworkProxy handling | ||||
|             const connInfo = { | ||||
|               sourceIp: remoteIP, | ||||
| @@ -1887,6 +1928,26 @@ export class PortProxy { | ||||
|                 ); | ||||
|               } | ||||
|                | ||||
|               // Check for session tickets if allowSessionTicket is disabled | ||||
|               if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { | ||||
|                 // Analyze for session resumption attempt | ||||
|                 const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); | ||||
|                  | ||||
|                 if (hasSessionTicket) { | ||||
|                   console.log( | ||||
|                     `[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` + | ||||
|                     `Terminating connection to force new TLS handshake.` | ||||
|                   ); | ||||
|                   if (connectionRecord.incomingTerminationReason === null) { | ||||
|                     connectionRecord.incomingTerminationReason = 'session_ticket_blocked'; | ||||
|                     this.incrementTerminationStat('incoming', 'session_ticket_blocked'); | ||||
|                   } | ||||
|                   socket.end(); | ||||
|                   this.cleanupConnection(connectionRecord, 'session_ticket_blocked'); | ||||
|                   return; | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               // Create connection info object for SNI extraction | ||||
|               const connInfo = { | ||||
|                 sourceIp: remoteIP, | ||||
|   | ||||
| @@ -279,6 +279,118 @@ export class SniHandler { | ||||
|     return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Checks if a ClientHello message contains session resumption indicators | ||||
|    * such as session tickets or PSK (Pre-Shared Key) extensions. | ||||
|    *  | ||||
|    * @param buffer - The buffer containing a ClientHello message | ||||
|    * @param enableLogging - Whether to enable logging | ||||
|    * @returns true if the ClientHello contains session resumption mechanisms | ||||
|    */ | ||||
|   public static hasSessionResumption( | ||||
|     buffer: Buffer, | ||||
|     enableLogging: boolean = false | ||||
|   ): boolean { | ||||
|     const log = (message: string) => { | ||||
|       if (enableLogging) { | ||||
|         console.log(`[Session Resumption] ${message}`); | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     if (!this.isClientHello(buffer)) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Check for session ID presence first | ||||
|       let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version | ||||
|       pos += 32; // Skip client random | ||||
|        | ||||
|       if (pos + 1 > buffer.length) return false; | ||||
|        | ||||
|       const sessionIdLength = buffer[pos]; | ||||
|       let hasNonEmptySessionId = sessionIdLength > 0; | ||||
|        | ||||
|       if (hasNonEmptySessionId) { | ||||
|         log(`Detected non-empty session ID (length: ${sessionIdLength})`); | ||||
|       } | ||||
|        | ||||
|       // Continue to check for extensions | ||||
|       pos += 1 + sessionIdLength; | ||||
|        | ||||
|       // Skip cipher suites | ||||
|       if (pos + 2 > buffer.length) return false; | ||||
|       const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1]; | ||||
|       pos += 2 + cipherSuitesLength; | ||||
|        | ||||
|       // Skip compression methods | ||||
|       if (pos + 1 > buffer.length) return false; | ||||
|       const compressionMethodsLength = buffer[pos]; | ||||
|       pos += 1 + compressionMethodsLength; | ||||
|        | ||||
|       // Check for extensions | ||||
|       if (pos + 2 > buffer.length) return false; | ||||
|        | ||||
|       // Look for session resumption extensions | ||||
|       const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1]; | ||||
|       pos += 2; | ||||
|        | ||||
|       // Extensions end position | ||||
|       const extensionsEnd = pos + extensionsLength; | ||||
|       if (extensionsEnd > buffer.length) return false; | ||||
|        | ||||
|       // Track resumption indicators | ||||
|       let hasSessionTicket = false; | ||||
|       let hasPSK = false; | ||||
|       let hasEarlyData = false; | ||||
|        | ||||
|       // Iterate through extensions | ||||
|       while (pos + 4 <= extensionsEnd) { | ||||
|         const extensionType = (buffer[pos] << 8) + buffer[pos + 1]; | ||||
|         pos += 2; | ||||
|          | ||||
|         const extensionLength = (buffer[pos] << 8) + buffer[pos + 1]; | ||||
|         pos += 2; | ||||
|          | ||||
|         if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) { | ||||
|           log('Found session ticket extension'); | ||||
|           hasSessionTicket = true; | ||||
|            | ||||
|           // Check if session ticket has non-zero length (active ticket) | ||||
|           if (extensionLength > 0) { | ||||
|             log(`Session ticket has length ${extensionLength} - active ticket present`); | ||||
|           } | ||||
|         } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) { | ||||
|           log('Found PSK extension (TLS 1.3 resumption mechanism)'); | ||||
|           hasPSK = true; | ||||
|         } else if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) { | ||||
|           log('Found Early Data extension (TLS 1.3 0-RTT)'); | ||||
|           hasEarlyData = true; | ||||
|         } | ||||
|          | ||||
|         // Skip extension data | ||||
|         pos += extensionLength; | ||||
|       } | ||||
|        | ||||
|       // Consider it a resumption if any resumption mechanism is present | ||||
|       const isResumption = hasSessionTicket || hasPSK || hasEarlyData ||  | ||||
|                          (hasNonEmptySessionId && !hasPSK); // Legacy resumption | ||||
|        | ||||
|       if (isResumption) { | ||||
|         log('Session resumption detected: ' +  | ||||
|             (hasSessionTicket ? 'session ticket, ' : '') + | ||||
|             (hasPSK ? 'PSK, ' : '') + | ||||
|             (hasEarlyData ? 'early data, ' : '') + | ||||
|             (hasNonEmptySessionId ? 'session ID' : '')); | ||||
|       } | ||||
|        | ||||
|       return isResumption; | ||||
|     } catch (error) { | ||||
|       log(`Error checking for session resumption: ${error}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Detects characteristics of a tab reactivation TLS handshake | ||||
|    * These often have specific patterns in Chrome and other browsers | ||||
|   | ||||
		Reference in New Issue
	
	Block a user