UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.

275 lines 23.5 kB
import { Buffer } from 'buffer'; import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js'; import { ClientHelloParser } from './client-hello-parser.js'; /** * Utilities for extracting SNI information from TLS handshakes */ export class SniExtraction { /** * Extracts the SNI (Server Name Indication) from a TLS ClientHello message. * * @param buffer The buffer containing the TLS ClientHello message * @param logger Optional logging function * @returns The extracted server name or undefined if not found */ static extractSNI(buffer, logger) { const log = logger || (() => { }); try { // Parse the ClientHello const parseResult = ClientHelloParser.parseClientHello(buffer, logger); if (!parseResult.isValid) { log(`Failed to parse ClientHello: ${parseResult.error}`); return undefined; } // Check if ServerName extension was found if (parseResult.serverNameList && parseResult.serverNameList.length > 0) { // Use the first hostname (most common case) const serverName = parseResult.serverNameList[0]; log(`Found SNI: ${serverName}`); return serverName; } log('No SNI extension found in ClientHello'); return undefined; } catch (error) { log(`Error extracting SNI: ${error instanceof Error ? error.message : String(error)}`); return undefined; } } /** * 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 logger Optional logging function * @returns The extracted server name or undefined if not found */ static extractSNIFromPSKExtension(buffer, logger) { const log = logger || (() => { }); try { // Ensure this is a ClientHello if (!TlsUtils.isClientHello(buffer)) { log('Not a ClientHello message'); return undefined; } // Parse the ClientHello to find PSK extension const parseResult = ClientHelloParser.parseClientHello(buffer, logger); if (!parseResult.isValid || !parseResult.extensions) { return undefined; } // Find the PSK extension const pskExtension = parseResult.extensions.find(ext => ext.type === TlsExtensionType.PRE_SHARED_KEY); if (!pskExtension) { log('No PSK extension found'); return undefined; } // Parse the PSK extension data const data = pskExtension.data; // PSK extension structure: // 2 bytes: identities list length if (data.length < 2) return undefined; const identitiesLength = (data[0] << 8) + data[1]; let pos = 2; // End of identities list const identitiesEnd = pos + identitiesLength; if (identitiesEnd > data.length) return undefined; // Process each PSK identity while (pos + 2 <= identitiesEnd) { // Identity length (2 bytes) if (pos + 2 > identitiesEnd) break; const identityLength = (data[pos] << 8) + data[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 = data.slice(pos, pos + identityLength); // Skip identity bytes pos += identityLength; // Skip obfuscated ticket age (4 bytes) if (pos + 4 <= identitiesEnd) { pos += 4; } else { break; } // 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'); } } } log('No hostname found in PSK extension'); return undefined; } catch (error) { log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`); return undefined; } } /** * Main entry point for SNI extraction with support for fragmented messages * and session resumption edge cases. * * @param buffer The buffer containing TLS data * @param connectionInfo Connection tracking information * @param logger Optional logging function * @param cachedSni Optional previously cached SNI value * @returns The extracted server name or undefined */ static extractSNIWithResumptionSupport(buffer, connectionInfo, logger, cachedSni) { const log = logger || (() => { }); // Log buffer details for debugging if (logger) { log(`Buffer size: ${buffer.length} bytes`); log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`); if (buffer.length >= 5) { const recordType = buffer[0]; const majorVersion = buffer[1]; const minorVersion = buffer[2]; const recordLength = (buffer[3] << 8) + buffer[4]; log(`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`); } } // Check if we need to handle fragmented packets let processBuffer = buffer; if (connectionInfo) { const connectionId = TlsUtils.createConnectionId(connectionInfo); const reassembledBuffer = ClientHelloParser.handleFragmentedClientHello(buffer, connectionId, logger); if (!reassembledBuffer) { log(`Waiting for more fragments on connection ${connectionId}`); return undefined; // Need more fragments to complete ClientHello } processBuffer = reassembledBuffer; log(`Using reassembled buffer of length ${processBuffer.length}`); } // First try the standard SNI extraction const standardSni = this.extractSNI(processBuffer, logger); if (standardSni) { log(`Found standard SNI: ${standardSni}`); return standardSni; } // Check for session resumption when standard SNI extraction fails if (TlsUtils.isClientHello(processBuffer)) { const resumptionInfo = ClientHelloParser.hasSessionResumption(processBuffer, logger); if (resumptionInfo.isResumption) { log(`Detected session resumption in ClientHello without standard SNI`); // Try to extract SNI from PSK extension const pskSni = this.extractSNIFromPSKExtension(processBuffer, logger); if (pskSni) { log(`Extracted SNI from PSK extension: ${pskSni}`); return pskSni; } } } // If cached SNI was provided, use it for application data packets if (cachedSni && TlsUtils.isTlsApplicationData(buffer)) { log(`Using provided cached SNI for application data: ${cachedSni}`); return cachedSni; } return undefined; } /** * Unified method for processing a TLS packet and extracting SNI. * Main entry point for SNI extraction that handles all edge cases. * * @param buffer The buffer containing TLS data * @param connectionInfo Connection tracking information * @param logger Optional logging function * @param cachedSni Optional previously cached SNI value * @returns The extracted server name or undefined */ static processTlsPacket(buffer, connectionInfo, logger, cachedSni) { const log = logger || (() => { }); // Add timestamp if not provided if (!connectionInfo.timestamp) { connectionInfo.timestamp = Date.now(); } // Check if this is a TLS handshake or application data if (!TlsUtils.isTlsHandshake(buffer) && !TlsUtils.isTlsApplicationData(buffer)) { log('Not a TLS handshake or application data packet'); return undefined; } // Create connection ID for tracking const connectionId = TlsUtils.createConnectionId(connectionInfo); log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`); // Handle application data with cached SNI (for connection racing) if (TlsUtils.isTlsApplicationData(buffer)) { // If explicit cachedSni was provided, use it if (cachedSni) { log(`Using provided cached SNI for application data: ${cachedSni}`); return cachedSni; } log('Application data packet without cached SNI, cannot determine hostname'); return undefined; } // Enhanced session resumption detection if (TlsUtils.isClientHello(buffer)) { const resumptionInfo = ClientHelloParser.hasSessionResumption(buffer, logger); if (resumptionInfo.isResumption) { log(`Session resumption detected in TLS packet`); // Always try standard SNI extraction first const standardSni = this.extractSNI(buffer, logger); if (standardSni) { log(`Found standard SNI in session resumption: ${standardSni}`); return standardSni; } // Enhanced session resumption SNI extraction // Try extracting from PSK identity const pskSni = this.extractSNIFromPSKExtension(buffer, logger); if (pskSni) { log(`Extracted SNI from PSK extension: ${pskSni}`); return pskSni; } log(`Session resumption without extractable SNI`); } } // For handshake messages, try the full extraction process const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, logger); if (sni) { log(`Successfully extracted SNI: ${sni}`); return sni; } // If we couldn't extract an SNI, check if this is a valid ClientHello if (TlsUtils.isClientHello(buffer)) { log('Valid ClientHello detected, but no SNI extracted - might need more data'); } return undefined; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic25pLWV4dHJhY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy90bHMvc25pL3NuaS1leHRyYWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDaEMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLFFBQVEsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ25FLE9BQU8sRUFDTCxpQkFBaUIsRUFFbEIsTUFBTSwwQkFBMEIsQ0FBQztBQWFsQzs7R0FFRztBQUNILE1BQU0sT0FBTyxhQUFhO0lBQ3hCOzs7Ozs7T0FNRztJQUNJLE1BQU0sQ0FBQyxVQUFVLENBQUMsTUFBYyxFQUFFLE1BQXVCO1FBQzlELE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFFLENBQUMsQ0FBQyxDQUFDO1FBRWpDLElBQUksQ0FBQztZQUNILHdCQUF3QjtZQUN4QixNQUFNLFdBQVcsR0FBRyxpQkFBaUIsQ0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLENBQUM7WUFDdkUsSUFBSSxDQUFDLFdBQVcsQ0FBQyxPQUFPLEVBQUUsQ0FBQztnQkFDekIsR0FBRyxDQUFDLGdDQUFnQyxXQUFXLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQztnQkFDekQsT0FBTyxTQUFTLENBQUM7WUFDbkIsQ0FBQztZQUVELDBDQUEwQztZQUMxQyxJQUFJLFdBQVcsQ0FBQyxjQUFjLElBQUksV0FBVyxDQUFDLGNBQWMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7Z0JBQ3hFLDRDQUE0QztnQkFDNUMsTUFBTSxVQUFVLEdBQUcsV0FBVyxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDakQsR0FBRyxDQUFDLGNBQWMsVUFBVSxFQUFFLENBQUMsQ0FBQztnQkFDaEMsT0FBTyxVQUFVLENBQUM7WUFDcEIsQ0FBQztZQUVELEdBQUcsQ0FB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