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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic25pLWV4dHJhY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9wcm90b2NvbHMvdGxzL3NuaS9zbmktZXh0cmFjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sUUFBUSxDQUFDO0FBQ2hDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxRQUFRLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRSxPQUFPLEVBQ0wsaUJBQWlCLEVBRWxCLE1BQU0sMEJBQTBCLENBQUM7QUFhbEM7O0dBRUc7QUFDSCxNQUFNLE9BQU8sYUFBYTtJQUN4Qjs7Ozs7O09BTUc7SUFDSSxNQUFNLENBQUMsVUFBVSxDQUFDLE1BQWMsRUFBRSxNQUF1QjtRQUM5RCxNQUFNLEdBQUcsR0FBRyxNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQztRQUVqQyxJQUFJLENBQUM7WUFDSCx3QkFBd0I7WUFDeEIsTUFBTSxXQUFXLEdBQUcsaUJBQWlCLENBQUMsZ0JBQWdCLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3ZFLElBQUksQ0FBQyxXQUFXLENBQUMsT0FBTyxFQUFFLENBQUM7Z0JBQ3pCLEdBQUcsQ0FBQyxnQ0FBZ0MsV0FBVyxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUM7Z0JBQ3pELE9BQU8sU0FBUyxDQUFDO1lBQ25CLENBQUM7WUFFRCwwQ0FBMEM7WUFDMUMsSUFBSSxXQUFXLENBQUMsY0FBYyxJQUFJLFdBQVcsQ0FBQyxjQUFjLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO2dCQUN4RSw0Q0FBNEM7Z0JBQzVDLE1BQU0sVUFBVSxHQUFHLFdBQVcsQ0FBQyxjQUFjLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0JBQ2pELEdBQUcsQ0FBQyxjQUFjLFVBQVUsRUFBRSxDQUFDLENBQUM7Z0JBQ2hDLE9BQU8sVUFBVSxDQUFDO1lBQ3BCLENBQUM7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