diff --git a/changelog.md b/changelog.md index 7dba950..81bc93d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-11 - 3.37.3 - fix(snihandler) +Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators. + +- Defined TLS_PSK_EXTENSION_TYPE and TLS_PSK_KE_MODES_EXTENSION_TYPE constants. +- Added extractSNIFromPSKExtension method to handle ClientHello messages containing PSK identities. +- Improved logging to indicate when session resumption indicators (ticket or PSK) are present but no standard SNI is found. +- Enhanced extractSNIWithResumptionSupport to attempt PSK extraction if standard SNI extraction fails. + ## 2025-03-11 - 3.37.2 - fix(PortProxy) Improve buffering and data handling during connection setup in PortProxy to prevent data loss diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 019face..f962f5f 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.37.2', + version: '3.37.3', 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.snihandler.ts b/ts/classes.snihandler.ts index 3c521d1..ef41e12 100644 --- a/ts/classes.snihandler.ts +++ b/ts/classes.snihandler.ts @@ -11,6 +11,8 @@ export class SniHandler { private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000; private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023; private static readonly TLS_SNI_HOST_NAME_TYPE = 0; + private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3 + private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002D; // PSK Key Exchange Modes /** * Checks if a buffer contains a TLS handshake message (record type 22) @@ -178,6 +180,7 @@ export class SniHandler { // Track if we found session tickets (for improved resumption handling) let hasSessionTicket = false; + let hasPskExtension = false; // Iterate through extensions while (pos + 4 <= extensionsEnd) { @@ -275,15 +278,21 @@ export class SniHandler { log('Found session ticket extension'); hasSessionTicket = true; pos += extensionLength; // Skip this extension + } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) { + // TLS 1.3 PSK extension - mark for resumption support + log('Found PSK extension (TLS 1.3 resumption indicator)'); + hasPskExtension = true; + // We'll skip the extension here and process it separately if needed + pos += extensionLength; } else { // Skip this extension pos += extensionLength; } } - // Log if we found a session ticket but no SNI - if (hasSessionTicket) { - log('Session ticket present but no SNI found - possible resumption scenario'); + // Log if we found session resumption indicators but no SNI + if (hasSessionTicket || hasPskExtension) { + log('Session resumption indicators present but no SNI found'); } log('No SNI extension found in ClientHello'); @@ -294,12 +303,177 @@ export class SniHandler { } } + /** + * Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello. + * + * In TLS 1.3, when a client attempts to resume a session, it may include + * the server name in the PSK identity hint rather than in the SNI extension. + * + * @param buffer - The buffer containing the TLS ClientHello message + * @param enableLogging - Whether to enable detailed debug logging + * @returns The extracted server name or undefined if not found + */ + public static extractSNIFromPSKExtension( + buffer: Buffer, + enableLogging: boolean = false + ): string | undefined { + const log = (message: string) => { + if (enableLogging) { + console.log(`[PSK-SNI Extraction] ${message}`); + } + }; + + try { + // Ensure this is a ClientHello + if (!this.isClientHello(buffer)) { + log('Not a ClientHello message'); + return undefined; + } + + // Find the start position of extensions + let pos = 5; // Start after record header + + // Skip handshake type (1 byte) + pos += 1; + + // Skip handshake length (3 bytes) + pos += 3; + + // Skip client version (2 bytes) + pos += 2; + + // Skip client random (32 bytes) + pos += 32; + + // Skip session ID + if (pos + 1 > buffer.length) return undefined; + const sessionIdLength = buffer[pos]; + pos += 1 + sessionIdLength; + + // Skip cipher suites + if (pos + 2 > buffer.length) return undefined; + const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1]; + pos += 2 + cipherSuitesLength; + + // Skip compression methods + if (pos + 1 > buffer.length) return undefined; + const compressionMethodsLength = buffer[pos]; + pos += 1 + compressionMethodsLength; + + // Check if we have extensions + if (pos + 2 > buffer.length) { + log('No extensions present'); + return undefined; + } + + // Get extensions length + const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1]; + pos += 2; + + // Extensions end position + const extensionsEnd = pos + extensionsLength; + if (extensionsEnd > buffer.length) return undefined; + + // Look for PSK extension + 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_PSK_EXTENSION_TYPE) { + log('Found PSK extension'); + + // PSK extension structure: + // 2 bytes: identities list length + if (pos + 2 > extensionsEnd) break; + const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1]; + pos += 2; + + // End of identities list + const identitiesEnd = pos + identitiesLength; + if (identitiesEnd > extensionsEnd) break; + + // Process each PSK identity + while (pos + 2 <= identitiesEnd) { + // Identity length (2 bytes) + if (pos + 2 > identitiesEnd) break; + const identityLength = (buffer[pos] << 8) + buffer[pos + 1]; + pos += 2; + + if (pos + identityLength > identitiesEnd) break; + + // Try to extract hostname from identity + // Chrome often embeds the hostname in the PSK identity + // This is a heuristic as there's no standard format + if (identityLength > 0) { + const identity = buffer.slice(pos, pos + identityLength); + + // Skip identity bytes + pos += identityLength; + + // Skip obfuscated ticket age (4 bytes) + pos += 4; + + // Try to parse the identity as UTF-8 + try { + const identityStr = identity.toString('utf8'); + log(`PSK identity: ${identityStr}`); + + // Check if the identity contains hostname hints + // Chrome often embeds the hostname in a known format + // Try to extract using common patterns + + // Pattern 1: Look for domain name pattern + const domainPattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i; + const domainMatch = identityStr.match(domainPattern); + if (domainMatch && domainMatch[0]) { + log(`Found domain in PSK identity: ${domainMatch[0]}`); + return domainMatch[0]; + } + + // Pattern 2: Chrome sometimes uses a specific format with delimiters + // This is a heuristic approach since the format isn't standardized + const parts = identityStr.split('|'); + if (parts.length > 1) { + for (const part of parts) { + if (part.includes('.') && !part.includes('/')) { + const possibleDomain = part.trim(); + if (/^[a-z0-9.-]+$/i.test(possibleDomain)) { + log(`Found possible domain in PSK delimiter format: ${possibleDomain}`); + return possibleDomain; + } + } + } + } + } catch (e) { + log('Failed to parse PSK identity as UTF-8'); + } + } + } + } else { + // Skip this extension + pos += extensionLength; + } + } + + log('No hostname found in PSK extension'); + return undefined; + } catch (error) { + log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`); + return undefined; + } + } + /** * Attempts to extract SNI from an initial ClientHello packet and handles * session resumption edge cases more robustly than the standard extraction. * - * This method is specifically designed for Chrome and other browsers that - * may send different ClientHello formats during session resumption. + * This method handles: + * 1. Standard SNI extraction + * 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.) + * 3. Session ticket-based resumption * * @param buffer - The buffer containing the TLS ClientHello message * @param enableLogging - Whether to enable detailed debug logging @@ -312,6 +486,9 @@ export class SniHandler { // First try the standard SNI extraction const standardSni = this.extractSNI(buffer, enableLogging); if (standardSni) { + if (enableLogging) { + console.log(`[SNI Extraction] Found standard SNI: ${standardSni}`); + } return standardSni; } @@ -322,8 +499,19 @@ export class SniHandler { console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption'); } - // Additional handling could be implemented here for specific browser behaviors - // For now, this is a placeholder for future improvements + // Try to extract from PSK extension (TLS 1.3 resumption) + const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging); + if (pskSni) { + if (enableLogging) { + console.log(`[SNI Extraction] Extracted SNI from PSK extension: ${pskSni}`); + } + return pskSni; + } + + // Could add more browser-specific heuristics here if needed + if (enableLogging) { + console.log('[SNI Extraction] Failed to extract SNI from resumption mechanisms'); + } } return undefined;