fix(portproxy): Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records.

This commit is contained in:
Philipp Kunz 2025-03-11 04:39:17 +00:00
parent 58ba0d9362
commit 865d21b36a
3 changed files with 294 additions and 56 deletions

@ -1,5 +1,15 @@
# Changelog # Changelog
## 2025-03-11 - 3.32.1 - fix(portproxy)
Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records.
- Increased TLS session cache maximum entries from 10,000 to 20,000, expiry time from 24 hours to 7 days, and cleanup interval from 10 minutes to 30 minutes
- Relaxed socket timeouts: standalone connections now use up to 6 hours, with chained proxies adjusted for 56 hours based on proxy position
- Updated inactivity, connection, and initial handshake timeouts to provide a more relaxed behavior under high-traffic chained proxy scenarios
- Increased keepAliveInitialDelay from 10 seconds to 30 seconds and introduced separate incoming and outgoing keep-alive flags
- Enhanced TLS renegotiation handling with more detailed logging and temporary processing flags to avoid duplicate processing
- Updated NetworkProxy integration to use optimized connection settings and more aggressive application-level keep-alive probes
## 2025-03-11 - 3.32.0 - feat(PortProxy) ## 2025-03-11 - 3.32.0 - feat(PortProxy)
Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh. Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh.

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

@ -100,6 +100,8 @@ interface IConnectionRecord {
// 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
incomingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on incoming socket
outgoingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on outgoing socket
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
@ -135,11 +137,11 @@ interface ITlsSessionCacheConfig {
enabled: boolean; // Whether session caching is enabled enabled: boolean; // Whether session caching is enabled
} }
// Default configuration for session cache // Default configuration for session cache with relaxed timeouts
const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = { const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
maxEntries: 10000, // Default max 10,000 entries maxEntries: 20000, // Default max 20,000 entries (doubled)
expiryTime: 24 * 60 * 60 * 1000, // 24 hours default expiryTime: 7 * 24 * 60 * 60 * 1000, // 7 days default (increased from 24 hours)
cleanupInterval: 10 * 60 * 1000, // Clean up every 10 minutes cleanupInterval: 30 * 60 * 1000, // Clean up every 30 minutes (relaxed from 10 minutes)
enabled: true // Enabled by default enabled: true // Enabled by default
}; };
@ -1025,32 +1027,33 @@ export class PortProxy {
} }
// Determine appropriate timeouts based on proxy chain position // Determine appropriate timeouts based on proxy chain position
let socketTimeout = 1800000; // 30 minutes default // Much more relaxed socket timeouts
let socketTimeout = 6 * 60 * 60 * 1000; // 6 hours default for standalone
if (isChainedProxy) { if (isChainedProxy) {
// Use shorter timeouts for chained proxies to prevent certificate issues // Still adjust based on chain position, but with more relaxed values
const chainPosition = settingsArg.chainPosition || 'middle'; const chainPosition = settingsArg.chainPosition || 'middle';
// Adjust timeouts based on position in chain // Adjust timeouts based on position in chain, but significantly relaxed
switch (chainPosition) { switch (chainPosition) {
case 'first': case 'first':
// First proxy can be a bit more lenient as it handles browser connections // First proxy handling browser connections
socketTimeout = 1500000; // 25 minutes socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
break; break;
case 'middle': case 'middle':
// Middle proxies need shorter timeouts // Middle proxies
socketTimeout = 1200000; // 20 minutes socketTimeout = 5 * 60 * 60 * 1000; // 5 hours
break; break;
case 'last': case 'last':
// Last proxy directly connects to backend // Last proxy connects to backend
socketTimeout = 1800000; // 30 minutes socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
break; break;
} }
console.log(`Configured as ${chainPosition} proxy in chain. Using adjusted timeouts for optimal TLS handling.`); console.log(`Configured as ${chainPosition} proxy in chain. Using relaxed timeouts for better stability.`);
} }
// Set hardcoded sensible defaults for all settings with chain-aware adjustments // Set sensible defaults with significantly relaxed timeouts
this.settings = { this.settings = {
...settingsArg, ...settingsArg,
targetIP: targetIP, targetIP: targetIP,
@ -1060,39 +1063,39 @@ export class PortProxy {
chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'), chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
aggressiveTlsRefresh: aggressiveTlsRefresh, aggressiveTlsRefresh: aggressiveTlsRefresh,
// Hardcoded timeout settings optimized for TLS safety in all deployment scenarios // Much more relaxed timeout settings
initialDataTimeout: 60000, // 60 seconds for initial handshake initialDataTimeout: 120000, // 2 minutes for initial handshake (doubled)
socketTimeout: socketTimeout, // Adjusted based on chain position socketTimeout: socketTimeout, // 5-6 hours based on chain position
inactivityCheckInterval: isChainedProxy ? 30000 : 60000, // More frequent checks for chains inactivityCheckInterval: 5 * 60 * 1000, // 5 minutes between checks (relaxed)
maxConnectionLifetime: isChainedProxy ? 2700000 : 3600000, // 45min or 1hr lifetime maxConnectionLifetime: 12 * 60 * 60 * 1000, // 12 hours lifetime
inactivityTimeout: isChainedProxy ? 1200000 : 1800000, // 20min or 30min inactivity timeout inactivityTimeout: 4 * 60 * 60 * 1000, // 4 hours inactivity timeout
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 60000, // 60 seconds
// Socket optimization settings // Socket optimization settings
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds (increased)
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes maxPendingDataSize: settingsArg.maxPendingDataSize || 20 * 1024 * 1024, // 20MB to handle large TLS handshakes
// Feature flags - simplified with sensible defaults // Feature flags - simplified with sensible defaults
disableInactivityCheck: false, // Always enable inactivity checks for TLS safety disableInactivityCheck: false, // Still enable inactivity checks
enableKeepAliveProbes: true, // Always enable keep-alive probes for connection health enableKeepAliveProbes: true, // Still enable keep-alive probes
enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
// Rate limiting defaults // Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 200, // 200 connections per IP (doubled)
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 500, // 500 per minute (increased)
// Keep-alive settings with sensible defaults that ensure certificate safety // Keep-alive settings with much more relaxed defaults
keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety keepAliveTreatment: 'extended', // Use extended keep-alive treatment
keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension keepAliveInactivityMultiplier: 3, // 3x normal inactivity timeout for longer extension
// Use shorter lifetime for chained proxies // Much longer keep-alive lifetimes
extendedKeepAliveLifetime: isChainedProxy extendedKeepAliveLifetime: isChainedProxy
? 2 * 60 * 60 * 1000 // 2 hours for chained proxies ? 24 * 60 * 60 * 1000 // 24 hours for chained proxies
: 3 * 60 * 60 * 1000, // 3 hours for standalone proxies : 48 * 60 * 60 * 1000, // 48 hours for standalone proxies
}; };
// Store NetworkProxy instances if provided // Store NetworkProxy instances if provided
@ -1154,10 +1157,13 @@ export class PortProxy {
); );
} }
// Create a connection to the NetworkProxy // Create a connection to the NetworkProxy with optimized settings for reliability
const proxySocket = plugins.net.connect({ const proxySocket = plugins.net.connect({
host: proxyHost, host: proxyHost,
port: proxyPort, port: proxyPort,
noDelay: true, // Disable Nagle's algorithm for NetworkProxy connections
keepAlive: this.settings.keepAlive, // Use the same keepAlive setting as regular connections
keepAliveInitialDelay: Math.max(this.settings.keepAliveInitialDelay - 5000, 5000) // Slightly faster
}); });
// Store the outgoing socket in the record // Store the outgoing socket in the record
@ -1165,6 +1171,30 @@ export class PortProxy {
record.outgoingStartTime = Date.now(); record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true; record.usingNetworkProxy = true;
record.networkProxyIndex = proxyIndex; record.networkProxyIndex = proxyIndex;
// Mark keep-alive as enabled on outgoing if requested
if (this.settings.keepAlive) {
record.outgoingKeepAliveEnabled = true;
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in proxySocket) {
(proxySocket as any).setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in proxySocket) {
(proxySocket as any).setKeepAliveInterval(800);
}
console.log(`[${connectionId}] Enhanced TCP keep-alive configured for NetworkProxy connection`);
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Enhanced keep-alive not supported for NetworkProxy: ${err}`);
}
}
}
}
// Set up error handlers // Set up error handlers
proxySocket.on('error', (err) => { proxySocket.on('error', (err) => {
@ -1213,6 +1243,41 @@ export class PortProxy {
// Update activity on data transfer from the proxy socket // Update activity on data transfer from the proxy socket
proxySocket.on('data', () => this.updateActivity(record)); proxySocket.on('data', () => this.updateActivity(record));
// Special handling for application-level keep-alives on NetworkProxy connections
if (this.settings.keepAlive && record.isTLS) {
// Set up a timer to periodically send application-level keep-alives
const keepAliveTimer = setInterval(() => {
if (proxySocket && !proxySocket.destroyed && record && !record.connectionClosed) {
try {
// Send 0-byte packet as application-level keep-alive
proxySocket.write(Buffer.alloc(0));
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Sent application-level keep-alive to NetworkProxy connection`);
}
} catch (err) {
// If we can't write, the connection is probably already dead
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Error sending application-level keep-alive to NetworkProxy: ${err}`);
}
// Stop the timer if we hit an error
clearInterval(keepAliveTimer);
}
} else {
// Clean up timer if connection is gone
clearInterval(keepAliveTimer);
}
}, 60000); // Send keep-alive every minute
// Make sure interval doesn't prevent process exit
if (keepAliveTimer.unref) {
keepAliveTimer.unref();
}
console.log(`[${connectionId}] Application-level keep-alive configured for NetworkProxy connection`);
}
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
@ -1334,17 +1399,25 @@ export class PortProxy {
// 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); // Use a slightly shorter initial delay for outgoing to ensure it stays active
const outgoingInitialDelay = Math.max(this.settings.keepAliveInitialDelay - 5000, 5000);
targetSocket.setKeepAlive(true, outgoingInitialDelay);
record.outgoingKeepAliveEnabled = true;
console.log(`[${connectionId}] Keep-alive enabled on outgoing connection with initial delay: ${outgoingInitialDelay}ms`);
// 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 {
if ('setKeepAliveProbes' in targetSocket) { if ('setKeepAliveProbes' in targetSocket) {
(targetSocket as any).setKeepAliveProbes(10); (targetSocket as any).setKeepAliveProbes(10); // Same probes as incoming
} }
if ('setKeepAliveInterval' in targetSocket) { if ('setKeepAliveInterval' in targetSocket) {
(targetSocket as any).setKeepAliveInterval(1000); // Use a shorter interval on outgoing for more reliable detection
(targetSocket as any).setKeepAliveInterval(800); // Slightly faster than incoming
} }
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on outgoing connection`);
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
@ -1354,6 +1427,43 @@ export class PortProxy {
} }
} }
} }
// Special handling for TLS keep-alive - we want to be more aggressive
// with keeping the outgoing connection alive in TLS mode
if (record.isTLS) {
// Set a timer to periodically send empty data to keep connection alive
// This is in addition to TCP keep-alive, works at application layer
const keepAliveTimer = setInterval(() => {
if (targetSocket && !targetSocket.destroyed && record && !record.connectionClosed) {
try {
// Send 0-byte packet as application-level keep-alive
targetSocket.write(Buffer.alloc(0));
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Sent application-level keep-alive to outgoing TLS connection`);
}
} catch (err) {
// If we can't write, the connection is probably already dead
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Error sending application-level keep-alive: ${err}`);
}
// Stop the timer if we hit an error
clearInterval(keepAliveTimer);
}
} else {
// Clean up timer if connection is gone
clearInterval(keepAliveTimer);
}
}, 60000); // Send keep-alive every minute
// Make sure interval doesn't prevent process exit
if (keepAliveTimer.unref) {
keepAliveTimer.unref();
}
console.log(`[${connectionId}] Application-level keep-alive configured for TLS outgoing connection`);
}
} }
// Setup specific error handler for connection phase with enhanced retries // Setup specific error handler for connection phase with enhanced retries
@ -1542,15 +1652,37 @@ export class PortProxy {
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
if (serverName && record.isTLS) { if (serverName && record.isTLS) {
// This listener handles TLS renegotiation detection // Create a flag to prevent double-processing of the same handshake packet
let processingRenegotiation = false;
// This listener handles TLS renegotiation detection on the incoming socket
socket.on('data', (renegChunk) => { socket.on('data', (renegChunk) => {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { // Only check for content type 22 (handshake) and not already processing
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
processingRenegotiation = true;
// Always update activity timestamp for any handshake packet // Always update activity timestamp for any handshake packet
this.updateActivity(record); this.updateActivity(record);
try { try {
// Enhanced logging for renegotiation
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
// Extract all TLS information including session resumption data // Extract all TLS information including session resumption data
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging); const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
// Log details about the handshake packet
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
isResumption: sniInfo?.isResumption || false,
serverName: sniInfo?.serverName || 'none',
resumedDomain: sniInfo?.resumedDomain || 'none',
recordsExamined: sniInfo?.recordsExamined || 0,
multipleRecords: sniInfo?.multipleRecords || false,
partialExtract: sniInfo?.partialExtract || false
});
}
let newSNI = sniInfo?.serverName; let newSNI = sniInfo?.serverName;
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
@ -1559,10 +1691,17 @@ export class PortProxy {
newSNI = sniInfo.resumedDomain; newSNI = sniInfo.resumedDomain;
} }
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through // IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
// we still need to make sure it's properly forwarded to maintain the TLS state
if (newSNI === undefined) { if (newSNI === undefined) {
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`); console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
return;
// Set a temporary timeout to reset the processing flag
setTimeout(() => {
processingRenegotiation = false;
}, 500);
return; // Let the piping handle the forwarding
} }
// Check if the SNI has changed // Check if the SNI has changed
@ -1605,15 +1744,34 @@ export class PortProxy {
} else { } else {
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`); console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
this.initiateCleanupOnce(record, 'sni_mismatch'); this.initiateCleanupOnce(record, 'sni_mismatch');
return;
} }
} else { } else {
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`); console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
} }
} catch (err) { } catch (err) {
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`); console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
} finally {
// Reset the processing flag after a small delay to prevent double-processing
// of packets that may be part of the same handshake
setTimeout(() => {
processingRenegotiation = false;
}, 500);
} }
} }
}); });
// Set up a listener on the outgoing socket to detect issues with renegotiation
// This helps catch cases where the outgoing connection has closed but the incoming is still active
targetSocket.on('error', (err) => {
// If we get an error during what might be a renegotiation, log it specially
if (processingRenegotiation) {
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
// Force immediate cleanup to prevent hanging connections
this.initiateCleanupOnce(record, 'renegotiation_error');
}
// The normal error handler will be called for other errors
});
} }
// Now set up piping for future data and resume the socket // Now set up piping for future data and resume the socket
@ -1651,15 +1809,37 @@ export class PortProxy {
} else { } else {
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
if (serverName && record.isTLS) { if (serverName && record.isTLS) {
// This listener handles TLS renegotiation detection // Create a flag to prevent double-processing of the same handshake packet
let processingRenegotiation = false;
// This listener handles TLS renegotiation detection on the incoming socket
socket.on('data', (renegChunk) => { socket.on('data', (renegChunk) => {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { // Only check for content type 22 (handshake) and not already processing
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
processingRenegotiation = true;
// Always update activity timestamp for any handshake packet // Always update activity timestamp for any handshake packet
this.updateActivity(record); this.updateActivity(record);
try { try {
// Enhanced logging for renegotiation
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
// Extract all TLS information including session resumption data // Extract all TLS information including session resumption data
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging); const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
// Log details about the handshake packet
if (this.settings.enableTlsDebugLogging) {
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
isResumption: sniInfo?.isResumption || false,
serverName: sniInfo?.serverName || 'none',
resumedDomain: sniInfo?.resumedDomain || 'none',
recordsExamined: sniInfo?.recordsExamined || 0,
multipleRecords: sniInfo?.multipleRecords || false,
partialExtract: sniInfo?.partialExtract || false
});
}
let newSNI = sniInfo?.serverName; let newSNI = sniInfo?.serverName;
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
@ -1668,10 +1848,17 @@ export class PortProxy {
newSNI = sniInfo.resumedDomain; newSNI = sniInfo.resumedDomain;
} }
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through // IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
// we still need to make sure it's properly forwarded to maintain the TLS state
if (newSNI === undefined) { if (newSNI === undefined) {
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`); console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
return;
// Set a temporary timeout to reset the processing flag
setTimeout(() => {
processingRenegotiation = false;
}, 500);
return; // Let the piping handle the forwarding
} }
// Check if the SNI has changed // Check if the SNI has changed
@ -1745,15 +1932,45 @@ export class PortProxy {
} else { } else {
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`); console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
this.initiateCleanupOnce(record, 'sni_mismatch'); this.initiateCleanupOnce(record, 'sni_mismatch');
return;
} }
} else { } else {
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`); console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
} }
} catch (err) { } catch (err) {
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`); console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
} finally {
// Reset the processing flag after a small delay to prevent double-processing
// of packets that may be part of the same handshake
setTimeout(() => {
processingRenegotiation = false;
}, 500);
} }
} }
}); });
// Set up a listener on the outgoing socket to detect issues with renegotiation
// This helps catch cases where the outgoing connection has closed but the incoming is still active
targetSocket.on('error', (err) => {
// If we get an error during what might be a renegotiation, log it specially
if (processingRenegotiation) {
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
// Force immediate cleanup to prevent hanging connections
this.initiateCleanupOnce(record, 'renegotiation_error');
}
// The normal error handler will be called for other errors
});
// Also monitor targetSocket for connection issues during client handshakes
targetSocket.on('close', () => {
// If the outgoing socket closes during renegotiation, it's a critical issue
if (processingRenegotiation) {
console.log(`[${connectionId}] CRITICAL: Outgoing socket closed during TLS renegotiation!`);
console.log(`[${connectionId}] This likely explains cert mismatch errors in the browser.`);
// Force immediate cleanup on the client side
this.initiateCleanupOnce(record, 'target_closed_during_renegotiation');
}
});
} }
// Now set up piping // Now set up piping
@ -1974,15 +2191,16 @@ export class PortProxy {
if (record.lastActivity > 0) { if (record.lastActivity > 0) {
const timeDiff = now - record.lastActivity; const timeDiff = now - record.lastActivity;
// Enhanced sleep detection with graduated thresholds // Enhanced sleep detection with graduated thresholds - much more relaxed
// For chained proxies, we need to be more aggressive about refreshing connections // Using chain detection from settings instead of recalculating
const isChainedProxy = this.settings.targetIP === 'localhost' || this.settings.targetIP === '127.0.0.1'; const isChainedProxy = this.settings.isChainedProxy || false;
const minuteInMs = 60 * 1000; const minuteInMs = 60 * 1000;
const hourInMs = 60 * minuteInMs;
// Different thresholds based on connection type and configuration // Significantly relaxed thresholds for better stability
const shortInactivityThreshold = isChainedProxy ? 10 * minuteInMs : 15 * minuteInMs; const shortInactivityThreshold = 30 * minuteInMs; // 30 minutes
const mediumInactivityThreshold = isChainedProxy ? 20 * minuteInMs : 30 * minuteInMs; const mediumInactivityThreshold = 2 * hourInMs; // 2 hours
const longInactivityThreshold = isChainedProxy ? 60 * minuteInMs : 120 * minuteInMs; const longInactivityThreshold = 8 * hourInMs; // 8 hours
// Short inactivity (10-15 mins) - Might be temporary network issue or short sleep // Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
if (timeDiff > shortInactivityThreshold) { if (timeDiff > shortInactivityThreshold) {
@ -2467,12 +2685,20 @@ export class PortProxy {
// Initialize sleep detection fields // Initialize sleep detection fields
possibleSystemSleep: false, possibleSystemSleep: false,
// Track keep-alive state for both sides of the connection
incomingKeepAliveEnabled: false,
outgoingKeepAliveEnabled: false,
}; };
// Apply keep-alive settings if enabled // Apply keep-alive settings if enabled
if (this.settings.keepAlive) { if (this.settings.keepAlive) {
// Configure incoming socket keep-alive
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
connectionRecord.incomingKeepAliveEnabled = true;
console.log(`[${connectionId}] Keep-alive enabled on incoming connection with initial delay: ${this.settings.keepAliveInitialDelay}ms`);
// Apply enhanced TCP keep-alive options if enabled // Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) { if (this.settings.enableKeepAliveProbes) {
@ -2484,6 +2710,8 @@ export class PortProxy {
if ('setKeepAliveInterval' in socket) { if ('setKeepAliveInterval' in socket) {
(socket as any).setKeepAliveInterval(1000); // 1 second interval between probes (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
} }
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
} catch (err) { } catch (err) {
// Ignore errors - these are optional enhancements // Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {