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
## 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)
Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues

View File

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

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
record.lastActivity = Date.now();
}
@ -848,11 +850,28 @@ export class PortProxy {
// Add the renegotiation listener for SNI validation
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) => {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
try {
// Try to extract SNI from potential renegotiation
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) {
console.log(
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
@ -864,6 +883,8 @@ export class PortProxy {
);
}
} catch (err) {
// Always allow the renegotiation to continue if we encounter an error
// This ensures Chrome can complete its TLS renegotiation
console.log(
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
);
@ -886,13 +907,12 @@ export class PortProxy {
}
// No cleanup timer for immortal connections
}
// For TLS keep-alive connections, use an aggressive timeout to ensure
// certificates are regularly refreshed even in chained proxy scenarios
// For TLS keep-alive connections, use a more generous timeout now that
// we've fixed the renegotiation handling issue that was causing certificate problems
else if (record.hasKeepAlive && record.isTLS) {
// Use a much shorter timeout for TLS connections to ensure certificate contexts are refreshed frequently
// This prevents issues with stale certificates in browser tabs that have been idle for a while
// 30 minutes is aggressive enough to handle multi-proxy chains without causing too many reconnects
const tlsKeepAliveTimeout = 30 * 60 * 1000; // 30 minutes for TLS keep-alive - dramatically reduced from 8 hours
// Use a longer timeout for TLS connections now that renegotiation handling is fixed
// This reduces unnecessary reconnections while still ensuring certificate freshness
const tlsKeepAliveTimeout = 4 * 60 * 60 * 1000; // 4 hours for TLS keep-alive - increased from 30 minutes
const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
record.cleanupTimer = setTimeout(() => {
@ -1063,34 +1083,34 @@ export class PortProxy {
// For TLS keep-alive connections after sleep/long inactivity, force close
// to make browser establish a new connection with fresh certificate context
if (record.isTLS && record.tlsHandshakeComplete) {
// Much more aggressive timeout (20 minutes) to ensure reliable operation in chained proxy scenarios
if (timeDiff > 20 * 60 * 1000) {
// If inactive for more than 20 minutes (reduced from 4 hours)
// More generous timeout now that we've fixed the renegotiation handling
if (timeDiff > 2 * 60 * 60 * 1000) {
// If inactive for more than 2 hours (increased from 20 minutes)
console.log(
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
`Closing to force new connection with fresh certificate.`
);
return this.initiateCleanupOnce(record, 'certificate_refresh_needed');
} else if (timeDiff > 10 * 60 * 1000) {
// For shorter but still significant inactivity (10+ minutes), be more aggressive with refresh
} else if (timeDiff > 30 * 60 * 1000) {
// For shorter but still significant inactivity (30+ minutes), refresh TLS state
console.log(
`[${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);
// 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 refreshCheck = setTimeout(() => {
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(
`[${refreshCheckId}] No activity detected after TLS refresh. ` +
`Closing connection to ensure certificate freshness.`
);
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
}
}, 5 * 60 * 1000);
}, 15 * 60 * 1000);
// Make sure timeout doesn't keep the process alive
if (refreshCheck.unref) {
@ -1133,9 +1153,9 @@ export class PortProxy {
const connectionAge = Date.now() - record.incomingStartTime;
const hourInMs = 60 * 60 * 1000;
// For TLS browser connections, use a much more aggressive timeout
// to avoid certificate issues, especially in chained proxy scenarios
if (record.isTLS && record.hasKeepAlive && connectionAge > 45 * 60 * 1000) { // 45 minutes instead of 12 hours
// For TLS browser connections, use a more generous timeout now that
// we've fixed the renegotiation handling issues
if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
console.log(
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
`Closing to ensure proper certificate handling on browser reconnect in proxy chain.`