From df7a12041efb8513ac67c7ea0a9c3833d031ee5f Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Tue, 11 Mar 2025 09:57:06 +0000 Subject: [PATCH] feat(portproxy): Add browser-friendly mode and SNI renegotiation configuration options to PortProxy --- changelog.md | 8 + ts/00_commitinfo_data.ts | 2 +- ts/classes.portproxy.ts | 415 ++++++++++++++++++++++++++++----------- 3 files changed, 307 insertions(+), 118 deletions(-) diff --git a/changelog.md b/changelog.md index adf143f..4bca20b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-11 - 3.33.0 - feat(portproxy) +Add browser-friendly mode and SNI renegotiation configuration options to PortProxy + +- Introduce new properties: browserFriendlyMode (default true) to optimize handling for browser connections. +- Add allowRenegotiationWithDifferentSNI (default false) to enable or disable SNI changes during renegotiation. +- Include relatedDomainPatterns to define patterns for related domains that can share connections. +- Update TypeScript interfaces and internal renegotiation logic to support these options. + ## 2025-03-11 - 3.32.2 - fix(PortProxy) Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability. diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e512f44..2e29b18 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.32.2', + version: '3.33.0', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.' } diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts index d179f93..eefb7a8 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -10,7 +10,7 @@ export interface IDomainConfig { portRanges?: Array<{ from: number; to: number }>; // Optional port ranges // Allow domain-specific timeout override connectionTimeout?: number; // Connection timeout override (ms) - + // New properties for NetworkProxy integration useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0) @@ -54,14 +54,19 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { // Rate limiting and security maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP - + // Enhanced keep-alive settings keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) - + // New property for NetworkProxy integration networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination + + // Browser optimization settings + browserFriendlyMode?: boolean; // Optimizes handling for browser connections + allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation + relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections } /** @@ -90,16 +95,23 @@ interface IConnectionRecord { tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete hasReceivedInitialData: boolean; // Whether initial data has been received domainConfig?: IDomainConfig; // Associated domain config for this connection - + // Keep-alive tracking hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued incomingTerminationReason?: string | null; // Reason for incoming termination outgoingTerminationReason?: string | null; // Reason for outgoing termination - + // New field for NetworkProxy tracking usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy networkProxyIndex?: number; // Which NetworkProxy instance is being used + + // New field for renegotiation handler + renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection + + // Browser connection tracking + isBrowserConnection?: boolean; // Whether this connection appears to be from a browser + domainSwitches?: number; // Number of times the domain has been switched on this connection } /** @@ -266,6 +278,58 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un } } +/** + * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type) + * @param buffer - Buffer containing the TLS record + * @returns true if the buffer contains a proper ClientHello message + */ +function isClientHello(buffer: Buffer): boolean { + try { + if (buffer.length < 9) return false; // Too small for a proper ClientHello + + // Check record type (has to be handshake - 22) + if (buffer.readUInt8(0) !== 22) return false; + + // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello) + if (buffer.readUInt8(5) !== 1) return false; + + // Basic checks passed, this appears to be a ClientHello + return true; + } catch (err) { + console.log(`Error checking for ClientHello: ${err}`); + return false; + } +} + +/** + * Checks if two domains are related based on configured patterns + * @param domain1 - First domain name + * @param domain2 - Second domain name + * @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related + * @returns true if domains are related, false otherwise + */ +function areDomainsRelated( + domain1: string, + domain2: string, + relatedPatterns?: string[][] +): boolean { + // Only exact same domains or empty domains are automatically related + if (!domain1 || !domain2 || domain1 === domain2) return true; + + // Check against configured related domain patterns - the ONLY source of truth + if (relatedPatterns && relatedPatterns.length > 0) { + for (const patternGroup of relatedPatterns) { + const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern)); + const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern)); + + if (domain1Matches && domain2Matches) return true; + } + } + + // If no patterns match, domains are not related + return false; +} + // Helper: Check if a port falls within any of the given port ranges const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => { return ranges.some((range) => port >= range.from && port <= range.to); @@ -348,7 +412,7 @@ export class PortProxy { // Connection tracking by IP for rate limiting private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); - + // New property to store NetworkProxy instances private networkProxies: NetworkProxy[] = []; @@ -375,8 +439,8 @@ export class PortProxy { // Feature flags disableInactivityCheck: settingsArg.disableInactivityCheck || false, - enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined - ? settingsArg.enableKeepAliveProbes : true, // Enable by default + enableKeepAliveProbes: + settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default @@ -384,13 +448,18 @@ export class PortProxy { // Rate limiting defaults maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute - + // Enhanced keep-alive settings keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days + + // Browser optimization settings (new) + browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default + allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default + relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default }; - + // Store NetworkProxy instances if provided this.networkProxies = settingsArg.networkProxies || []; } @@ -413,58 +482,66 @@ export class PortProxy { serverName?: string ): void { // Determine which NetworkProxy to use - const proxyIndex = domainConfig.networkProxyIndex !== undefined - ? domainConfig.networkProxyIndex - : 0; - + const proxyIndex = + domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0; + // Validate the NetworkProxy index if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) { - console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`); + console.log( + `[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.` + ); // Fall back to direct connection - return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData); + return this.setupDirectConnection( + connectionId, + socket, + record, + domainConfig, + serverName, + initialData + ); } - + const networkProxy = this.networkProxies[proxyIndex]; const proxyPort = networkProxy.getListeningPort(); const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}` ); } - + // Create a connection to the NetworkProxy const proxySocket = plugins.net.connect({ host: proxyHost, - port: proxyPort + port: proxyPort, }); - + // Store the outgoing socket in the record record.outgoing = proxySocket; record.outgoingStartTime = Date.now(); record.usingNetworkProxy = true; record.networkProxyIndex = proxyIndex; - + // Set up error handlers proxySocket.on('error', (err) => { console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`); this.cleanupConnection(record, 'network_proxy_connect_error'); }); - + // Handle connection to NetworkProxy proxySocket.on('connect', () => { if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`); } - + // First send the initial data that contains the TLS ClientHello proxySocket.write(initialData); - + // Now set up bidirectional piping between client and NetworkProxy socket.pipe(proxySocket); proxySocket.pipe(socket); - + // Setup cleanup handlers proxySocket.on('close', () => { if (this.settings.enableDetailedLogging) { @@ -472,18 +549,20 @@ export class PortProxy { } this.cleanupConnection(record, 'network_proxy_closed'); }); - + socket.on('close', () => { if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`); + console.log( + `[${connectionId}] Client connection closed after forwarding to NetworkProxy` + ); } this.cleanupConnection(record, 'client_closed'); }); - + // Update activity on data transfer socket.on('data', () => this.updateActivity(record)); proxySocket.on('data', () => this.updateActivity(record)); - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]` @@ -491,7 +570,7 @@ export class PortProxy { } }); } - + /** * Sets up a direct connection to the target (original behavior) * This is used when NetworkProxy isn't configured or as a fallback @@ -568,11 +647,11 @@ export class PortProxy { // Apply socket optimizations targetSocket.setNoDelay(this.settings.noDelay); - + // Apply keep-alive settings to the outgoing connection as well if (this.settings.keepAlive) { targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); - + // Apply enhanced TCP keep-alive options if enabled if (this.settings.enableKeepAliveProbes) { try { @@ -585,7 +664,9 @@ export class PortProxy { } catch (err) { // Ignore errors - these are optional enhancements if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`); + console.log( + `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}` + ); } } } @@ -642,19 +723,21 @@ export class PortProxy { // For keep-alive connections, just log a warning instead of closing if (record.hasKeepAlive) { console.log( - `[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs( + `[${connectionId}] Timeout event on incoming keep-alive connection from ${ + record.remoteIP + } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); // Don't close the connection - just log return; } - + // For non-keep-alive connections, proceed with normal cleanup console.log( - `[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs( - this.settings.socketTimeout || 3600000 - )}` + `[${connectionId}] Timeout on incoming side from ${ + record.remoteIP + } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); if (record.incomingTerminationReason === null) { record.incomingTerminationReason = 'timeout'; @@ -667,19 +750,21 @@ export class PortProxy { // For keep-alive connections, just log a warning instead of closing if (record.hasKeepAlive) { console.log( - `[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs( + `[${connectionId}] Timeout event on outgoing keep-alive connection from ${ + record.remoteIP + } after ${plugins.prettyMs( this.settings.socketTimeout || 3600000 )}. Connection preserved.` ); // Don't close the connection - just log return; } - + // For non-keep-alive connections, proceed with normal cleanup console.log( - `[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs( - this.settings.socketTimeout || 3600000 - )}` + `[${connectionId}] Timeout on outgoing side from ${ + record.remoteIP + } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}` ); if (record.outgoingTerminationReason === null) { record.outgoingTerminationReason = 'timeout'; @@ -693,9 +778,11 @@ export class PortProxy { // Disable timeouts completely for immortal connections socket.setTimeout(0); targetSocket.setTimeout(0); - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`); + console.log( + `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection` + ); } } else { // Set normal timeouts for other connections @@ -725,9 +812,7 @@ export class PortProxy { const combinedData = Buffer.concat(record.pendingData); targetSocket.write(combinedData, (err) => { if (err) { - console.log( - `[${connectionId}] Error writing pending data to target: ${err.message}` - ); + console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`); return this.initiateCleanupOnce(record, 'write_error'); } @@ -746,7 +831,9 @@ export class PortProxy { ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` + - ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` ); } else { console.log( @@ -777,7 +864,9 @@ export class PortProxy { ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` : '' }` + - ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` ); } else { console.log( @@ -797,82 +886,134 @@ export class PortProxy { record.pendingData = []; record.pendingDataSize = 0; - // Add the renegotiation listener for SNI validation + // Add the renegotiation handler for SNI validation, with browser-friendly improvements if (serverName) { - socket.on('data', (renegChunk: Buffer) => { - if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { + // Define a handler for checking renegotiation with improved detection + const renegotiationHandler = (renegChunk: Buffer) => { + // Only process if this looks like a TLS ClientHello (more precise than just checking for type 22) + if (isClientHello(renegChunk)) { try { - // Try to extract SNI from potential renegotiation + // Extract SNI from ClientHello const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging); - if (newSNI && newSNI !== record.lockedDomain) { - console.log( - `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.` + + // Skip if no SNI was found + if (!newSNI) return; + + // Handle SNI change during renegotiation + if (newSNI !== record.lockedDomain) { + // Track domain switches for browser connections + if (!record.domainSwitches) record.domainSwitches = 0; + record.domainSwitches++; + + // Check if this is a normal behavior of browser connection reuse + const isRelatedDomain = areDomainsRelated( + newSNI, + record.lockedDomain || '', + this.settings.relatedDomainPatterns ); - this.initiateCleanupOnce(record, 'sni_mismatch'); - } else if (newSNI && this.settings.enableDetailedLogging) { + + // Decide how to handle the SNI change based on settings + if (this.settings.browserFriendlyMode && isRelatedDomain) { + console.log( + `[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` + + `Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).` + ); + + // Update the locked domain to the new one + record.lockedDomain = newSNI; + } else if (this.settings.allowRenegotiationWithDifferentSNI) { + console.log( + `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` + + `Allowing due to allowRenegotiationWithDifferentSNI setting.` + ); + + // Update the locked domain to the new one + record.lockedDomain = newSNI; + } else { + // Standard strict behavior - terminate connection on SNI mismatch + console.log( + `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` + + `Terminating connection. Enable browserFriendlyMode to allow this.` + ); + this.initiateCleanupOnce(record, 'sni_mismatch'); + } + } else if (this.settings.enableDetailedLogging) { console.log( - `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.` + `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.` ); } } catch (err) { console.log( - `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.` + `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.` ); } } - }); + }; + + // Store the handler in the connection record so we can remove it during cleanup + record.renegotiationHandler = renegotiationHandler; + + // Add the listener + socket.on('data', renegotiationHandler); } // Set connection timeout with simpler logic if (record.cleanupTimer) { clearTimeout(record.cleanupTimer); } - + // For immortal keep-alive connections, skip setting a timeout completely if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`); + console.log( + `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime` + ); } // No cleanup timer for immortal connections - } + } // For extended keep-alive connections, use extended timeout else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days const safeTimeout = ensureSafeTimeout(extendedTimeout); - + record.cleanupTimer = setTimeout(() => { console.log( - `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs( - extendedTimeout - )}), forcing cleanup.` + `[${connectionId}] Keep-alive connection from ${ + record.remoteIP + } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.` ); this.initiateCleanupOnce(record, 'extended_lifetime'); }, safeTimeout); - + // Make sure timeout doesn't keep the process alive if (record.cleanupTimer.unref) { record.cleanupTimer.unref(); } - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`); + console.log( + `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs( + extendedTimeout + )}` + ); } } // For standard connections, use normal timeout else { // Use domain-specific timeout if available, otherwise use default - const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!; + const connectionTimeout = + record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!; const safeTimeout = ensureSafeTimeout(connectionTimeout); - + record.cleanupTimer = setTimeout(() => { console.log( - `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs( - connectionTimeout - )}), forcing cleanup.` + `[${connectionId}] Connection from ${ + record.remoteIP + } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.` ); this.initiateCleanupOnce(record, 'connection_timeout'); }, safeTimeout); - + // Make sure timeout doesn't keep the process alive if (record.cleanupTimer.unref) { record.cleanupTimer.unref(); @@ -973,6 +1114,16 @@ export class PortProxy { const bytesReceived = record.bytesReceived; const bytesSent = record.bytesSent; + // Remove the renegotiation handler if present + if (record.renegotiationHandler && record.incoming) { + try { + record.incoming.removeListener('data', record.renegotiationHandler); + record.renegotiationHandler = undefined; + } catch (err) { + console.log(`[${record.id}] Error removing renegotiation handler: ${err}`); + } + } + try { if (!record.incoming.destroyed) { // Try graceful shutdown first, then force destroy after a short timeout @@ -1047,8 +1198,11 @@ export class PortProxy { ` Duration: ${plugins.prettyMs( duration )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + - `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + - `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` + `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${ + record.hasKeepAlive ? 'Yes' : 'No' + }` + + `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` + + `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}` ); } else { console.log( @@ -1063,7 +1217,7 @@ export class PortProxy { */ private updateActivity(record: IConnectionRecord): void { record.lastActivity = Date.now(); - + // Clear any inactivity warning if (record.inactivityWarningIssued) { record.inactivityWarningIssued = false; @@ -1082,7 +1236,7 @@ export class PortProxy { } return this.settings.targetIP!; } - + /** * Initiates cleanup once for a connection */ @@ -1090,12 +1244,15 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); } - - if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) { + + if ( + record.incomingTerminationReason === null || + record.incomingTerminationReason === undefined + ) { record.incomingTerminationReason = reason; this.incrementTerminationStat('incoming', reason); } - + this.cleanupConnection(record, reason); } @@ -1219,7 +1376,7 @@ export class PortProxy { // Apply socket optimizations socket.setNoDelay(this.settings.noDelay); - + // Create a unique connection ID and record const connectionId = generateConnectionId(); const connectionRecord: IConnectionRecord = { @@ -1243,16 +1400,20 @@ export class PortProxy { hasKeepAlive: false, // Will set to true if keep-alive is applied incomingTerminationReason: null, outgoingTerminationReason: null, - + // Initialize NetworkProxy tracking fields - usingNetworkProxy: false + usingNetworkProxy: false, + + // Initialize browser connection tracking + isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled + domainSwitches: 0, // Track domain switches }; - + // Apply keep-alive settings if enabled if (this.settings.keepAlive) { socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive - + // Apply enhanced TCP keep-alive options if enabled if (this.settings.enableKeepAliveProbes) { try { @@ -1266,7 +1427,9 @@ export class PortProxy { } catch (err) { // Ignore errors - these are optional enhancements if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`); + console.log( + `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}` + ); } } } @@ -1279,8 +1442,9 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + - `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + - `Active connections: ${this.connectionRecords.size}` + `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + + `Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` + + `Active connections: ${this.connectionRecords.size}` ); } else { console.log( @@ -1418,12 +1582,12 @@ export class PortProxy { )}` ); } - + // Check if we should forward this to a NetworkProxy if ( - isTlsHandshakeDetected && - domainConfig.useNetworkProxy === true && - initialChunk && + isTlsHandshakeDetected && + domainConfig.useNetworkProxy === true && + initialChunk && this.networkProxies.length > 0 ) { return this.forwardToNetworkProxy( @@ -1450,6 +1614,11 @@ export class PortProxy { } } + // Save the initial SNI for browser connection management + if (serverName) { + connectionRecord.lockedDomain = serverName; + } + // If we didn't forward to NetworkProxy, proceed with direct connection return this.setupDirectConnection( connectionId, @@ -1622,7 +1791,9 @@ export class PortProxy { console.log( `PortProxy -> OK: Now listening on port ${port}${ this.settings.sniEnabled ? ' (SNI passthrough enabled)' : '' - }${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}` + }${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${ + this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : '' + }` ); }); this.netServers.push(server); @@ -1642,6 +1813,7 @@ export class PortProxy { let pendingTlsHandshakes = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; + let domainSwitchedConnections = 0; // Create a copy of the keys to avoid modification during iteration const connectionIds = [...this.connectionRecords.keys()]; @@ -1661,20 +1833,23 @@ export class PortProxy { } else { nonTlsConnections++; } - + if (record.hasKeepAlive) { keepAliveConnections++; } - + if (record.usingNetworkProxy) { networkProxyConnections++; } + if (record.domainSwitches && record.domainSwitches > 0) { + domainSwitchedConnections++; + } + maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } - // Parity check: if outgoing socket closed and incoming remains active if ( record.outgoingClosedTime && @@ -1706,35 +1881,38 @@ export class PortProxy { } // Skip inactivity check if disabled or for immortal keep-alive connections - if (!this.settings.disableInactivityCheck && - !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) { - + if ( + !this.settings.disableInactivityCheck && + !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') + ) { const inactivityTime = now - record.lastActivity; - + // Use extended timeout for extended-treatment keep-alive connections let effectiveTimeout = this.settings.inactivityTimeout!; if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { const multiplier = this.settings.keepAliveInactivityMultiplier || 6; effectiveTimeout = effectiveTimeout * multiplier; } - + if (inactivityTime > effectiveTimeout && !record.connectionClosed) { // For keep-alive connections, issue a warning first if (record.hasKeepAlive && !record.inactivityWarningIssued) { console.log( - `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + - `Will close in 10 minutes if no activity.` + `[${id}] Warning: Keep-alive connection from ${ + record.remoteIP + } inactive for ${plugins.prettyMs(inactivityTime)}. ` + + `Will close in 10 minutes if no activity.` ); - + // Set warning flag and add grace period record.inactivityWarningIssued = true; record.lastActivity = now - (effectiveTimeout - 600000); - + // Try to stimulate activity with a probe packet if (record.outgoing && !record.outgoing.destroyed) { try { record.outgoing.write(Buffer.alloc(0)); - + if (this.settings.enableDetailedLogging) { console.log(`[${id}] Sent probe packet to test keep-alive connection`); } @@ -1746,15 +1924,17 @@ export class PortProxy { // For non-keep-alive or after warning, close the connection console.log( `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` + - `for ${plugins.prettyMs(inactivityTime)}.` + - (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') + `for ${plugins.prettyMs(inactivityTime)}.` + + (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') ); this.cleanupConnection(record, 'inactivity'); } } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) { // If activity detected after warning, clear the warning if (this.settings.enableDetailedLogging) { - console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`); + console.log( + `[${id}] Connection activity detected after inactivity warning, resetting warning` + ); } record.inactivityWarningIssued = false; } @@ -1765,7 +1945,8 @@ export class PortProxy { console.log( `Active connections: ${this.connectionRecords.size}. ` + `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + - `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` + + `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` + + `DomainSwitched=${domainSwitchedConnections}. ` + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs( maxOutgoing )}. ` + @@ -1903,4 +2084,4 @@ export class PortProxy { console.log('PortProxy shutdown complete.'); } -} \ No newline at end of file +}