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

@ -10,7 +10,7 @@ export interface IDomainConfig {
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Allow domain-specific timeout override // Allow domain-specific timeout override
connectionTimeout?: number; // Connection timeout override (ms) connectionTimeout?: number; // Connection timeout override (ms)
// New properties for NetworkProxy integration // New properties for NetworkProxy integration
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0) 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 // Rate limiting and security
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
// Enhanced keep-alive settings // Enhanced keep-alive settings
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
// 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
} }
/** /**
@ -90,16 +95,23 @@ interface IConnectionRecord {
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
hasReceivedInitialData: boolean; // Whether initial data has been received hasReceivedInitialData: boolean; // Whether initial data has been received
domainConfig?: IDomainConfig; // Associated domain config for this connection domainConfig?: IDomainConfig; // Associated domain config for this connection
// Keep-alive tracking // Keep-alive tracking
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
incomingTerminationReason?: string | null; // Reason for incoming termination incomingTerminationReason?: string | null; // Reason for incoming termination
outgoingTerminationReason?: string | null; // Reason for outgoing termination outgoingTerminationReason?: string | null; // Reason for outgoing termination
// 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);
@ -348,7 +412,7 @@ export class PortProxy {
// Connection tracking by IP for rate limiting // Connection tracking by IP for rate limiting
private connectionsByIP: Map<string, Set<string>> = new Map(); private connectionsByIP: Map<string, Set<string>> = new Map();
private connectionRateByIP: Map<string, number[]> = new Map(); private connectionRateByIP: Map<string, number[]> = new Map();
// New property to store NetworkProxy instances // New property to store NetworkProxy instances
private networkProxies: NetworkProxy[] = []; private networkProxies: NetworkProxy[] = [];
@ -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
@ -384,13 +448,18 @@ export class PortProxy {
// Rate limiting defaults // Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
// Enhanced keep-alive settings // Enhanced keep-alive settings
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
this.networkProxies = settingsArg.networkProxies || []; this.networkProxies = settingsArg.networkProxies || [];
} }
@ -413,58 +482,66 @@ 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];
const proxyPort = networkProxy.getListeningPort(); const proxyPort = networkProxy.getListeningPort();
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}` `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
); );
} }
// 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
record.outgoing = proxySocket; record.outgoing = proxySocket;
record.outgoingStartTime = Date.now(); record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true; record.usingNetworkProxy = true;
record.networkProxyIndex = proxyIndex; record.networkProxyIndex = proxyIndex;
// Set up error handlers // Set up error handlers
proxySocket.on('error', (err) => { proxySocket.on('error', (err) => {
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`); console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
this.cleanupConnection(record, 'network_proxy_connect_error'); this.cleanupConnection(record, 'network_proxy_connect_error');
}); });
// Handle connection to NetworkProxy // Handle connection to NetworkProxy
proxySocket.on('connect', () => { proxySocket.on('connect', () => {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`); console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
} }
// First send the initial data that contains the TLS ClientHello // First send the initial data that contains the TLS ClientHello
proxySocket.write(initialData); proxySocket.write(initialData);
// Now set up bidirectional piping between client and NetworkProxy // Now set up bidirectional piping between client and NetworkProxy
socket.pipe(proxySocket); socket.pipe(proxySocket);
proxySocket.pipe(socket); proxySocket.pipe(socket);
// Setup cleanup handlers // Setup cleanup handlers
proxySocket.on('close', () => { proxySocket.on('close', () => {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
@ -472,18 +549,20 @@ export class PortProxy {
} }
this.cleanupConnection(record, 'network_proxy_closed'); this.cleanupConnection(record, 'network_proxy_closed');
}); });
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');
}); });
// Update activity on data transfer // Update activity on data transfer
socket.on('data', () => this.updateActivity(record)); socket.on('data', () => this.updateActivity(record));
proxySocket.on('data', () => this.updateActivity(record)); proxySocket.on('data', () => this.updateActivity(record));
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]` `[${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) * Sets up a direct connection to the target (original behavior)
* This is used when NetworkProxy isn't configured or as a fallback * This is used when NetworkProxy isn't configured or as a fallback
@ -568,11 +647,11 @@ export class PortProxy {
// Apply socket optimizations // Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay); targetSocket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings to the outgoing connection as well // Apply keep-alive settings to the outgoing connection as well
if (this.settings.keepAlive) { if (this.settings.keepAlive) {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
// Apply enhanced TCP keep-alive options if enabled // Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) { if (this.settings.enableKeepAliveProbes) {
try { try {
@ -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,19 +723,21 @@ 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.`
); );
// Don't close the connection - just log // Don't close the connection - just log
return; return;
} }
// 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,19 +750,21 @@ 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.`
); );
// Don't close the connection - just log // Don't close the connection - just log
return; return;
} }
// 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';
@ -693,9 +778,11 @@ export class PortProxy {
// Disable timeouts completely for immortal connections // Disable timeouts completely for immortal connections
socket.setTimeout(0); socket.setTimeout(0);
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,82 +886,134 @@ 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) {
console.log( // Skip if no SNI was found
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.` 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( 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
if (record.cleanupTimer) { if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer); clearTimeout(record.cleanupTimer);
} }
// 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
} }
// For extended keep-alive connections, use extended timeout // For extended keep-alive connections, use extended timeout
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
const safeTimeout = ensureSafeTimeout(extendedTimeout); const safeTimeout = ensureSafeTimeout(extendedTimeout);
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);
// Make sure timeout doesn't keep the process alive // Make sure timeout doesn't keep the process alive
if (record.cleanupTimer.unref) { if (record.cleanupTimer.unref) {
record.cleanupTimer.unref(); record.cleanupTimer.unref();
} }
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);
// Make sure timeout doesn't keep the process alive // Make sure timeout doesn't keep the process alive
if (record.cleanupTimer.unref) { if (record.cleanupTimer.unref) {
record.cleanupTimer.unref(); record.cleanupTimer.unref();
@ -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(
@ -1063,7 +1217,7 @@ export class PortProxy {
*/ */
private updateActivity(record: IConnectionRecord): void { private updateActivity(record: IConnectionRecord): void {
record.lastActivity = Date.now(); record.lastActivity = Date.now();
// Clear any inactivity warning // Clear any inactivity warning
if (record.inactivityWarningIssued) { if (record.inactivityWarningIssued) {
record.inactivityWarningIssued = false; record.inactivityWarningIssued = false;
@ -1082,7 +1236,7 @@ export class PortProxy {
} }
return this.settings.targetIP!; return this.settings.targetIP!;
} }
/** /**
* Initiates cleanup once for a connection * Initiates cleanup once for a connection
*/ */
@ -1090,12 +1244,15 @@ export class PortProxy {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
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);
} }
this.cleanupConnection(record, reason); this.cleanupConnection(record, reason);
} }
@ -1219,7 +1376,7 @@ export class PortProxy {
// Apply socket optimizations // Apply socket optimizations
socket.setNoDelay(this.settings.noDelay); socket.setNoDelay(this.settings.noDelay);
// Create a unique connection ID and record // Create a unique connection ID and record
const connectionId = generateConnectionId(); const connectionId = generateConnectionId();
const connectionRecord: IConnectionRecord = { const connectionRecord: IConnectionRecord = {
@ -1243,16 +1400,20 @@ export class PortProxy {
hasKeepAlive: false, // Will set to true if keep-alive is applied hasKeepAlive: false, // Will set to true if keep-alive is applied
incomingTerminationReason: null, incomingTerminationReason: null,
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
if (this.settings.keepAlive) { if (this.settings.keepAlive) {
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
// Apply enhanced TCP keep-alive options if enabled // Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) { if (this.settings.enableKeepAliveProbes) {
try { try {
@ -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}`
);
} }
} }
} }
@ -1279,8 +1442,9 @@ export class PortProxy {
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
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'}. ` +
`Active connections: ${this.connectionRecords.size}` `Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
`Active connections: ${this.connectionRecords.size}`
); );
} else { } else {
console.log( console.log(
@ -1418,12 +1582,12 @@ export class PortProxy {
)}` )}`
); );
} }
// Check if we should forward this to a NetworkProxy // Check if we should forward this to a NetworkProxy
if ( if (
isTlsHandshakeDetected && isTlsHandshakeDetected &&
domainConfig.useNetworkProxy === true && domainConfig.useNetworkProxy === true &&
initialChunk && initialChunk &&
this.networkProxies.length > 0 this.networkProxies.length > 0
) { ) {
return this.forwardToNetworkProxy( 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 // 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()];
@ -1661,20 +1833,23 @@ export class PortProxy {
} else { } else {
nonTlsConnections++; nonTlsConnections++;
} }
if (record.hasKeepAlive) { if (record.hasKeepAlive) {
keepAliveConnections++; keepAliveConnections++;
} }
if (record.usingNetworkProxy) { if (record.usingNetworkProxy) {
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,35 +1881,38 @@ 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
let effectiveTimeout = this.settings.inactivityTimeout!; let effectiveTimeout = this.settings.inactivityTimeout!;
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
const multiplier = this.settings.keepAliveInactivityMultiplier || 6; const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
effectiveTimeout = effectiveTimeout * multiplier; effectiveTimeout = effectiveTimeout * multiplier;
} }
if (inactivityTime > effectiveTimeout && !record.connectionClosed) { if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
// 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 ${
`Will close in 10 minutes if no activity.` record.remoteIP
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
`Will close in 10 minutes if no activity.`
); );
// Set warning flag and add grace period // Set warning flag and add grace period
record.inactivityWarningIssued = true; record.inactivityWarningIssued = true;
record.lastActivity = now - (effectiveTimeout - 600000); record.lastActivity = now - (effectiveTimeout - 600000);
// Try to stimulate activity with a probe packet // Try to stimulate activity with a probe packet
if (record.outgoing && !record.outgoing.destroyed) { if (record.outgoing && !record.outgoing.destroyed) {
try { try {
record.outgoing.write(Buffer.alloc(0)); record.outgoing.write(Buffer.alloc(0));
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${id}] Sent probe packet to test keep-alive connection`); 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 // For non-keep-alive or after warning, close the connection
console.log( console.log(
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` + `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
`for ${plugins.prettyMs(inactivityTime)}.` + `for ${plugins.prettyMs(inactivityTime)}.` +
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '') (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
); );
this.cleanupConnection(record, 'inactivity'); this.cleanupConnection(record, 'inactivity');
} }
} 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
)}. ` + )}. ` +
@ -1903,4 +2084,4 @@ export class PortProxy {
console.log('PortProxy shutdown complete.'); console.log('PortProxy shutdown complete.');
} }
} }