|
|
|
@ -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
|
|
|
|
@ -1166,6 +1172,30 @@ export class PortProxy {
|
|
|
|
|
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) => {
|
|
|
|
|
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
|
|
@ -1214,6 +1244,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(
|
|
|
|
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
|
|
@ -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) {
|
|
|
|
|