Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99d28eafd1 | |||
| 788b444fcc | |||
| 4225abe3c4 | |||
| 74fdb58f84 | |||
| bffdaffe39 | |||
| 67a4228518 | |||
| 681209f2e1 | |||
| c415a6c361 |
26
changelog.md
26
changelog.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-11 - 3.30.7 - fix(PortProxy)
|
||||
Improve TLS renegotiation SNI handling by first checking if the new SNI is allowed under the existing domain config. If not, attempt to find an alternative domain config and update the locked domain accordingly; otherwise, terminate the connection on SNI mismatch.
|
||||
|
||||
- Added a preliminary check against the original domain config to allow re-handshakes if the new SNI matches allowed patterns.
|
||||
- If the original config does not allow, search for an alternative domain config and validate IP rules.
|
||||
- Update the locked domain when allowed, ensuring connection reuse with valid certificate context.
|
||||
- Terminate the connection if no suitable domain config is found or IP restrictions are violated.
|
||||
|
||||
## 2025-03-11 - 3.30.6 - fix(PortProxy)
|
||||
Improve TLS renegotiation handling in PortProxy by validating the new SNI against allowed domain configurations. If the new SNI is permitted based on existing IP rules, update the locked domain to allow connection reuse; otherwise, terminate the connection to prevent misrouting.
|
||||
|
||||
- Added logic to check if a new SNI during renegotiation is allowed by comparing IP rules from the matching domain configuration.
|
||||
- Updated detailed logging to indicate when a valid SNI change is accepted and when it results in a mismatch termination.
|
||||
|
||||
## 2025-03-10 - 3.30.5 - fix(internal)
|
||||
No uncommitted changes detected; project files and tests remain unchanged.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "3.30.3",
|
||||
"version": "3.30.7",
|
||||
"private": false,
|
||||
"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.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '3.30.3',
|
||||
version: '3.30.7',
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -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,22 +850,104 @@ 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;
|
||||
}
|
||||
|
||||
// Check if the SNI has changed
|
||||
if (newSNI && newSNI !== record.lockedDomain) {
|
||||
console.log(
|
||||
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
|
||||
);
|
||||
this.initiateCleanupOnce(record, 'sni_mismatch');
|
||||
// Always check whether the new SNI would be allowed by the EXISTING domain config first
|
||||
// This ensures we're using the same ruleset that allowed the initial connection
|
||||
let allowed = false;
|
||||
|
||||
// First check if the exact original domain config would allow this new SNI
|
||||
if (record.domainConfig) {
|
||||
// Check if the new SNI matches any domain pattern in the original domain config
|
||||
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
||||
|
||||
if (allowed && this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Rehandshake with new SNI: ${newSNI} matched existing domain config ` +
|
||||
`patterns ${record.domainConfig.domains.join(', ')}. Allowing connection reuse.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If not allowed by the existing domain config, try to find another domain config
|
||||
if (!allowed) {
|
||||
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
||||
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
||||
);
|
||||
|
||||
// If we found a matching domain config, check IP rules
|
||||
if (newDomainConfig) {
|
||||
const effectiveAllowedIPs = [
|
||||
...newDomainConfig.allowedIPs,
|
||||
...(this.settings.defaultAllowedIPs || []),
|
||||
];
|
||||
const effectiveBlockedIPs = [
|
||||
...(newDomainConfig.blockedIPs || []),
|
||||
...(this.settings.defaultBlockedIPs || []),
|
||||
];
|
||||
|
||||
// Check if the IP is allowed for the new domain
|
||||
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
||||
|
||||
if (allowed && this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Rehandshake with new SNI: ${newSNI} (previously ${record.lockedDomain}). ` +
|
||||
`New domain is allowed by different domain config rules, permitting connection reuse.`
|
||||
);
|
||||
}
|
||||
|
||||
// Update the domain config reference to the new one
|
||||
if (allowed) {
|
||||
record.domainConfig = newDomainConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
// Update the locked domain to the new domain
|
||||
record.lockedDomain = newSNI;
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Updated locked domain for connection from ${record.remoteIP} to: ${newSNI}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If we get here, either no matching domain config was found or the IP is not allowed
|
||||
console.log(
|
||||
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. ` +
|
||||
`New domain not allowed by any rules. Terminating connection.`
|
||||
);
|
||||
this.initiateCleanupOnce(record, 'sni_mismatch');
|
||||
}
|
||||
} else if (newSNI && this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
||||
);
|
||||
}
|
||||
} 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 +970,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 +1146,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 +1216,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.`
|
||||
|
||||
Reference in New Issue
Block a user