diff --git a/changelog.md b/changelog.md index 5a0776a..5e01976 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-12 - 3.41.7 - fix(core) +Refactor PortProxy and SniHandler: improve configuration handling, logging, and whitespace consistency + +- Standardized indentation and spacing for configuration properties in PortProxy settings (e.g. ACME options, keepAliveProbes, allowSessionTicket) +- Simplified conditional formatting and improved inline comments in PortProxy +- Enhanced logging messages in SniHandler for TLS handshake and session resumption detection +- Improved debugging output (e.g. hexdump of initial TLS packet) and consistency of multi-line expressions + ## 2025-03-12 - 3.41.6 - fix(SniHandler) Refactor SniHandler: update whitespace, comment formatting, and consistent type definitions diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3b053ea..9d90f8c 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.41.6', + version: '3.41.7', 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 7cf4071..9f182d9 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -11,7 +11,7 @@ export interface IDomainConfig { portRanges?: Array<{ from: number; to: number }>; // Optional port ranges // Allow domain-specific timeout override connectionTimeout?: number; // Connection timeout override (ms) - + // NetworkProxy integration options for this specific domain useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain networkProxyPort?: number; // Override default NetworkProxy port for this domain @@ -65,17 +65,17 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { // NetworkProxy integration useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) - + // ACME certificate management options acme?: { - enabled?: boolean; // Whether to enable automatic certificate management - port?: number; // Port to listen on for ACME challenges (default: 80) - contactEmail?: string; // Email for Let's Encrypt account - useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging) - renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30) - autoRenew?: boolean; // Whether to automatically renew certificates (default: true) - certificateStore?: string; // Directory to store certificates (default: ./certs) - skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured + enabled?: boolean; // Whether to enable automatic certificate management + port?: number; // Port to listen on for ACME challenges (default: 80) + contactEmail?: string; // Email for Let's Encrypt account + useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging) + renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30) + autoRenew?: boolean; // Whether to automatically renew certificates (default: true) + certificateStore?: string; // Directory to store certificates (default: ./certs) + skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured }; } @@ -232,13 +232,13 @@ export class PortProxy { // Feature flags disableInactivityCheck: settingsArg.disableInactivityCheck || false, - enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined - ? settingsArg.enableKeepAliveProbes : true, + enableKeepAliveProbes: + settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, - allowSessionTicket: settingsArg.allowSessionTicket !== undefined - ? settingsArg.allowSessionTicket : true, + allowSessionTicket: + settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true, // Rate limiting defaults maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, @@ -248,10 +248,10 @@ export class PortProxy { keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days - + // NetworkProxy settings networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port - + // ACME certificate settings with reasonable defaults acme: settingsArg.acme || { enabled: false, @@ -261,8 +261,8 @@ export class PortProxy { renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', - skipConfiguredCerts: false - } + skipConfiguredCerts: false, + }, }; // Initialize NetworkProxy if enabled @@ -280,23 +280,23 @@ export class PortProxy { const networkProxyOptions: any = { port: this.settings.networkProxyPort!, portProxyIntegration: true, - logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info' + logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info', }; - + // Add ACME settings if configured if (this.settings.acme) { networkProxyOptions.acme = { ...this.settings.acme }; } - + this.networkProxy = new NetworkProxy(networkProxyOptions); - + console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`); - + // Convert and apply domain configurations to NetworkProxy await this.syncDomainConfigsToNetworkProxy(); } } - + /** * Updates the domain configurations for the proxy * @param newDomainConfigs The new domain configurations @@ -304,47 +304,47 @@ export class PortProxy { public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise { console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); this.settings.domainConfigs = newDomainConfigs; - + // If NetworkProxy is initialized, resync the configurations if (this.networkProxy) { await this.syncDomainConfigsToNetworkProxy(); } } - + /** * Updates the ACME certificate settings * @param acmeSettings New ACME settings */ public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise { console.log('Updating ACME certificate settings'); - + // Update settings this.settings.acme = { ...this.settings.acme, - ...acmeSettings + ...acmeSettings, }; - + // If NetworkProxy is initialized, update its ACME settings if (this.networkProxy) { try { // Recreate NetworkProxy with new settings if ACME enabled state has changed if (this.settings.acme.enabled !== acmeSettings.enabled) { console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`); - + // Stop the current NetworkProxy await this.networkProxy.stop(); this.networkProxy = null; - + // Reinitialize with new settings await this.initializeNetworkProxy(); - + // Use start() to make sure ACME gets initialized if newly enabled await this.networkProxy.start(); } else { // Update existing NetworkProxy with new settings // Note: Some settings may require a restart to take effect console.log('Updating ACME settings in NetworkProxy'); - + // For certificate renewals, we might want to trigger checks with the new settings if (acmeSettings.renewThresholdDays) { console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`); @@ -359,7 +359,7 @@ export class PortProxy { } } } - + /** * Synchronizes PortProxy domain configurations to NetworkProxy * This allows domains configured in PortProxy to be used by NetworkProxy @@ -369,60 +369,67 @@ export class PortProxy { console.log('Cannot sync configurations - NetworkProxy not initialized'); return; } - + try { // Get SSL certificates from assets // Import fs directly since it's not in plugins const fs = await import('fs'); - + let certPair; try { certPair = { key: fs.readFileSync('assets/certs/key.pem', 'utf8'), - cert: fs.readFileSync('assets/certs/cert.pem', 'utf8') + cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'), }; } catch (certError) { console.log(`Warning: Could not read default certificates: ${certError}`); - console.log('Using empty certificate placeholders - ACME will generate proper certificates if enabled'); - + console.log( + 'Using empty certificate placeholders - ACME will generate proper certificates if enabled' + ); + // Use empty placeholders - NetworkProxy will use its internal defaults // or ACME will generate proper ones if enabled certPair = { key: '', - cert: '' + cert: '', }; } - + // Convert domain configs to NetworkProxy configs const proxyConfigs = this.networkProxy.convertPortProxyConfigs( this.settings.domainConfigs, certPair ); - + // Log ACME-eligible domains if ACME is enabled if (this.settings.acme?.enabled) { const acmeEligibleDomains = proxyConfigs - .filter(config => !config.hostName.includes('*')) // Exclude wildcards - .map(config => config.hostName); - + .filter((config) => !config.hostName.includes('*')) // Exclude wildcards + .map((config) => config.hostName); + if (acmeEligibleDomains.length > 0) { console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`); } else { console.log('No domains eligible for ACME certificates found in configuration'); } } - + // Update NetworkProxy with the converted configs - this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => { - console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); - }).catch(err => { - console.log(`Error synchronizing configurations: ${err.message}`); - }); + this.networkProxy + .updateProxyConfigs(proxyConfigs) + .then(() => { + console.log( + `Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy` + ); + }) + .catch((err) => { + console.log(`Error synchronizing configurations: ${err.message}`); + }); } catch (err) { console.log(`Failed to sync configurations: ${err}`); } } - + /** * Requests a certificate for a specific domain * @param domain The domain to request a certificate for @@ -433,12 +440,12 @@ export class PortProxy { console.log('Cannot request certificate - NetworkProxy not initialized'); return false; } - + if (!this.settings.acme?.enabled) { console.log('Cannot request certificate - ACME is not enabled'); return false; } - + try { const result = await this.networkProxy.requestCertificate(domain); if (result) { @@ -546,9 +553,7 @@ export class PortProxy { proxySocket.on('data', () => this.updateActivity(record)); if (this.settings.enableDetailedLogging) { - console.log( - `[${connectionId}] TLS connection successfully forwarded to NetworkProxy` - ); + console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`); } }); } @@ -582,11 +587,11 @@ export class PortProxy { let queueSize = 0; let processingQueue = false; let drainPending = false; - + // Flag to track if we've switched to the final piping mechanism // Once this is true, we no longer buffer data in dataQueue let pipingEstablished = false; - + // Pause the incoming socket to prevent buffer overflows // This ensures we control the flow of data until piping is set up socket.pause(); @@ -594,22 +599,22 @@ export class PortProxy { // Function to safely process the data queue without losing events const processDataQueue = () => { if (processingQueue || dataQueue.length === 0 || pipingEstablished) return; - + processingQueue = true; - + try { // Process all queued chunks with the current active handler while (dataQueue.length > 0) { const chunk = dataQueue.shift()!; queueSize -= chunk.length; - + // Once piping is established, we shouldn't get here, // but just in case, pass to the outgoing socket directly if (pipingEstablished && record.outgoing) { record.outgoing.write(chunk); continue; } - + // Track bytes received record.bytesReceived += chunk.length; @@ -643,7 +648,7 @@ export class PortProxy { } } finally { processingQueue = false; - + // If there's a pending drain and we've processed everything, // signal we're ready for more data if we haven't established piping yet if (drainPending && dataQueue.length === 0 && !pipingEstablished) { @@ -657,17 +662,17 @@ export class PortProxy { const safeDataHandler = (chunk: Buffer) => { // If piping is already established, just let the pipe handle it if (pipingEstablished) return; - + // Add to our queue for orderly processing dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe queueSize += chunk.length; - + // If queue is getting large, pause socket until we catch up if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) { socket.pause(); drainPending = true; } - + // Process the queue processDataQueue(); }; @@ -848,19 +853,19 @@ export class PortProxy { // Process any remaining data in the queue before switching to piping processDataQueue(); - + // Setup function to establish piping - we'll use this after flushing data const setupPiping = () => { // Mark that we're switching to piping mode pipingEstablished = true; - + // Setup piping in both directions socket.pipe(targetSocket); targetSocket.pipe(socket); - + // Resume the socket to ensure data flows socket.resume(); - + // Process any data that might be queued in the interim if (dataQueue.length > 0) { // Write any remaining queued data directly to the target socket @@ -871,7 +876,7 @@ export class PortProxy { dataQueue.length = 0; queueSize = 0; } - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` + @@ -935,30 +940,36 @@ export class PortProxy { sourceIp: record.remoteIP, sourcePort: record.incoming.remotePort || 0, destIp: record.incoming.localAddress || '', - destPort: record.incoming.localPort || 0 + 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 resumptionInfo = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging); - + const resumptionInfo = SniHandler.hasSessionResumption( + renegChunk, + this.settings.enableTlsDebugLogging + ); + if (resumptionInfo.isResumption) { // Always log resumption attempt for easier debugging // Try to extract SNI for logging - const extractedSNI = SniHandler.extractSNI(renegChunk, this.settings.enableTlsDebugLogging); + const extractedSNI = SniHandler.extractSNI( + renegChunk, + this.settings.enableTlsDebugLogging + ); console.log( `[${connectionId}] Session resumption detected in renegotiation. ` + - `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + - `SNI value: ${extractedSNI || 'None'}, ` + - `allowSessionTicket: ${this.settings.allowSessionTicket}` + `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + + `SNI value: ${extractedSNI || 'None'}, ` + + `allowSessionTicket: ${this.settings.allowSessionTicket}` ); - + // Block if there's session resumption without SNI if (!resumptionInfo.hasSNI) { console.log( `[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` + - `Terminating connection to force new TLS handshake.` + `Terminating connection to force new TLS handshake.` ); this.initiateCleanupOnce(record, 'session_ticket_blocked'); return; @@ -966,14 +977,18 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Session resumption with SNI detected in renegotiation. ` + - `Allowing connection since SNI is present.` + `Allowing connection since SNI is present.` ); } } } } - - const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging); + + const newSNI = SniHandler.extractSNIWithResumptionSupport( + renegChunk, + connInfo, + this.settings.enableTlsDebugLogging + ); // Skip if no SNI was found if (!newSNI) return; @@ -983,7 +998,7 @@ export class PortProxy { // Log and terminate the connection for any SNI change console.log( `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` + - `Terminating connection - SNI domain switching is not allowed.` + `Terminating connection - SNI domain switching is not allowed.` ); this.initiateCleanupOnce(record, 'sni_mismatch'); } else if (this.settings.enableDetailedLogging) { @@ -1005,11 +1020,15 @@ export class PortProxy { // The renegotiation handler is added when piping is established // Making it part of setupPiping ensures proper sequencing of event handlers socket.on('data', renegotiationHandler); - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`); + 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.`); + console.log( + `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.` + ); } } } @@ -1176,7 +1195,7 @@ export class PortProxy { try { // Remove our safe data handler record.incoming.removeAllListeners('data'); - + // Reset the handler references record.renegotiationHandler = undefined; } catch (err) { @@ -1402,7 +1421,11 @@ export class PortProxy { } // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized) - if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) { + if ( + this.settings.useNetworkProxy && + this.settings.useNetworkProxy.length > 0 && + !this.networkProxy + ) { await this.initializeNetworkProxy(); } @@ -1410,12 +1433,16 @@ export class PortProxy { if (this.networkProxy) { await this.networkProxy.start(); console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`); - + // Log ACME status if (this.settings.acme?.enabled) { - console.log(`ACME certificate management is enabled (${this.settings.acme.useProduction ? 'Production' : 'Staging'} mode)`); + console.log( + `ACME certificate management is enabled (${ + this.settings.acme.useProduction ? 'Production' : 'Staging' + } mode)` + ); console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`); - + // Register domains for ACME certificates if enabled if (this.networkProxy.options.acme?.enabled) { console.log('Registering domains with ACME certificate manager...'); @@ -1489,7 +1516,7 @@ export class PortProxy { // Initialize browser connection tracking isBrowserConnection: false, - domainSwitches: 0, + domainSwitches: 0, }; // Apply keep-alive settings if enabled @@ -1536,9 +1563,9 @@ export class PortProxy { // Check if this connection should be forwarded directly to NetworkProxy // First check port-based forwarding settings - let shouldUseNetworkProxy = this.settings.useNetworkProxy && - this.settings.useNetworkProxy.includes(localPort); - + let shouldUseNetworkProxy = + this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(localPort); + // We'll look for domain-specific settings after SNI extraction if (shouldUseNetworkProxy) { @@ -1577,13 +1604,13 @@ export class PortProxy { initialDataReceived = true; connectionRecord.hasReceivedInitialData = true; - + // Block non-TLS connections on port 443 // Always enforce TLS on standard HTTPS port if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) { console.log( `[${connectionId}] Non-TLS connection detected on port 443. ` + - `Terminating connection - only TLS traffic is allowed on standard HTTPS port.` + `Terminating connection - only TLS traffic is allowed on standard HTTPS port.` ); if (connectionRecord.incomingTerminationReason === null) { connectionRecord.incomingTerminationReason = 'non_tls_blocked'; @@ -1597,29 +1624,35 @@ export class PortProxy { // Check if this looks like a TLS handshake if (SniHandler.isTlsHandshake(chunk)) { connectionRecord.isTLS = true; - + // Check for TLS ClientHello with either no SNI or session tickets if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { // Extract SNI first - const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging); + const extractedSNI = SniHandler.extractSNI( + chunk, + this.settings.enableTlsDebugLogging + ); const hasSNI = !!extractedSNI; - + // Analyze for session resumption attempt - const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); - + const resumptionInfo = SniHandler.hasSessionResumption( + chunk, + this.settings.enableTlsDebugLogging + ); + // Always log for debugging purposes console.log( `[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` + - `Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` + - `SNI value: ${extractedSNI || 'None'}, ` + - `Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}` + `Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` + + `SNI value: ${extractedSNI || 'None'}, ` + + `Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}` ); - + // Block if this is a connection with session resumption but no SNI if (resumptionInfo.isResumption && !hasSNI) { console.log( `[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` + - `Terminating connection to force new TLS handshake.` + `Terminating connection to force new TLS handshake.` ); if (connectionRecord.incomingTerminationReason === null) { connectionRecord.incomingTerminationReason = 'session_ticket_blocked'; @@ -1629,13 +1662,13 @@ export class PortProxy { this.cleanupConnection(connectionRecord, 'session_ticket_blocked'); return; } - + // Also block if this is a TLS connection without SNI when allowSessionTicket is false // This forces clients to send SNI which helps with routing if (!hasSNI && localPort === 443) { console.log( `[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` + - `Terminating connection to force proper SNI in handshake.` + `Terminating connection to force proper SNI in handshake.` ); if (connectionRecord.incomingTerminationReason === null) { connectionRecord.incomingTerminationReason = 'no_sni_blocked'; @@ -1646,57 +1679,70 @@ export class PortProxy { return; } } - + // Try to extract SNI for domain-specific NetworkProxy handling const connInfo = { sourceIp: remoteIP, sourcePort: socket.remotePort || 0, destIp: socket.localAddress || '', - destPort: socket.localPort || 0 + destPort: socket.localPort || 0, }; - + // Extract SNI to check for domain-specific NetworkProxy settings const serverName = SniHandler.processTlsPacket( - chunk, + chunk, connInfo, this.settings.enableTlsDebugLogging ); - + if (serverName) { // If we got an SNI, check for domain-specific NetworkProxy settings const domainConfig = this.settings.domainConfigs.find((config) => config.domains.some((d) => plugins.minimatch(serverName, d)) ); - + // Save domain config and SNI in connection record connectionRecord.domainConfig = domainConfig; connectionRecord.lockedDomain = serverName; - + // Use domain-specific NetworkProxy port if configured if (domainConfig?.useNetworkProxy) { - const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort; - + const networkProxyPort = + domainConfig.networkProxyPort || this.settings.networkProxyPort; + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}` ); } - + // Forward to NetworkProxy with domain-specific port - this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk, networkProxyPort); + this.forwardToNetworkProxy( + connectionId, + socket, + connectionRecord, + chunk, + networkProxyPort + ); return; } } - + // Forward directly to NetworkProxy without domain-specific settings this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk); } else { // If not TLS, use normal direct connection console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`); - this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk); + this.setupDirectConnection( + connectionId, + socket, + connectionRecord, + undefined, + undefined, + chunk + ); } }); - } else { // For non-NetworkProxy ports, proceed with normal processing @@ -1760,9 +1806,9 @@ export class PortProxy { sourceIp: remoteIP, sourcePort: socket.remotePort || 0, destIp: socket.localAddress || '', - destPort: socket.localPort || 0 + destPort: socket.localPort || 0, }; - + SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true); } } @@ -1814,7 +1860,7 @@ export class PortProxy { // Save domain config in connection record connectionRecord.domainConfig = domainConfig; - + // Check if this domain should use NetworkProxy (domain-specific setting) if (domainConfig?.useNetworkProxy && this.networkProxy) { if (this.settings.enableDetailedLogging) { @@ -1822,15 +1868,16 @@ export class PortProxy { `[${connectionId}] Domain ${serverName} is configured to use NetworkProxy` ); } - - const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort; - + + const networkProxyPort = + domainConfig.networkProxyPort || this.settings.networkProxyPort; + if (initialChunk && connectionRecord.isTLS) { // For TLS connections with initial chunk, forward to NetworkProxy this.forwardToNetworkProxy( - connectionId, - socket, - connectionRecord, + connectionId, + socket, + connectionRecord, initialChunk, networkProxyPort // Pass the domain-specific NetworkProxy port if configured ); @@ -1861,7 +1908,10 @@ export class PortProxy { )}` ); } - } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { + } else if ( + this.settings.defaultAllowedIPs && + this.settings.defaultAllowedIPs.length > 0 + ) { if ( !isGlobIPAllowed( remoteIP, @@ -1980,13 +2030,32 @@ export class PortProxy { } initialDataReceived = true; - + + // ADD THE DEBUGGING CODE RIGHT HERE, BEFORE ANY OTHER PROCESSING + if (SniHandler.isClientHello(chunk)) { + // Log more details to understand session resumption + const resumptionInfo = SniHandler.hasSessionResumption(chunk, true); + console.log( + `[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}` + ); + + // Try both extraction methods + const standardSNI = SniHandler.extractSNI(chunk, true); + const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true); + + console.log( + `[${connectionId}] SNI extraction results: standardSNI=${ + standardSNI || 'none' + }, pskSNI=${pskSNI || 'none'}` + ); + } + // Block non-TLS connections on port 443 // Always enforce TLS on standard HTTPS port if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) { console.log( `[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` + - `Terminating connection - only TLS traffic is allowed on standard HTTPS port.` + `Terminating connection - only TLS traffic is allowed on standard HTTPS port.` ); if (connectionRecord.incomingTerminationReason === null) { connectionRecord.incomingTerminationReason = 'non_tls_blocked'; @@ -2008,28 +2077,34 @@ 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 resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); - + const resumptionInfo = SniHandler.hasSessionResumption( + chunk, + this.settings.enableTlsDebugLogging + ); + if (resumptionInfo.isResumption) { // Always log resumption attempt for easier debugging // Try to extract SNI for logging - const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging); + const extractedSNI = SniHandler.extractSNI( + chunk, + this.settings.enableTlsDebugLogging + ); console.log( `[${connectionId}] Session resumption detected in SNI handler. ` + - `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + - `SNI value: ${extractedSNI || 'None'}, ` + - `allowSessionTicket: ${this.settings.allowSessionTicket}` + `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + + `SNI value: ${extractedSNI || 'None'}, ` + + `allowSessionTicket: ${this.settings.allowSessionTicket}` ); - + // Block if there's session resumption without SNI if (!resumptionInfo.hasSNI) { console.log( `[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` + - `Terminating connection to force new TLS handshake.` + `Terminating connection to force new TLS handshake.` ); if (connectionRecord.incomingTerminationReason === null) { connectionRecord.incomingTerminationReason = 'session_ticket_blocked'; @@ -2042,7 +2117,7 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Session resumption with SNI detected in SNI handler. ` + - `Allowing connection since SNI is present.` + `Allowing connection since SNI is present.` ); } } @@ -2054,16 +2129,17 @@ export class PortProxy { sourceIp: remoteIP, sourcePort: socket.remotePort || 0, destIp: socket.localAddress || '', - destPort: socket.localPort || 0 + destPort: socket.localPort || 0, }; - + // Use the new processTlsPacket method for comprehensive handling - serverName = SniHandler.processTlsPacket( - chunk, - connInfo, - this.settings.enableTlsDebugLogging, - connectionRecord.lockedDomain // Pass any previously negotiated domain as a hint - ) || ''; + serverName = + SniHandler.processTlsPacket( + chunk, + connInfo, + this.settings.enableTlsDebugLogging, + connectionRecord.lockedDomain // Pass any previously negotiated domain as a hint + ) || ''; } // Lock the connection to the negotiated SNI. @@ -2392,7 +2468,7 @@ export class PortProxy { console.log('Stopping NetworkProxy...'); await this.networkProxy.stop(); console.log('NetworkProxy stopped successfully'); - + // Log ACME shutdown if it was enabled if (this.settings.acme?.enabled) { console.log('ACME certificate manager stopped'); @@ -2417,4 +2493,4 @@ export class PortProxy { console.log('PortProxy shutdown complete.'); } -} \ No newline at end of file +} diff --git a/ts/classes.snihandler.ts b/ts/classes.snihandler.ts index 0b5c494..5b76e4e 100644 --- a/ts/classes.snihandler.ts +++ b/ts/classes.snihandler.ts @@ -1321,6 +1321,7 @@ export class SniHandler { * @param cachedSni - Optional cached SNI from previous connections (for racing detection) * @returns The extracted server name or undefined if not found or more data needed */ + public static processTlsPacket( buffer: Buffer, connectionInfo: { @@ -1373,6 +1374,74 @@ export class SniHandler { return undefined; } + // Enhanced session resumption detection + if (this.isClientHello(buffer)) { + const resumptionInfo = this.hasSessionResumption(buffer, enableLogging); + + if (resumptionInfo.isResumption) { + log(`Session resumption detected in TLS packet`); + + // Always try standard SNI extraction first + const standardSni = this.extractSNI(buffer, enableLogging); + if (standardSni) { + log(`Found standard SNI in session resumption: ${standardSni}`); + + // Cache this SNI + this.cacheSession(connectionInfo.sourceIp, standardSni); + return standardSni; + } + + // Enhanced session resumption SNI extraction + // Try extracting from PSK identity + const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging); + if (pskSni) { + log(`Extracted SNI from PSK extension: ${pskSni}`); + this.cacheSession(connectionInfo.sourceIp, pskSni); + return pskSni; + } + + // Additional check for SNI in session tickets + if (enableLogging) { + log(`Checking for session ticket information to extract server name...`); + // Log more details for debugging + try { + // Look at the raw buffer for patterns + log(`Buffer hexdump (first 100 bytes): ${buffer.slice(0, 100).toString('hex')}`); + + // Try to find hostname-like patterns in the buffer + const bufferStr = buffer.toString('utf8', 0, buffer.length); + const hostnamePattern = + /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/gi; + const hostMatches = bufferStr.match(hostnamePattern); + + if (hostMatches && hostMatches.length > 0) { + log(`Possible hostnames found in buffer: ${hostMatches.join(', ')}`); + + // Check if any match looks like a valid domain + for (const match of hostMatches) { + if (match.includes('.') && match.length > 3) { + log(`Potential SNI found in session data: ${match}`); + // Don't automatically use this - just log for debugging + } + } + } + } catch (e) { + log(`Error scanning for patterns: ${e}`); + } + } + + // If we still don't have SNI, check for cached sessions + const cachedSni = this.getCachedSession(connectionInfo.sourceIp); + if (cachedSni) { + log(`Using cached SNI for session resumption: ${cachedSni}`); + return cachedSni; + } + + log(`Session resumption without extractable SNI`); + // If allowSessionTicket=false, should be rejected by caller + } + } + // For handshake messages, try the full extraction process const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging);