import * as plugins from './plugins.js'; import type { IPortProxySettings } from './classes.pp.interfaces.js'; import { SniHandler } from './classes.pp.snihandler.js'; /** * Interface for connection information used for SNI extraction */ interface IConnectionInfo { sourceIp: string; sourcePort: number; destIp: string; destPort: number; } /** * Manages TLS-related operations including SNI extraction and validation */ export class TlsManager { constructor(private settings: IPortProxySettings) {} /** * Check if a data chunk appears to be a TLS handshake */ public isTlsHandshake(chunk: Buffer): boolean { return SniHandler.isTlsHandshake(chunk); } /** * Check if a data chunk appears to be a TLS ClientHello */ public isClientHello(chunk: Buffer): boolean { return SniHandler.isClientHello(chunk); } /** * Extract Server Name Indication (SNI) from TLS handshake */ public extractSNI( chunk: Buffer, connInfo: IConnectionInfo, previousDomain?: string ): string | undefined { // Use the SniHandler to process the TLS packet return SniHandler.processTlsPacket( chunk, connInfo, this.settings.enableTlsDebugLogging || false, previousDomain ); } /** * Handle session resumption attempts */ public handleSessionResumption( chunk: Buffer, connectionId: string, hasSNI: boolean ): { shouldBlock: boolean; reason?: string } { // Skip if session tickets are allowed if (this.settings.allowSessionTicket !== false) { return { shouldBlock: false }; } // Check for session resumption attempt const resumptionInfo = SniHandler.hasSessionResumption( chunk, this.settings.enableTlsDebugLogging || false ); // If this is a resumption attempt without SNI, block it if (resumptionInfo.isResumption && !hasSNI && !resumptionInfo.hasSNI) { if (this.settings.enableTlsDebugLogging) { console.log( `[${connectionId}] Session resumption detected without SNI and allowSessionTicket=false. ` + `Terminating connection to force new TLS handshake.` ); } return { shouldBlock: true, reason: 'session_ticket_blocked' }; } return { shouldBlock: false }; } /** * Check for SNI mismatch during renegotiation */ public checkRenegotiationSNI( chunk: Buffer, connInfo: IConnectionInfo, expectedDomain: string, connectionId: string ): { hasMismatch: boolean; extractedSNI?: string } { // Only process if this looks like a TLS ClientHello if (!this.isClientHello(chunk)) { return { hasMismatch: false }; } try { // Extract SNI with renegotiation support const newSNI = SniHandler.extractSNIWithResumptionSupport( chunk, connInfo, this.settings.enableTlsDebugLogging || false ); // Skip if no SNI was found if (!newSNI) return { hasMismatch: false }; // Check for SNI mismatch if (newSNI !== expectedDomain) { if (this.settings.enableTlsDebugLogging) { console.log( `[${connectionId}] Renegotiation with different SNI: ${expectedDomain} -> ${newSNI}. ` + `Terminating connection - SNI domain switching is not allowed.` ); } return { hasMismatch: true, extractedSNI: newSNI }; } else if (this.settings.enableTlsDebugLogging) { console.log( `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.` ); } } catch (err) { console.log( `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.` ); } return { hasMismatch: false }; } /** * Create a renegotiation handler function for a connection */ public createRenegotiationHandler( connectionId: string, lockedDomain: string, connInfo: IConnectionInfo, onMismatch: (connectionId: string, reason: string) => void ): (chunk: Buffer) => void { return (chunk: Buffer) => { const result = this.checkRenegotiationSNI(chunk, connInfo, lockedDomain, connectionId); if (result.hasMismatch) { onMismatch(connectionId, 'sni_mismatch'); } }; } /** * Analyze TLS connection for browser fingerprinting * This helps identify browser vs non-browser connections */ public analyzeClientHello(chunk: Buffer): { isBrowserConnection: boolean; isRenewal: boolean; hasSNI: boolean; } { // Default result const result = { isBrowserConnection: false, isRenewal: false, hasSNI: false }; try { // Check if it's a ClientHello if (!this.isClientHello(chunk)) { return result; } // Check for session resumption const resumptionInfo = SniHandler.hasSessionResumption( chunk, this.settings.enableTlsDebugLogging || false ); // Extract SNI const sni = SniHandler.extractSNI( chunk, this.settings.enableTlsDebugLogging || false ); // Update result result.isRenewal = resumptionInfo.isResumption; result.hasSNI = !!sni; // Browsers typically: // 1. Send SNI extension // 2. Have a variety of extensions (ALPN, etc.) // 3. Use standard cipher suites // ...more complex heuristics could be implemented here // Simple heuristic: presence of SNI suggests browser result.isBrowserConnection = !!sni; return result; } catch (err) { console.log(`Error analyzing ClientHello: ${err}`); return result; } } }