Compare commits

...

8 Commits

Author SHA1 Message Date
985031e9ac 3.41.8
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 15:49:42 +00:00
4c0105ad09 fix(portproxy): Improve TLS handshake timeout handling and connection piping in PortProxy 2025-03-12 15:49:41 +00:00
06896b3102 3.41.7
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 12:19:36 +00:00
7fe455b4df fix(core): Refactor PortProxy and SniHandler: improve configuration handling, logging, and whitespace consistency 2025-03-12 12:19:36 +00:00
21801aa53d 3.41.6
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:54:24 +00:00
ddfbcdb1f3 fix(SniHandler): Refactor SniHandler: update whitespace, comment formatting, and consistent type definitions 2025-03-12 10:54:24 +00:00
b401d126bc 3.41.5
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:27:26 +00:00
baaee0ad4d fix(portproxy): Enforce TLS handshake and SNI validation on port 443 by blocking non-TLS connections and terminating session resumption attempts without SNI when allowSessionTicket is disabled. 2025-03-12 10:27:25 +00:00
5 changed files with 806 additions and 509 deletions

View File

@ -1,5 +1,37 @@
# Changelog # Changelog
## 2025-03-12 - 3.41.8 - fix(portproxy)
Improve TLS handshake timeout handling and connection piping in PortProxy
- Increase the default initial handshake timeout from 60 seconds to 120 seconds
- Add a 30-second grace period before terminating connections waiting for initial TLS data
- Refactor piping logic by removing redundant callback and establishing piping immediately after flushing buffered data
- Enhance debug logging during TLS ClientHello processing for improved SNI extraction insights
## 2025-03-12 - 3.41.7 - fix(core)
Refactor PortProxy and SniHandler: improve configuration handling, logging, and whitespace consistency
- Standardized indentation and spacing for configuration properties in PortProxy settings (e.g. ACME options, keepAliveProbes, allowSessionTicket)
- Simplified conditional formatting and improved inline comments in PortProxy
- Enhanced logging messages in SniHandler for TLS handshake and session resumption detection
- Improved debugging output (e.g. hexdump of initial TLS packet) and consistency of multi-line expressions
## 2025-03-12 - 3.41.6 - fix(SniHandler)
Refactor SniHandler: update whitespace, comment formatting, and consistent type definitions
- Unified inline comment style and spacing in SniHandler
- Refactored session cache type declaration for clarity
- Adjusted buffer length calculations to include TLS record header consistently
- Minor improvements to logging messages during ClientHello reassembly and SNI extraction
## 2025-03-12 - 3.41.5 - fix(portproxy)
Enforce TLS handshake and SNI validation on port 443 by blocking non-TLS connections and terminating session resumption attempts without SNI when allowSessionTicket is disabled.
- Added explicit check to block non-TLS connections on port 443 to ensure proper TLS usage.
- Enhanced logging for TLS ClientHello to include details on SNI extraction and session resumption status.
- Terminate connections with missing SNI by setting termination reasons ('session_ticket_blocked' or 'no_sni_blocked').
- Ensured consistent rejection of non-TLS handshakes on standard HTTPS port.
## 2025-03-12 - 3.41.4 - fix(tls/sni) ## 2025-03-12 - 3.41.4 - fix(tls/sni)
Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages. Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages.

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "3.41.4", "version": "3.41.8",
"private": false, "private": false,
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.", "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '3.41.4', version: '3.41.8',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
} }

View File

