diff --git a/changelog.md b/changelog.md index 5c0c27e..392ab4b 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 5fdeacf..a6f558c 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.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.' } diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts index 40b9c71..ea088ea 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -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, @@ -1886,6 +1927,26 @@ export class PortProxy { `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes` ); } + + // 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 = { diff --git a/ts/classes.snihandler.ts b/ts/classes.snihandler.ts index 2ad230d..55ff5dc 100644 --- a/ts/classes.snihandler.ts +++ b/ts/classes.snihandler.ts @@ -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