|
|
|
@ -501,26 +501,17 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
this.cleanupConnection(record, 'client_closed');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update activity on data transfer
|
|
|
|
|
|
|
|
|
|
// Special handler for TLS handshake detection with NetworkProxy
|
|
|
|
|
socket.on('data', (chunk: Buffer) => {
|
|
|
|
|
this.updateActivity(record);
|
|
|
|
|
|
|
|
|
|
// Check for potential TLS renegotiation or reconnection packets
|
|
|
|
|
// Check for TLS handshake packets (ContentType.handshake)
|
|
|
|
|
if (chunk.length > 0 && chunk[0] === 22) {
|
|
|
|
|
// ContentType.handshake
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Detected potential TLS handshake data while connected to NetworkProxy`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Let the NetworkProxy handle the TLS renegotiation
|
|
|
|
|
// Just update the activity timestamp to prevent timeouts
|
|
|
|
|
record.lastActivity = Date.now();
|
|
|
|
|
console.log(`[${connectionId}] Detected potential TLS handshake with NetworkProxy, updating activity`);
|
|
|
|
|
this.updateActivity(record);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update activity on data transfer from the proxy socket
|
|
|
|
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
@ -776,6 +767,75 @@ export class PortProxy {
|
|
|
|
|
return this.initiateCleanupOnce(record, 'write_error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
socket.on('data', (renegChunk) => {
|
|
|
|
|
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
|
|
|
// Always update activity timestamp for any handshake packet
|
|
|
|
|
this.updateActivity(record);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
if (newSNI === undefined) {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the SNI has changed
|
|
|
|
|
if (newSNI !== serverName) {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
|
|
|
|
|
|
|
|
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
|
|
|
let allowed = false;
|
|
|
|
|
|
|
|
|
|
if (record.domainConfig) {
|
|
|
|
|
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
|
|
|
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (newDomainConfig) {
|
|
|
|
|
const effectiveAllowedIPs = [
|
|
|
|
|
...newDomainConfig.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs = [
|
|
|
|
|
...(newDomainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
|
|
|
|
|
|
|
|
if (allowed) {
|
|
|
|
|
record.domainConfig = newDomainConfig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allowed) {
|
|
|
|
|
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
|
|
|
record.lockedDomain = newSNI;
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
|
|
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now set up piping for future data and resume the socket
|
|
|
|
|
socket.pipe(targetSocket);
|
|
|
|
|
targetSocket.pipe(socket);
|
|
|
|
@ -809,7 +869,76 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// No pending data, so just set up piping
|
|
|
|
|
// 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
|
|
|
|
|
socket.on('data', (renegChunk) => {
|
|
|
|
|
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
|
|
|
// Always update activity timestamp for any handshake packet
|
|
|
|
|
this.updateActivity(record);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
if (newSNI === undefined) {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the SNI has changed
|
|
|
|
|
if (newSNI !== serverName) {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
|
|
|
|
|
|
|
|
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
|
|
|
let allowed = false;
|
|
|
|
|
|
|
|
|
|
if (record.domainConfig) {
|
|
|
|
|
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
|
|
|
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (newDomainConfig) {
|
|
|
|
|
const effectiveAllowedIPs = [
|
|
|
|
|
...newDomainConfig.allowedIPs,
|
|
|
|
|
...(this.settings.defaultAllowedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
const effectiveBlockedIPs = [
|
|
|
|
|
...(newDomainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || []),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
|
|
|
|
|
|
|
|
if (allowed) {
|
|
|
|
|
record.domainConfig = newDomainConfig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allowed) {
|
|
|
|
|
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
|
|
|
record.lockedDomain = newSNI;
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
|
|
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now set up piping
|
|
|
|
|
socket.pipe(targetSocket);
|
|
|
|
|
targetSocket.pipe(socket);
|
|
|
|
|
socket.resume(); // Resume the socket after piping is established
|
|
|
|
@ -846,31 +975,8 @@ export class PortProxy {
|
|
|
|
|
record.pendingData = [];
|
|
|
|
|
record.pendingDataSize = 0;
|
|
|
|
|
|
|
|
|
|
// Add the renegotiation listener for SNI validation
|
|
|
|
|
if (serverName) {
|
|
|
|
|
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);
|
|
|
|
|
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');
|
|
|
|
|
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// Renegotiation detection is now handled before piping is established
|
|
|
|
|
// This ensures the data listener receives all packets properly
|
|
|
|
|
|
|
|
|
|
// Set connection timeout with simpler logic
|
|
|
|
|
if (record.cleanupTimer) {
|
|
|
|
@ -886,13 +992,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 +1168,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 +1238,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.`
|
|
|
|
@ -1601,6 +1706,12 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
// Save domain config in connection record
|
|
|
|
|
connectionRecord.domainConfig = domainConfig;
|
|
|
|
|
|
|
|
|
|
// Always set the lockedDomain, even for non-SNI connections
|
|
|
|
|
if (serverName) {
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
console.log(`[${connectionId}] Locked connection to domain: ${serverName}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IP validation is skipped if allowedIPs is empty
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|