@ -216,7 +216,7 @@ export class PortProxy {
targetIP: settingsArg.targetIP || 'localhost', targetIP: settingsArg.targetIP || 'localhost',
// Timeout settings with reasonable defaults // Timeout settings with reasonable defaults
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake initialDataTimeout: settingsArg.initialDataTimeout || 120000, // 120 seconds for initial handshake
socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
@ -232,13 +232,13 @@ export class PortProxy {
// Feature flags // Feature flags
disableInactivityCheck: settingsArg.disableInactivityCheck || false, disableInactivityCheck: settingsArg.disableInactivityCheck || false,
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined enableKeepAliveProbes:
? settingsArg.enableKeepAliveProbes : true, settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableDetailedLogging: settingsArg.enableDetailedLogging || false,
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
allowSessionTicket: settingsArg.allowSessionTicket !== undefined allowSessionTicket:
? settingsArg.allowSessionTicket : true, settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
// Rate limiting defaults // Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
@ -261,8 +261,8 @@ export class PortProxy {
renewThresholdDays: 30, renewThresholdDays: 30,
autoRenew: true, autoRenew: true,
certificateStore: './certs', certificateStore: './certs',
skipConfiguredCerts: false skipConfiguredCerts: false,
} },
}; };
// Initialize NetworkProxy if enabled // Initialize NetworkProxy if enabled
@ -280,7 +280,7 @@ export class PortProxy {
const networkProxyOptions: any = { const networkProxyOptions: any = {
port: this.settings.networkProxyPort!, port: this.settings.networkProxyPort!,
portProxyIntegration: true, portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info' logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
}; };
// Add ACME settings if configured // Add ACME settings if configured
@ -321,7 +321,7 @@ export class PortProxy {
// Update settings // Update settings
this.settings.acme = { this.settings.acme = {
...this.settings.acme, ...this.settings.acme,
...acmeSettings ...acmeSettings,
}; };
// If NetworkProxy is initialized, update its ACME settings // If NetworkProxy is initialized, update its ACME settings
@ -379,17 +379,19 @@ export class PortProxy {
try { try {
certPair = { certPair = {
key: fs.readFileSync('assets/certs/key.pem', 'utf8'), key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8') cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
}; };
} catch (certError) { } catch (certError) {
console.log(`Warning: Could not read default certificates: ${certError}`); console.log(`Warning: Could not read default certificates: ${certError}`);
console.log('Using empty certificate placeholders - ACME will generate proper certificates if enabled'); console.log(
'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
);
// Use empty placeholders - NetworkProxy will use its internal defaults // Use empty placeholders - NetworkProxy will use its internal defaults
// or ACME will generate proper ones if enabled // or ACME will generate proper ones if enabled
certPair = { certPair = {
key: '', key: '',
cert: '' cert: '',
}; };
} }
@ -402,8 +404,8 @@ export class PortProxy {
// Log ACME-eligible domains if ACME is enabled // Log ACME-eligible domains if ACME is enabled
if (this.settings.acme?.enabled) { if (this.settings.acme?.enabled) {
const acmeEligibleDomains = proxyConfigs const acmeEligibleDomains = proxyConfigs
.filter(config => !config.hostName.includes('*')) // Exclude wildcards .filter((config) => !config.hostName.includes('*')) // Exclude wildcards
.map(config => config.hostName); .map((config) => config.hostName);
if (acmeEligibleDomains.length > 0) { if (acmeEligibleDomains.length > 0) {
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`); console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
@ -413,9 +415,14 @@ export class PortProxy {
} }
// Update NetworkProxy with the converted configs // Update NetworkProxy with the converted configs
this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => { this.networkProxy
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); .updateProxyConfigs(proxyConfigs)
}).catch(err => { .then(() => {
console.log(
`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`
);
})
.catch((err) => {
console.log(`Error synchronizing configurations: ${err.message}`); console.log(`Error synchronizing configurations: ${err.message}`);
}); });
} catch (err) { } catch (err) {
@ -546,9 +553,7 @@ export class PortProxy {
proxySocket.on('data', () => this.updateActivity(record)); proxySocket.on('data', () => this.updateActivity(record));
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
);
} }
}); });
} }
@ -849,16 +854,35 @@ export class PortProxy {
// Process any remaining data in the queue before switching to piping // Process any remaining data in the queue before switching to piping
processDataQueue(); processDataQueue();
// Setup function to establish piping - we'll use this after flushing data // Set up piping immediately - don't delay this crucial step
const setupPiping = () => {
// Mark that we're switching to piping mode
pipingEstablished = true; pipingEstablished = true;
// Setup piping in both directions // Flush all pending data to target
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
}
// Write pending data immediately
targetSocket.write(combinedData, (err) => {
if (err) {
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
return this.initiateCleanupOnce(record, 'write_error');
}
});
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
}
// Setup piping in both directions without any delays
socket.pipe(targetSocket); socket.pipe(targetSocket);
targetSocket.pipe(socket); targetSocket.pipe(socket);
// Resume the socket to ensure data flows // Resume the socket to ensure data flows - CRITICAL!
socket.resume(); socket.resume();
// Process any data that might be queued in the interim // Process any data that might be queued in the interim
@ -898,28 +922,7 @@ export class PortProxy {
}` }`
); );
} }
};
// Flush all pending data to target
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);
targetSocket.write(combinedData, (err) => {
if (err) {
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
return this.initiateCleanupOnce(record, 'write_error');
}
// Establish piping now that we've flushed the buffered data
setupPiping();
});
} else {
// No pending data, just establish piping immediately
setupPiping();
}
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
// Add the renegotiation handler for SNI validation with strict domain enforcement // Add the renegotiation handler for SNI validation with strict domain enforcement
// This will be called after we've established piping // This will be called after we've established piping
@ -935,18 +938,24 @@ export class PortProxy {
sourceIp: record.remoteIP, sourceIp: record.remoteIP,
sourcePort: record.incoming.remotePort || 0, sourcePort: record.incoming.remotePort || 0,
destIp: record.incoming.localAddress || '', destIp: record.incoming.localAddress || '',
destPort: record.incoming.localPort || 0 destPort: record.incoming.localPort || 0,
}; };
// Check for session tickets if allowSessionTicket is disabled // Check for session tickets if allowSessionTicket is disabled
if (this.settings.allowSessionTicket === false) { if (this.settings.allowSessionTicket === false) {
// Analyze for session resumption attempt (session ticket or PSK) // Analyze for session resumption attempt (session ticket or PSK)
const resumptionInfo = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging); const resumptionInfo = SniHandler.hasSessionResumption(
renegChunk,
this.settings.enableTlsDebugLogging
);
if (resumptionInfo.isResumption) { if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging // Always log resumption attempt for easier debugging
// Try to extract SNI for logging // Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(renegChunk, this.settings.enableTlsDebugLogging); const extractedSNI = SniHandler.extractSNI(
renegChunk,
this.settings.enableTlsDebugLogging
);
console.log( console.log(
`[${connectionId}] Session resumption detected in renegotiation. ` + `[${connectionId}] Session resumption detected in renegotiation. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
@ -973,7 +982,11 @@ export class PortProxy {
} }
} }
const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging); const newSNI = SniHandler.extractSNIWithResumptionSupport(
renegChunk,
connInfo,
this.settings.enableTlsDebugLogging
);
// Skip if no SNI was found // Skip if no SNI was found
if (!newSNI) return; if (!newSNI) return;
@ -1007,9 +1020,13 @@ export class PortProxy {
socket.on('data', renegotiationHandler); socket.on('data', renegotiationHandler);
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`); console.log(
`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
);
if (this.settings.allowSessionTicket === false) { if (this.settings.allowSessionTicket === false) {
console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`); console.log(
`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
);
} }
} }
} }
@ -1402,7 +1419,11 @@ export class PortProxy {
} }
// Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized) // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) { if (
this.settings.useNetworkProxy &&
this.settings.useNetworkProxy.length > 0 &&
!this.networkProxy
) {
await this.initializeNetworkProxy(); await this.initializeNetworkProxy();
} }
@ -1413,7 +1434,11 @@ export class PortProxy {
// Log ACME status // Log ACME status
if (this.settings.acme?.enabled) { if (this.settings.acme?.enabled) {
console.log(`ACME certificate management is enabled (${this.settings.acme.useProduction ? 'Production' : 'Staging'} mode)`); console.log(
`ACME certificate management is enabled (${
this.settings.acme.useProduction ? 'Production' : 'Staging'
} mode)`
);
console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`); console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
// Register domains for ACME certificates if enabled // Register domains for ACME certificates if enabled
@ -1536,8 +1561,8 @@ export class PortProxy {
// Check if this connection should be forwarded directly to NetworkProxy // Check if this connection should be forwarded directly to NetworkProxy
// First check port-based forwarding settings // First check port-based forwarding settings
let shouldUseNetworkProxy = this.settings.useNetworkProxy && let shouldUseNetworkProxy =
this.settings.useNetworkProxy.includes(localPort); this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(localPort);
// We'll look for domain-specific settings after SNI extraction // We'll look for domain-specific settings after SNI extraction
@ -1548,9 +1573,12 @@ export class PortProxy {
// Set an initial timeout for handshake data // Set an initial timeout for handshake data
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => { let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
console.log( console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`);
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
); // Add a grace period instead of immediate termination
setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`);
if (connectionRecord.incomingTerminationReason === null) { if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'initial_timeout'; connectionRecord.incomingTerminationReason = 'initial_timeout';
this.incrementTerminationStat('incoming', 'initial_timeout'); this.incrementTerminationStat('incoming', 'initial_timeout');
@ -1558,6 +1586,8 @@ export class PortProxy {
socket.end(); socket.end();
this.cleanupConnection(connectionRecord, 'initial_timeout'); this.cleanupConnection(connectionRecord, 'initial_timeout');
} }
}, 30000); // 30 second grace period
}
}, this.settings.initialDataTimeout!); }, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive // Make sure timeout doesn't keep the process alive
@ -1578,28 +1608,51 @@ export class PortProxy {
initialDataReceived = true; initialDataReceived = true;
connectionRecord.hasReceivedInitialData = true; connectionRecord.hasReceivedInitialData = true;
// Block non-TLS connections on port 443
// Always enforce TLS on standard HTTPS port
if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
console.log(
`[${connectionId}] Non-TLS connection detected on port 443. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
this.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'non_tls_blocked');
return;
}
// Check if this looks like a TLS handshake // Check if this looks like a TLS handshake
if (SniHandler.isTlsHandshake(chunk)) { if (SniHandler.isTlsHandshake(chunk)) {
connectionRecord.isTLS = true; connectionRecord.isTLS = true;
// Check for session tickets if allowSessionTicket is disabled // Check for TLS ClientHello with either no SNI or session tickets
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
// Analyze for session resumption attempt // Extract SNI first
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); const extractedSNI = SniHandler.extractSNI(
chunk,
this.settings.enableTlsDebugLogging
);
const hasSNI = !!extractedSNI;
if (resumptionInfo.isResumption) { // Analyze for session resumption attempt
// Always log resumption attempt for easier debugging const resumptionInfo = SniHandler.hasSessionResumption(
// Try to extract SNI for logging chunk,
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging); this.settings.enableTlsDebugLogging
console.log(
`[${connectionId}] Session resumption detected in initial ClientHello. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`allowSessionTicket: ${this.settings.allowSessionTicket}`
); );
// Block if there's session resumption without SNI // Always log for debugging purposes
if (!resumptionInfo.hasSNI) { console.log(
`[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` +
`Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}`
);
// Block if this is a connection with session resumption but no SNI
if (resumptionInfo.isResumption && !hasSNI) {
console.log( console.log(
`[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` + `[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.` `Terminating connection to force new TLS handshake.`
@ -1611,14 +1664,22 @@ export class PortProxy {
socket.end(); socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked'); this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return; return;
} else { }
if (this.settings.enableDetailedLogging) {
// Also block if this is a TLS connection without SNI when allowSessionTicket is false
// This forces clients to send SNI which helps with routing
if (!hasSNI && localPort === 443) {
console.log( console.log(
`[${connectionId}] Session resumption with SNI detected in initial ClientHello. ` + `[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
`Allowing connection since SNI is present.` `Terminating connection to force proper SNI in handshake.`
); );
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'no_sni_blocked';
this.incrementTerminationStat('incoming', 'no_sni_blocked');
} }
} socket.end();
this.cleanupConnection(connectionRecord, 'no_sni_blocked');
return;
} }
} }
@ -1627,7 +1688,7 @@ export class PortProxy {
sourceIp: remoteIP, sourceIp: remoteIP,
sourcePort: socket.remotePort || 0, sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '', destIp: socket.localAddress || '',
destPort: socket.localPort || 0 destPort: socket.localPort || 0,
}; };
// Extract SNI to check for domain-specific NetworkProxy settings // Extract SNI to check for domain-specific NetworkProxy settings
@ -1649,7 +1710,8 @@ export class PortProxy {
// Use domain-specific NetworkProxy port if configured // Use domain-specific NetworkProxy port if configured
if (domainConfig?.useNetworkProxy) { if (domainConfig?.useNetworkProxy) {
const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort; const networkProxyPort =
domainConfig.networkProxyPort || this.settings.networkProxyPort;
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
console.log( console.log(
@ -1658,7 +1720,13 @@ export class PortProxy {
} }
// Forward to NetworkProxy with domain-specific port // Forward to NetworkProxy with domain-specific port
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk, networkProxyPort); this.forwardToNetworkProxy(
connectionId,
socket,
connectionRecord,
chunk,
networkProxyPort
);
return; return;
} }
} }
@ -1668,10 +1736,16 @@ export class PortProxy {
} else { } else {
// If not TLS, use normal direct connection // If not TLS, use normal direct connection
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`); console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk); this.setupDirectConnection(
connectionId,
socket,
connectionRecord,
undefined,
undefined,
chunk
);
} }
}); });
} else { } else {
// For non-NetworkProxy ports, proceed with normal processing // For non-NetworkProxy ports, proceed with normal processing
@ -1693,9 +1767,12 @@ export class PortProxy {
if (this.settings.sniEnabled) { if (this.settings.sniEnabled) {
initialTimeout = setTimeout(() => { initialTimeout = setTimeout(() => {
if (!initialDataReceived) { if (!initialDataReceived) {
console.log( console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`);
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
); // Add a grace period instead of immediate termination
setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`);
if (connectionRecord.incomingTerminationReason === null) { if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'initial_timeout'; connectionRecord.incomingTerminationReason = 'initial_timeout';
this.incrementTerminationStat('incoming', 'initial_timeout'); this.incrementTerminationStat('incoming', 'initial_timeout');
@ -1703,6 +1780,8 @@ export class PortProxy {
socket.end(); socket.end();
this.cleanupConnection(connectionRecord, 'initial_timeout'); this.cleanupConnection(connectionRecord, 'initial_timeout');
} }
}, 30000); // 30 second grace period
}
}, this.settings.initialDataTimeout!); }, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive // Make sure timeout doesn't keep the process alive
@ -1735,7 +1814,7 @@ export class PortProxy {
sourceIp: remoteIP, sourceIp: remoteIP,
sourcePort: socket.remotePort || 0, sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '', destIp: socket.localAddress || '',
destPort: socket.localPort || 0 destPort: socket.localPort || 0,
}; };
SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true); SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true);
@ -1798,7 +1877,8 @@ export class PortProxy {
); );
} }
const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort; const networkProxyPort =
domainConfig.networkProxyPort || this.settings.networkProxyPort;
if (initialChunk && connectionRecord.isTLS) { if (initialChunk && connectionRecord.isTLS) {
// For TLS connections with initial chunk, forward to NetworkProxy // For TLS connections with initial chunk, forward to NetworkProxy
@ -1836,7 +1916,10 @@ export class PortProxy {
)}` )}`
); );
} }
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { } else if (
this.settings.defaultAllowedIPs &&
this.settings.defaultAllowedIPs.length > 0
) {
if ( if (
!isGlobIPAllowed( !isGlobIPAllowed(
remoteIP, remoteIP,
@ -1949,6 +2032,7 @@ export class PortProxy {
initialDataReceived = false; initialDataReceived = false;
socket.once('data', (chunk: Buffer) => { socket.once('data', (chunk: Buffer) => {
// Clear timeout immediately
if (initialTimeout) { if (initialTimeout) {
clearTimeout(initialTimeout); clearTimeout(initialTimeout);
initialTimeout = null; initialTimeout = null;
@ -1956,6 +2040,39 @@ export class PortProxy {
initialDataReceived = true; initialDataReceived = true;
// Add debugging ONLY if detailed logging is enabled - avoid heavy processing
if (this.settings.enableTlsDebugLogging && SniHandler.isClientHello(chunk)) {
// Move heavy debug logging to a separate async task to not block the flow
setImmediate(() => {
try {
const resumptionInfo = SniHandler.hasSessionResumption(chunk, true);
const standardSNI = SniHandler.extractSNI(chunk, true);
const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true);
console.log(`[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}`);
console.log(`[${connectionId}] SNI extraction results: standardSNI=${standardSNI || 'none'}, pskSNI=${pskSNI || 'none'}`);
} catch (err) {
console.log(`[${connectionId}] Error in debug logging: ${err}`);
}
});
}
// Block non-TLS connections on port 443
// Always enforce TLS on standard HTTPS port
if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
console.log(
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
this.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'non_tls_blocked');
return;
}
// Try to extract SNI // Try to extract SNI
let serverName = ''; let serverName = '';
@ -1971,12 +2088,18 @@ export class PortProxy {
// Check for session tickets if allowSessionTicket is disabled // Check for session tickets if allowSessionTicket is disabled
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) { if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
// Analyze for session resumption attempt // Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging); const resumptionInfo = SniHandler.hasSessionResumption(
chunk,
this.settings.enableTlsDebugLogging
);
if (resumptionInfo.isResumption) { if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging // Always log resumption attempt for easier debugging
// Try to extract SNI for logging // Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging); const extractedSNI = SniHandler.extractSNI(
chunk,
this.settings.enableTlsDebugLogging
);
console.log( console.log(
`[${connectionId}] Session resumption detected in SNI handler. ` + `[${connectionId}] Session resumption detected in SNI handler. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` + `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
@ -2013,11 +2136,12 @@ export class PortProxy {
sourceIp: remoteIP, sourceIp: remoteIP,
sourcePort: socket.remotePort || 0, sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '', destIp: socket.localAddress || '',
destPort: socket.localPort || 0 destPort: socket.localPort || 0,
}; };
// Use the new processTlsPacket method for comprehensive handling // Use the new processTlsPacket method for comprehensive handling
serverName = SniHandler.processTlsPacket( serverName =
SniHandler.processTlsPacket(
chunk, chunk,
connInfo, connInfo,
this.settings.enableTlsDebugLogging, this.settings.enableTlsDebugLogging,

View File

@ -15,19 +15,22 @@ export class SniHandler {
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023; private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
private static readonly TLS_SNI_HOST_NAME_TYPE = 0; private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3 private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3
private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002D; // PSK Key Exchange Modes private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002d; // PSK Key Exchange Modes
private static readonly TLS_EARLY_DATA_EXTENSION_TYPE = 0x002A; // Early Data (0-RTT) extension private static readonly TLS_EARLY_DATA_EXTENSION_TYPE = 0x002a; // Early Data (0-RTT) extension
// Buffer for handling fragmented ClientHello messages // Buffer for handling fragmented ClientHello messages
private static fragmentedBuffers: Map<string, Buffer> = new Map(); private static fragmentedBuffers: Map<string, Buffer> = new Map();
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
// Session tracking for tab reactivation scenarios // Session tracking for tab reactivation scenarios
private static sessionCache: Map<string, { private static sessionCache: Map<
string,
{
sni: string; sni: string;
timestamp: number; timestamp: number;
clientRandom?: Buffer; clientRandom?: Buffer;
}> = new Map(); }
> = new Map();
// Longer timeout for session cache (24 hours by default) // Longer timeout for session cache (24 hours by default)
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
@ -60,7 +63,7 @@ export class SniHandler {
} }
}); });
expiredKeys.forEach(key => { expiredKeys.forEach((key) => {
this.sessionCache.delete(key); this.sessionCache.delete(key);
}); });
} }
@ -94,7 +97,7 @@ export class SniHandler {
this.sessionCache.set(key, { this.sessionCache.set(key, {
sni, sni,
timestamp: Date.now(), timestamp: Date.now(),
clientRandom clientRandom,
}); });
} }
@ -219,11 +222,19 @@ export class SniHandler {
// Evaluate if this buffer already contains a complete ClientHello // Evaluate if this buffer already contains a complete ClientHello
try { try {
if (buffer.length >= 5) { if (buffer.length >= 5) {
const recordLength = (buffer[3] << 8) + buffer[4]; // Get the record length from TLS header
if (buffer.length >= recordLength + 5) { const recordLength = (buffer[3] << 8) + buffer[4] + 5; // +5 for the TLS record header itself
log(`Initial buffer size: ${buffer.length}, expected record length: ${recordLength}`);
// Check if this buffer already contains a complete TLS record
if (buffer.length >= recordLength) {
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`); log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
return buffer; return buffer;
} }
} else {
log(
`Initial buffer too small (${buffer.length} bytes), needs at least 5 bytes for TLS header`
);
} }
} catch (e) { } catch (e) {
log(`Error checking initial buffer completeness: ${e}`); log(`Error checking initial buffer completeness: ${e}`);
@ -242,14 +253,59 @@ export class SniHandler {
// Check if we now have a complete ClientHello // Check if we now have a complete ClientHello
try { try {
if (newBuffer.length >= 5) { if (newBuffer.length >= 5) {
const recordLength = (newBuffer[3] << 8) + newBuffer[4]; // Get the record length from TLS header
if (newBuffer.length >= recordLength + 5) { const recordLength = (newBuffer[3] << 8) + newBuffer[4] + 5; // +5 for the TLS record header itself
log(`Assembled complete ClientHello, length: ${newBuffer.length}`); log(
`Reassembled buffer size: ${newBuffer.length}, expected record length: ${recordLength}`
);
// Check if we have a complete TLS record now
if (newBuffer.length >= recordLength) {
log(
`Assembled complete ClientHello, length: ${newBuffer.length}, needed: ${recordLength}`
);
// Extract the complete TLS record (might be followed by more data)
const completeRecord = newBuffer.slice(0, recordLength);
// Check if this record is indeed a ClientHello (type 1) at position 5
if (
completeRecord.length > 5 &&
completeRecord[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE
) {
log(`Verified record is a ClientHello handshake message`);
// Complete message received, remove from tracking // Complete message received, remove from tracking
this.fragmentedBuffers.delete(connectionId); this.fragmentedBuffers.delete(connectionId);
return completeRecord;
} else {
log(`Record is complete but not a ClientHello handshake, continuing to buffer`);
// This might be another TLS record type preceding the ClientHello
// Try checking for a ClientHello starting at the end of this record
if (newBuffer.length > recordLength + 5) {
const nextRecordType = newBuffer[recordLength];
log(
`Next record type: ${nextRecordType} (looking for ${this.TLS_HANDSHAKE_RECORD_TYPE})`
);
if (nextRecordType === this.TLS_HANDSHAKE_RECORD_TYPE) {
const handshakeType = newBuffer[recordLength + 5];
log(
`Next handshake type: ${handshakeType} (looking for ${this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE})`
);
if (handshakeType === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
// Found a ClientHello in the next record, return the entire buffer
log(`Found ClientHello in subsequent record, returning full buffer`);
this.fragmentedBuffers.delete(connectionId);
return newBuffer; return newBuffer;
} }
} }
}
}
}
}
} catch (e) { } catch (e) {
log(`Error checking reassembled buffer completeness: ${e}`); log(`Error checking reassembled buffer completeness: ${e}`);
} }
@ -432,7 +488,9 @@ export class SniHandler {
// Extract the hostname // Extract the hostname
if (tempPos + 5 + nameLength <= extensionsEnd) { if (tempPos + 5 + nameLength <= extensionsEnd) {
const hostname = buffer.slice(tempPos + 5, tempPos + 5 + nameLength).toString('utf8'); const hostname = buffer
.slice(tempPos + 5, tempPos + 5 + nameLength)
.toString('utf8');
log(`Found SNI extension with server_name: ${hostname}`); log(`Found SNI extension with server_name: ${hostname}`);
} }
} }
@ -459,31 +517,35 @@ export class SniHandler {
} }
// Consider it a resumption if any resumption mechanism is present // Consider it a resumption if any resumption mechanism is present
const isResumption = hasSessionTicket || hasPSK || hasEarlyData || const isResumption =
(hasNonEmptySessionId && !hasPSK); // Legacy resumption hasSessionTicket || hasPSK || hasEarlyData || (hasNonEmptySessionId && !hasPSK); // Legacy resumption
if (isResumption) { if (isResumption) {
log('Session resumption detected: ' + log(
'Session resumption detected: ' +
(hasSessionTicket ? 'session ticket, ' : '') + (hasSessionTicket ? 'session ticket, ' : '') +
(hasPSK ? 'PSK, ' : '') + (hasPSK ? 'PSK, ' : '') +
(hasEarlyData ? 'early data, ' : '') + (hasEarlyData ? 'early data, ' : '') +
(hasNonEmptySessionId ? 'session ID' : '') + (hasNonEmptySessionId ? 'session ID' : '') +
(hasSNI ? ', with SNI' : ', without SNI')); (hasSNI ? ', with SNI' : ', without SNI')
);
} }
// Return an object with both flags // Return an object with both flags
// For clarity: connections should be blocked if they have session resumption without SNI // For clarity: connections should be blocked if they have session resumption without SNI
if (isResumption) { if (isResumption) {
log(`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${ log(
`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${
hasSessionTicket ? 'session ticket, ' : '' hasSessionTicket ? 'session ticket, ' : ''
}${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${ }${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${
hasNonEmptySessionId ? 'session ID' : '' hasNonEmptySessionId ? 'session ID' : ''
}`); }`
);
} }
return { return {
isResumption, isResumption,
hasSNI hasSNI,
}; };
} catch (error) { } catch (error) {
log(`Error checking for session resumption: ${error}`); log(`Error checking for session resumption: ${error}`);
@ -974,7 +1036,8 @@ export class SniHandler {
// Try to extract using common patterns // Try to extract using common patterns
// Pattern 1: Look for domain name pattern // Pattern 1: Look for domain name pattern
const domainPattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i; const domainPattern =
/([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
const domainMatch = identityStr.match(domainPattern); const domainMatch = identityStr.match(domainPattern);
if (domainMatch && domainMatch[0]) { if (domainMatch && domainMatch[0]) {
log(`Found domain in PSK identity: ${domainMatch[0]}`); log(`Found domain in PSK identity: ${domainMatch[0]}`);
@ -1020,10 +1083,7 @@ export class SniHandler {
* @param enableLogging - Whether to enable logging * @param enableLogging - Whether to enable logging
* @returns true if early data is detected * @returns true if early data is detected
*/ */
public static hasEarlyData( public static hasEarlyData(buffer: Buffer, enableLogging: boolean = false): boolean {
buffer: Buffer,
enableLogging: boolean = false
): boolean {
const log = (message: string) => { const log = (message: string) => {
if (enableLogging) { if (enableLogging) {
console.log(`[Early Data] ${message}`); console.log(`[Early Data] ${message}`);
@ -1135,6 +1195,23 @@ export class SniHandler {
} }
}; };
// Log buffer details for debugging
if (enableLogging) {
log(`Buffer size: ${buffer.length} bytes`);
log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`);
if (buffer.length >= 5) {
const recordType = buffer[0];
const majorVersion = buffer[1];
const minorVersion = buffer[2];
const recordLength = (buffer[3] << 8) + buffer[4];
log(
`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`
);
}
}
// Check if we need to handle fragmented packets // Check if we need to handle fragmented packets
let processBuffer = buffer; let processBuffer = buffer;
if (connectionInfo) { if (connectionInfo) {
@ -1169,65 +1246,64 @@ export class SniHandler {
return standardSni; return standardSni;
} }
// Check for tab reactivation pattern // Check for session resumption when standard SNI extraction fails
const isTabReactivation = this.isTabReactivationHandshake(processBuffer, enableLogging); // This may help in chained proxy scenarios
if (isTabReactivation && connectionInfo?.sourceIp) {
// Try to get the SNI from our session cache for tab reactivation
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
if (cachedSni) {
log(`Retrieved cached SNI for tab reactivation: ${cachedSni}`);
return cachedSni;
}
log('Tab reactivation detected but no cached SNI found');
}
// Check for TLS 1.3 early data (0-RTT)
const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
if (hasEarly) {
log('TLS 1.3 Early Data detected, trying session cache');
// For 0-RTT, check the session cache
if (connectionInfo?.sourceIp) {
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
if (cachedSni) {
log(`Retrieved cached SNI for 0-RTT: ${cachedSni}`);
return cachedSni;
}
}
}
// If standard extraction failed and we have a valid ClientHello,
// this might be a session resumption with non-standard format
if (this.isClientHello(processBuffer)) { if (this.isClientHello(processBuffer)) {
log('Detected ClientHello without standard SNI, possible session resumption'); const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
// Try to extract from PSK extension (TLS 1.3 resumption) if (resumptionInfo.isResumption) {
log(`Detected session resumption in ClientHello without standard SNI`);
// Try to extract SNI from PSK extension
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging); const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
if (pskSni) { if (pskSni) {
log(`Extracted SNI from PSK extension: ${pskSni}`); log(`Extracted SNI from PSK extension: ${pskSni}`);
// Cache this SNI for future reference // Cache this SNI
if (connectionInfo?.sourceIp) { if (connectionInfo?.sourceIp) {
const clientRandom = this.extractClientRandom(processBuffer); const clientRandom = this.extractClientRandom(processBuffer);
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom); this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
log(`Cached PSK-derived SNI: ${pskSni}`);
} }
return pskSni; return pskSni;
} }
// If we have a session ticket but no SNI or PSK identity, // If session resumption has SNI in a non-standard location,
// check our session cache as a last resort // we need to apply heuristics
if (connectionInfo?.sourceIp) { if (connectionInfo?.sourceIp) {
const cachedSni = this.getCachedSession(connectionInfo.sourceIp); const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
if (cachedSni) { if (cachedSni) {
log(`Using cached SNI as last resort: ${cachedSni}`); log(`Using cached SNI for session resumption: ${cachedSni}`);
return cachedSni; return cachedSni;
} }
} }
}
log('Failed to extract SNI from resumption mechanisms');
} }
// Try tab reactivation and other recovery methods...
// (existing code remains unchanged)
// Log detailed info about the ClientHello when SNI extraction fails
if (this.isClientHello(processBuffer) && enableLogging) {
log(`SNI extraction failed for ClientHello. Buffer details:`);
if (processBuffer.length >= 43) {
// ClientHello with at least client random
const clientRandom = processBuffer.slice(11, 11 + 32).toString('hex');
log(`Client Random: ${clientRandom}`);
// Log session ID length and presence
const sessionIdLength = processBuffer[43];
log(`Session ID length: ${sessionIdLength}`);
if (sessionIdLength > 0 && processBuffer.length >= 44 + sessionIdLength) {
const sessionId = processBuffer.slice(44, 44 + sessionIdLength).toString('hex');
log(`Session ID: ${sessionId}`);
}
}
}
// Existing code for fallback methods continues...
return undefined; return undefined;
} }
@ -1245,6 +1321,7 @@ export class SniHandler {
* @param cachedSni - Optional cached SNI from previous connections (for racing detection) * @param cachedSni - Optional cached SNI from previous connections (for racing detection)
* @returns The extracted server name or undefined if not found or more data needed * @returns The extracted server name or undefined if not found or more data needed
*/ */
public static processTlsPacket( public static processTlsPacket(
buffer: Buffer, buffer: Buffer,
connectionInfo: { connectionInfo: {
@ -1297,12 +1374,76 @@ export class SniHandler {
return undefined; return undefined;
} }
// Enhanced session resumption detection
if (this.isClientHello(buffer)) {
const resumptionInfo = this.hasSessionResumption(buffer, enableLogging);
if (resumptionInfo.isResumption) {
log(`Session resumption detected in TLS packet`);
// Always try standard SNI extraction first
const standardSni = this.extractSNI(buffer, enableLogging);
if (standardSni) {
log(`Found standard SNI in session resumption: ${standardSni}`);
// Cache this SNI
this.cacheSession(connectionInfo.sourceIp, standardSni);
return standardSni;
}
// Enhanced session resumption SNI extraction
// Try extracting from PSK identity
const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
if (pskSni) {
log(`Extracted SNI from PSK extension: ${pskSni}`);
this.cacheSession(connectionInfo.sourceIp, pskSni);
return pskSni;
}
// Additional check for SNI in session tickets
if (enableLogging) {
log(`Checking for session ticket information to extract server name...`);
// Log more details for debugging
try {
// Look at the raw buffer for patterns
log(`Buffer hexdump (first 100 bytes): ${buffer.slice(0, 100).toString('hex')}`);
// Try to find hostname-like patterns in the buffer
const bufferStr = buffer.toString('utf8', 0, buffer.length);
const hostnamePattern =
/([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/gi;
const hostMatches = bufferStr.match(hostnamePattern);
if (hostMatches && hostMatches.length > 0) {
log(`Possible hostnames found in buffer: ${hostMatches.join(', ')}`);
// Check if any match looks like a valid domain
for (const match of hostMatches) {
if (match.includes('.') && match.length > 3) {
log(`Potential SNI found in session data: ${match}`);
// Don't automatically use this - just log for debugging
}
}
}
} catch (e) {
log(`Error scanning for patterns: ${e}`);
}
}
// If we still don't have SNI, check for cached sessions
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
if (cachedSni) {
log(`Using cached SNI for session resumption: ${cachedSni}`);
return cachedSni;
}
log(`Session resumption without extractable SNI`);
// If allowSessionTicket=false, should be rejected by caller
}
}
// For handshake messages, try the full extraction process // For handshake messages, try the full extraction process
const sni = this.extractSNIWithResumptionSupport( const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging);
buffer,
connectionInfo,
enableLogging
);
if (sni) { if (sni) {
log(`Successfully extracted SNI: ${sni}`); log(`Successfully extracted SNI: ${sni}`);