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:
2025-03-11 04:39:17 +00:00
parent 58ba0d9362
commit 865d21b36a
3 changed files with 294 additions and 56 deletions

View File

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

View File

@ -100,6 +100,8 @@ interface IConnectionRecord {
// Keep-alive tracking
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
incomingTerminationReason?: string | null; // Reason for incoming termination
outgoingTerminationReason?: string | null; // Reason for outgoing termination
@ -135,11 +137,11 @@ interface ITlsSessionCacheConfig {
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 = {
maxEntries: 10000, // Default max 10,000 entries
expiryTime: 24 * 60 * 60 * 1000, // 24 hours default
cleanupInterval: 10 * 60 * 1000, // Clean up every 10 minutes
maxEntries: 20000, // Default max 20,000 entries (doubled)
expiryTime: 7 * 24 * 60 * 60 * 1000, // 7 days default (increased from 24 hours)
cleanupInterval: 30 * 60 * 1000, // Clean up every 30 minutes (relaxed from 10 minutes)
enabled: true // Enabled by default
};
@ -1025,32 +1027,33 @@ export class PortProxy {
}
// 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) {
// 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';
// Adjust timeouts based on position in chain
// Adjust timeouts based on position in chain, but significantly relaxed
switch (chainPosition) {
case 'first':
// First proxy can be a bit more lenient as it handles browser connections
socketTimeout = 1500000; // 25 minutes
// First proxy handling browser connections
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
break;
case 'middle':
// Middle proxies need shorter timeouts
socketTimeout = 1200000; // 20 minutes
// Middle proxies
socketTimeout = 5 * 60 * 60 * 1000; // 5 hours
break;
case 'last':
// Last proxy directly connects to backend
socketTimeout = 1800000; // 30 minutes
// Last proxy connects to backend
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
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 = {
...settingsArg,
targetIP: targetIP,
@ -1060,39 +1063,39 @@ export class PortProxy {
chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
aggressiveTlsRefresh: aggressiveTlsRefresh,
// Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
initialDataTimeout: 60000, // 60 seconds for initial handshake
socketTimeout: socketTimeout, // Adjusted based on chain position
inactivityCheckInterval: isChainedProxy ? 30000 : 60000, // More frequent checks for chains
maxConnectionLifetime: isChainedProxy ? 2700000 : 3600000, // 45min or 1hr lifetime
inactivityTimeout: isChainedProxy ? 1200000 : 1800000, // 20min or 30min inactivity timeout
// Much more relaxed timeout settings
initialDataTimeout: 120000, // 2 minutes for initial handshake (doubled)
socketTimeout: socketTimeout, // 5-6 hours based on chain position
inactivityCheckInterval: 5 * 60 * 1000, // 5 minutes between checks (relaxed)
maxConnectionLifetime: 12 * 60 * 60 * 1000, // 12 hours lifetime
inactivityTimeout: 4 * 60 * 60 * 1000, // 4 hours inactivity timeout
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 60000, // 60 seconds
// Socket optimization settings
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds (increased)
maxPendingDataSize: settingsArg.maxPendingDataSize || 20 * 1024 * 1024, // 20MB to handle large TLS handshakes
// Feature flags - simplified with sensible defaults
disableInactivityCheck: false, // Always enable inactivity checks for TLS safety
enableKeepAliveProbes: true, // Always enable keep-alive probes for connection health
disableInactivityCheck: false, // Still enable inactivity checks
enableKeepAliveProbes: true, // Still enable keep-alive probes
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
// Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 200, // 200 connections per IP (doubled)
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 500, // 500 per minute (increased)
// Keep-alive settings with sensible defaults that ensure certificate safety
keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety
keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension
// Use shorter lifetime for chained proxies
// Keep-alive settings with much more relaxed defaults
keepAliveTreatment: 'extended', // Use extended keep-alive treatment
keepAliveInactivityMultiplier: 3, // 3x normal inactivity timeout for longer extension
// Much longer keep-alive lifetimes
extendedKeepAliveLifetime: isChainedProxy
? 2 * 60 * 60 * 1000 // 2 hours for chained proxies
: 3 * 60 * 60 * 1000, // 3 hours for standalone proxies
? 24 * 60 * 60 * 1000 // 24 hours for chained proxies
: 48 * 60 * 60 * 1000, // 48 hours for standalone proxies
};
// 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({
host: proxyHost,
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
@ -1165,6 +1171,30 @@ export class PortProxy {
record.outgoingStartTime = Date.now();
record.usingNetworkProxy = true;
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
proxySocket.on('error', (err) => {
@ -1213,6 +1243,41 @@ export class PortProxy {
// Update activity on data transfer from the proxy socket
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) {
console.log(
@ -1334,17 +1399,25 @@ export class PortProxy {
// Apply keep-alive settings to the outgoing connection as well
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
if (this.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in targetSocket) {
(targetSocket as any).setKeepAliveProbes(10);
(targetSocket as any).setKeepAliveProbes(10); // Same probes as incoming
}
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) {
// Ignore errors - these are optional enhancements
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
@ -1542,15 +1652,37 @@ export class PortProxy {
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
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) => {
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
this.updateActivity(record);
try {
// Enhanced logging for renegotiation
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
// Extract all TLS information including session resumption data
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;
// 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;
}
// 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) {
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
return;
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
// 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
@ -1605,15 +1744,34 @@ export class PortProxy {
} else {
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
this.initiateCleanupOnce(record, 'sni_mismatch');
return;
}
} else {
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
}
} catch (err) {
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
@ -1651,15 +1809,37 @@ export class PortProxy {
} else {
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
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) => {
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
this.updateActivity(record);
try {
// Enhanced logging for renegotiation
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
// Extract all TLS information including session resumption data
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;
// 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;
}
// 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) {
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
return;
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
// 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
@ -1745,15 +1932,45 @@ export class PortProxy {
} else {
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
this.initiateCleanupOnce(record, 'sni_mismatch');
return;
}
} else {
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
}
} catch (err) {
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
@ -1974,15 +2191,16 @@ export class PortProxy {
if (record.lastActivity > 0) {
const timeDiff = now - record.lastActivity;
// Enhanced sleep detection with graduated thresholds
// For chained proxies, we need to be more aggressive about refreshing connections
const isChainedProxy = this.settings.targetIP === 'localhost' || this.settings.targetIP === '127.0.0.1';
// Enhanced sleep detection with graduated thresholds - much more relaxed
// Using chain detection from settings instead of recalculating
const isChainedProxy = this.settings.isChainedProxy || false;
const minuteInMs = 60 * 1000;
const hourInMs = 60 * minuteInMs;
// Different thresholds based on connection type and configuration
const shortInactivityThreshold = isChainedProxy ? 10 * minuteInMs : 15 * minuteInMs;
const mediumInactivityThreshold = isChainedProxy ? 20 * minuteInMs : 30 * minuteInMs;
const longInactivityThreshold = isChainedProxy ? 60 * minuteInMs : 120 * minuteInMs;
// Significantly relaxed thresholds for better stability
const shortInactivityThreshold = 30 * minuteInMs; // 30 minutes
const mediumInactivityThreshold = 2 * hourInMs; // 2 hours
const longInactivityThreshold = 8 * hourInMs; // 8 hours
// Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
if (timeDiff > shortInactivityThreshold) {
@ -2467,12 +2685,20 @@ export class PortProxy {
// Initialize sleep detection fields
possibleSystemSleep: false,
// Track keep-alive state for both sides of the connection
incomingKeepAliveEnabled: false,
outgoingKeepAliveEnabled: false,
};
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
// Configure incoming socket keep-alive
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
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
if (this.settings.enableKeepAliveProbes) {
@ -2484,6 +2710,8 @@ export class PortProxy {
if ('setKeepAliveInterval' in socket) {
(socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
}
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {