feat(portproxy): Add browser-friendly mode and SNI renegotiation configuration options to PortProxy

This commit is contained in:
Philipp Kunz 2025-03-11 09:57:06 +00:00
parent 2b69150545
commit df7a12041e
3 changed files with 307 additions and 118 deletions

View File

@ -1,5 +1,13 @@
# Changelog # 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) ## 2025-03-11 - 3.32.2 - fix(PortProxy)
Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability. Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability.

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', 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.' 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.'
} }

View File

@ -62,6 +62,11 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
// New property for NetworkProxy integration // New property for NetworkProxy integration
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination 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
} }
/** /**
@ -100,6 +105,13 @@ interface IConnectionRecord {
// New field for NetworkProxy tracking // New field for NetworkProxy tracking
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
networkProxyIndex?: number; // Which NetworkProxy instance is being used 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 // Helper: Check if a port falls within any of the given port ranges
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => { const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some((range) => port >= range.from && port <= range.to); return ranges.some((range) => port >= range.from && port <= range.to);
@ -375,8 +439,8 @@ export class PortProxy {
// Feature flags // Feature flags
disableInactivityCheck: settingsArg.disableInactivityCheck || false, disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined enableKeepAliveProbes:
? settingsArg.enableKeepAliveProbes : true, // Enable by default settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
@ -389,6 +453,11 @@ export class PortProxy {
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days 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 // Store NetworkProxy instances if provided
@ -413,15 +482,23 @@ export class PortProxy {
serverName?: string serverName?: string
): void { ): void {
// Determine which NetworkProxy to use // Determine which NetworkProxy to use
const proxyIndex = domainConfig.networkProxyIndex !== undefined const proxyIndex =
? domainConfig.networkProxyIndex domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
: 0;
// Validate the NetworkProxy index // Validate the NetworkProxy index
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) { 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 // 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 networkProxy = this.networkProxies[proxyIndex];
@ -437,7 +514,7 @@ export class PortProxy {
// Create a connection to the NetworkProxy // Create a connection to the NetworkProxy
const proxySocket = plugins.net.connect({ const proxySocket = plugins.net.connect({
host: proxyHost, host: proxyHost,
port: proxyPort port: proxyPort,
}); });
// Store the outgoing socket in the record // Store the outgoing socket in the record
@ -475,7 +552,9 @@ export class PortProxy {
socket.on('close', () => { socket.on('close', () => {
if (this.settings.enableDetailedLogging) { 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'); this.cleanupConnection(record, 'client_closed');
}); });
@ -585,7 +664,9 @@ export class PortProxy {
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { 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,7 +723,9 @@ export class PortProxy {
// For keep-alive connections, just log a warning instead of closing // For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) { if (record.hasKeepAlive) {
console.log( 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 this.settings.socketTimeout || 3600000
)}. Connection preserved.` )}. Connection preserved.`
); );
@ -652,9 +735,9 @@ export class PortProxy {
// For non-keep-alive connections, proceed with normal cleanup // For non-keep-alive connections, proceed with normal cleanup
console.log( console.log(
`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs( `[${connectionId}] Timeout on incoming side from ${
this.settings.socketTimeout || 3600000 record.remoteIP
)}` } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
); );
if (record.incomingTerminationReason === null) { if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'timeout'; record.incomingTerminationReason = 'timeout';
@ -667,7 +750,9 @@ export class PortProxy {
// For keep-alive connections, just log a warning instead of closing // For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) { if (record.hasKeepAlive) {
console.log( 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 this.settings.socketTimeout || 3600000
)}. Connection preserved.` )}. Connection preserved.`
); );
@ -677,9 +762,9 @@ export class PortProxy {
// For non-keep-alive connections, proceed with normal cleanup // For non-keep-alive connections, proceed with normal cleanup
console.log( console.log(
`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs( `[${connectionId}] Timeout on outgoing side from ${
this.settings.socketTimeout || 3600000 record.remoteIP
)}` } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
); );
if (record.outgoingTerminationReason === null) { if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'timeout'; record.outgoingTerminationReason = 'timeout';
@ -695,7 +780,9 @@ export class PortProxy {
targetSocket.setTimeout(0); targetSocket.setTimeout(0);
if (this.settings.enableDetailedLogging) { 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 { } else {
// Set normal timeouts for other connections // Set normal timeouts for other connections
@ -725,9 +812,7 @@ export class PortProxy {
const combinedData = Buffer.concat(record.pendingData); const combinedData = Buffer.concat(record.pendingData);
targetSocket.write(combinedData, (err) => { targetSocket.write(combinedData, (err) => {
if (err) { if (err) {
console.log( console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
`[${connectionId}] Error writing pending data to target: ${err.message}`
);
return this.initiateCleanupOnce(record, 'write_error'); return this.initiateCleanupOnce(record, 'write_error');
} }
@ -746,7 +831,9 @@ export class PortProxy {
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` ? ` (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 { } else {
console.log( console.log(
@ -777,7 +864,9 @@ export class PortProxy {
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})` ? ` (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 { } else {
console.log( console.log(
@ -797,30 +886,75 @@ export class PortProxy {
record.pendingData = []; record.pendingData = [];
record.pendingDataSize = 0; record.pendingDataSize = 0;
// Add the renegotiation listener for SNI validation // Add the renegotiation handler for SNI validation, with browser-friendly improvements
if (serverName) { if (serverName) {
socket.on('data', (renegChunk: Buffer) => { // Define a handler for checking renegotiation with improved detection
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { 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 {
// Try to extract SNI from potential renegotiation // Extract SNI from ClientHello
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging); const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
if (newSNI && newSNI !== record.lockedDomain) {
// 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
);
// Decide how to handle the SNI change based on settings
if (this.settings.browserFriendlyMode && isRelatedDomain) {
console.log( console.log(
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.` `[${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'); this.initiateCleanupOnce(record, 'sni_mismatch');
} else if (newSNI && this.settings.enableDetailedLogging) { }
} else if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.` `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
); );
} }
} catch (err) { } catch (err) {
console.log( 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 // Set connection timeout with simpler logic
@ -831,7 +965,9 @@ export class PortProxy {
// For immortal keep-alive connections, skip setting a timeout completely // For immortal keep-alive connections, skip setting a timeout completely
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') { if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
if (this.settings.enableDetailedLogging) { 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 // No cleanup timer for immortal connections
} }
@ -842,9 +978,9 @@ export class PortProxy {
record.cleanupTimer = setTimeout(() => { record.cleanupTimer = setTimeout(() => {
console.log( console.log(
`[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs( `[${connectionId}] Keep-alive connection from ${
extendedTimeout record.remoteIP
)}), forcing cleanup.` } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
); );
this.initiateCleanupOnce(record, 'extended_lifetime'); this.initiateCleanupOnce(record, 'extended_lifetime');
}, safeTimeout); }, safeTimeout);
@ -855,20 +991,25 @@ export class PortProxy {
} }
if (this.settings.enableDetailedLogging) { 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 // For standard connections, use normal timeout
else { else {
// Use domain-specific timeout if available, otherwise use default // 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); const safeTimeout = ensureSafeTimeout(connectionTimeout);
record.cleanupTimer = setTimeout(() => { record.cleanupTimer = setTimeout(() => {
console.log( console.log(
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs( `[${connectionId}] Connection from ${
connectionTimeout record.remoteIP
)}), forcing cleanup.` } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
); );
this.initiateCleanupOnce(record, 'connection_timeout'); this.initiateCleanupOnce(record, 'connection_timeout');
}, safeTimeout); }, safeTimeout);
@ -973,6 +1114,16 @@ export class PortProxy {
const bytesReceived = record.bytesReceived; const bytesReceived = record.bytesReceived;
const bytesSent = record.bytesSent; 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 { try {
if (!record.incoming.destroyed) { if (!record.incoming.destroyed) {
// Try graceful shutdown first, then force destroy after a short timeout // Try graceful shutdown first, then force destroy after a short timeout
@ -1047,8 +1198,11 @@ export class PortProxy {
` Duration: ${plugins.prettyMs( ` Duration: ${plugins.prettyMs(
duration duration
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` + `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` record.hasKeepAlive ? 'Yes' : 'No'
}` +
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
); );
} else { } else {
console.log( console.log(
@ -1091,7 +1245,10 @@ export class PortProxy {
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`); 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; record.incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason); this.incrementTerminationStat('incoming', reason);
} }
@ -1245,7 +1402,11 @@ export class PortProxy {
outgoingTerminationReason: null, outgoingTerminationReason: null,
// Initialize NetworkProxy tracking fields // 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 // Apply keep-alive settings if enabled
@ -1266,7 +1427,9 @@ export class PortProxy {
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { 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}`
);
} }
} }
} }
@ -1280,6 +1443,7 @@ export class PortProxy {
console.log( console.log(
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` + `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` + `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
`Active connections: ${this.connectionRecords.size}` `Active connections: ${this.connectionRecords.size}`
); );
} else { } else {
@ -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 // If we didn't forward to NetworkProxy, proceed with direct connection
return this.setupDirectConnection( return this.setupDirectConnection(
connectionId, connectionId,
@ -1622,7 +1791,9 @@ export class PortProxy {
console.log( console.log(
`PortProxy -> OK: Now listening on port ${port}${ `PortProxy -> OK: Now listening on port ${port}${
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : '' 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); this.netServers.push(server);
@ -1642,6 +1813,7 @@ export class PortProxy {
let pendingTlsHandshakes = 0; let pendingTlsHandshakes = 0;
let keepAliveConnections = 0; let keepAliveConnections = 0;
let networkProxyConnections = 0; let networkProxyConnections = 0;
let domainSwitchedConnections = 0;
// Create a copy of the keys to avoid modification during iteration // Create a copy of the keys to avoid modification during iteration
const connectionIds = [...this.connectionRecords.keys()]; const connectionIds = [...this.connectionRecords.keys()];
@ -1670,11 +1842,14 @@ export class PortProxy {
networkProxyConnections++; networkProxyConnections++;
} }
if (record.domainSwitches && record.domainSwitches > 0) {
domainSwitchedConnections++;
}
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
if (record.outgoingStartTime) { if (record.outgoingStartTime) {
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
} }
// Parity check: if outgoing socket closed and incoming remains active // Parity check: if outgoing socket closed and incoming remains active
if ( if (
record.outgoingClosedTime && record.outgoingClosedTime &&
@ -1706,9 +1881,10 @@ export class PortProxy {
} }
// Skip inactivity check if disabled or for immortal keep-alive connections // Skip inactivity check if disabled or for immortal keep-alive connections
if (!this.settings.disableInactivityCheck && if (
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) { !this.settings.disableInactivityCheck &&
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
) {
const inactivityTime = now - record.lastActivity; const inactivityTime = now - record.lastActivity;
// Use extended timeout for extended-treatment keep-alive connections // Use extended timeout for extended-treatment keep-alive connections
@ -1722,7 +1898,9 @@ export class PortProxy {
// For keep-alive connections, issue a warning first // For keep-alive connections, issue a warning first
if (record.hasKeepAlive && !record.inactivityWarningIssued) { if (record.hasKeepAlive && !record.inactivityWarningIssued) {
console.log( console.log(
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` + `[${id}] Warning: Keep-alive connection from ${
record.remoteIP
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
`Will close in 10 minutes if no activity.` `Will close in 10 minutes if no activity.`
); );
@ -1754,7 +1932,9 @@ export class PortProxy {
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) { } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
// If activity detected after warning, clear the warning // If activity detected after warning, clear the warning
if (this.settings.enableDetailedLogging) { 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; record.inactivityWarningIssued = false;
} }
@ -1765,7 +1945,8 @@ export class PortProxy {
console.log( console.log(
`Active connections: ${this.connectionRecords.size}. ` + `Active connections: ${this.connectionRecords.size}. ` +
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + `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( `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
maxOutgoing maxOutgoing
)}. ` + )}. ` +