fix(PortProxy): Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation

This commit is contained in:
Philipp Kunz 2025-03-10 22:35:34 +00:00
parent 009e3c4f0e
commit c415a6c361
3 changed files with 48 additions and 20 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-03-10 - 3.30.4 - fix(PortProxy)
Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation
- Allow TLS renegotiation data without an explicit SNI extraction to pass through, ensuring valid renegotiations are not dropped (critical for Chrome).
- Update TLS keep-alive timeout from an aggressive 30 minutes to a more generous 4 hours to reduce unnecessary reconnections.
- Increase inactivity thresholds for TLS connections from 20 minutes to 2 hours with an additional verification interval extended from 5 to 15 minutes.
- Adjust long-lived TLS connection timeout from 45 minutes to 8 hours for improved certificate context refresh in chained proxy scenarios.
## 2025-03-10 - 3.30.3 - fix(classes.portproxy.ts) ## 2025-03-10 - 3.30.3 - fix(classes.portproxy.ts)
Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues

View File

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

@ -515,7 +515,9 @@ export class PortProxy {
); );
} }
// Let the NetworkProxy handle the TLS renegotiation // NOTE: We don't need to explicitly forward the renegotiation packets
// because socket.pipe(proxySocket) is already handling that.
// The pipe ensures all data (including renegotiation) flows through properly.
// Just update the activity timestamp to prevent timeouts // Just update the activity timestamp to prevent timeouts
record.lastActivity = Date.now(); record.lastActivity = Date.now();
} }
@ -848,11 +850,28 @@ export class PortProxy {
// Add the renegotiation listener for SNI validation // Add the renegotiation listener for SNI validation
if (serverName) { if (serverName) {
// This listener will check for TLS renegotiation attempts
// Note: We don't need to explicitly forward the renegotiation packets
// since socket.pipe(targetSocket) is already set up earlier and handles that
socket.on('data', (renegChunk: Buffer) => { socket.on('data', (renegChunk: Buffer) => {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) { if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
try { try {
// Try to extract SNI from potential renegotiation // Try to extract SNI from potential renegotiation
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging); const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
// Otherwise valid renegotiations that don't explicitly repeat the SNI will break
if (newSNI === undefined) {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Rehandshake detected without SNI, allowing it through.`
);
}
// Let it pass through - this is critical for Chrome's TLS handling
return;
}
// Only block if we positively identify a different SNI
if (newSNI && newSNI !== record.lockedDomain) { if (newSNI && newSNI !== record.lockedDomain) {
console.log( console.log(
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.` `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
@ -864,6 +883,8 @@ export class PortProxy {
); );
} }
} catch (err) { } catch (err) {
// Always allow the renegotiation to continue if we encounter an error
// This ensures Chrome can complete its TLS renegotiation
console.log( console.log(
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.` `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
); );
@ -886,13 +907,12 @@ export class PortProxy {
} }
// No cleanup timer for immortal connections // No cleanup timer for immortal connections
} }
// For TLS keep-alive connections, use an aggressive timeout to ensure // For TLS keep-alive connections, use a more generous timeout now that
// certificates are regularly refreshed even in chained proxy scenarios // we've fixed the renegotiation handling issue that was causing certificate problems
else if (record.hasKeepAlive && record.isTLS) { else if (record.hasKeepAlive && record.isTLS) {
// Use a much shorter timeout for TLS connections to ensure certificate contexts are refreshed frequently // Use a longer timeout for TLS connections now that renegotiation handling is fixed
// This prevents issues with stale certificates in browser tabs that have been idle for a while // This reduces unnecessary reconnections while still ensuring certificate freshness
// 30 minutes is aggressive enough to handle multi-proxy chains without causing too many reconnects const tlsKeepAliveTimeout = 4 * 60 * 60 * 1000; // 4 hours for TLS keep-alive - increased from 30 minutes
const tlsKeepAliveTimeout = 30 * 60 * 1000; // 30 minutes for TLS keep-alive - dramatically reduced from 8 hours
const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout); const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
record.cleanupTimer = setTimeout(() => { record.cleanupTimer = setTimeout(() => {
@ -1063,34 +1083,34 @@ export class PortProxy {
// For TLS keep-alive connections after sleep/long inactivity, force close // For TLS keep-alive connections after sleep/long inactivity, force close
// to make browser establish a new connection with fresh certificate context // to make browser establish a new connection with fresh certificate context
if (record.isTLS && record.tlsHandshakeComplete) { if (record.isTLS && record.tlsHandshakeComplete) {
// Much more aggressive timeout (20 minutes) to ensure reliable operation in chained proxy scenarios // More generous timeout now that we've fixed the renegotiation handling
if (timeDiff > 20 * 60 * 1000) { if (timeDiff > 2 * 60 * 60 * 1000) {
// If inactive for more than 20 minutes (reduced from 4 hours) // If inactive for more than 2 hours (increased from 20 minutes)
console.log( console.log(
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` + `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
`Closing to force new connection with fresh certificate.` `Closing to force new connection with fresh certificate.`
); );
return this.initiateCleanupOnce(record, 'certificate_refresh_needed'); return this.initiateCleanupOnce(record, 'certificate_refresh_needed');
} else if (timeDiff > 10 * 60 * 1000) { } else if (timeDiff > 30 * 60 * 1000) {
// For shorter but still significant inactivity (10+ minutes), be more aggressive with refresh // For shorter but still significant inactivity (30+ minutes), refresh TLS state
console.log( console.log(
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` + `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
`Aggressively refreshing TLS state to prevent certificate issues in proxy chains.` `Refreshing TLS state.`
); );
this.refreshTlsStateAfterSleep(record); this.refreshTlsStateAfterSleep(record);
// Add an additional check in 5 minutes if no activity // Add an additional check in 15 minutes if no activity
const refreshCheckId = record.id; const refreshCheckId = record.id;
const refreshCheck = setTimeout(() => { const refreshCheck = setTimeout(() => {
const currentRecord = this.connectionRecords.get(refreshCheckId); const currentRecord = this.connectionRecords.get(refreshCheckId);
if (currentRecord && Date.now() - currentRecord.lastActivity > 5 * 60 * 1000) { if (currentRecord && Date.now() - currentRecord.lastActivity > 15 * 60 * 1000) {
console.log( console.log(
`[${refreshCheckId}] No activity detected after TLS refresh. ` + `[${refreshCheckId}] No activity detected after TLS refresh. ` +
`Closing connection to ensure certificate freshness.` `Closing connection to ensure certificate freshness.`
); );
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed'); this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
} }
}, 5 * 60 * 1000); }, 15 * 60 * 1000);
// Make sure timeout doesn't keep the process alive // Make sure timeout doesn't keep the process alive
if (refreshCheck.unref) { if (refreshCheck.unref) {
@ -1133,9 +1153,9 @@ export class PortProxy {
const connectionAge = Date.now() - record.incomingStartTime; const connectionAge = Date.now() - record.incomingStartTime;
const hourInMs = 60 * 60 * 1000; const hourInMs = 60 * 60 * 1000;
// For TLS browser connections, use a much more aggressive timeout // For TLS browser connections, use a more generous timeout now that
// to avoid certificate issues, especially in chained proxy scenarios // we've fixed the renegotiation handling issues
if (record.isTLS && record.hasKeepAlive && connectionAge > 45 * 60 * 1000) { // 45 minutes instead of 12 hours if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
console.log( console.log(
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` + `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
`Closing to ensure proper certificate handling on browser reconnect in proxy chain.` `Closing to ensure proper certificate handling on browser reconnect in proxy chain.`