Compare commits

...

20 Commits

Author SHA1 Message Date
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
fe7c4c2f5e 3.41.4
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-12 10:01:54 +00:00
ab1ec84832 fix(tls/sni): Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages. 2025-03-12 10:01:54 +00:00
156abbf5b4 3.41.3
Some checks failed
Default (tags) / security (push) Failing after 10m42s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-12 09:56:21 +00:00
1a90566622 fix(TLS/SNI): Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation. 2025-03-12 09:56:21 +00:00
b48b90d613 3.41.2
Some checks failed
Default (tags) / security (push) Successful in 28s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:41:04 +00:00
124f8d48b7 fix(SniHandler): Refactor hasSessionResumption to return detailed session resumption info 2025-03-11 19:41:04 +00:00
b2a57ada5d 3.41.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:38:41 +00:00
62a3e1f4b7 fix(SniHandler): Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection. 2025-03-11 19:38:41 +00:00
3a1485213a 3.41.0
Some checks failed
Default (tags) / security (push) Failing after 10m42s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 19:31:20 +00:00
9dbf6fdeb5 feat(PortProxy/TLS): Add allowSessionTicket option to control TLS session ticket handling 2025-03-11 19:31:20 +00:00
9496dd5336 3.40.0
Some checks failed
Default (tags) / security (push) Failing after 11m44s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 18:05:20 +00:00
29d28fba93 feat(SniHandler): Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes 2025-03-11 18:05:20 +00:00
8196de4fa3 3.39.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:50:57 +00:00
6fddafe9fd feat(PortProxy): Add domain-specific NetworkProxy integration support to PortProxy 2025-03-11 17:50:56 +00:00
1e89062167 3.38.2
Some checks failed
Default (tags) / security (push) Successful in 22s
Default (tags) / test (push) Failing after 1m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:38:32 +00:00
21a24fd95b fix(core): No code changes detected; bumping patch version for consistency. 2025-03-11 17:38:32 +00:00
5 changed files with 1031 additions and 161 deletions

View File

@ -1,5 +1,75 @@
# Changelog # Changelog
## 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)
Improve logging for TLS session resumption by extracting and logging SNI values from ClientHello messages.
- Added logging to output the extracted SNI value during renegotiation, initial ClientHello and in the SNI handler.
- Enhanced error handling during SNI extraction to aid troubleshooting of TLS session resumption issues.
## 2025-03-12 - 3.41.3 - fix(TLS/SNI)
Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation.
- Always log session resumption in both renegotiation and initial ClientHello processing.
- Terminate connections that attempt session resumption without SNI when allowSessionTicket is false.
- Treat empty SNI extensions as absence of SNI to improve consistency in TLS handshake processing.
## 2025-03-11 - 3.41.2 - fix(SniHandler)
Refactor hasSessionResumption to return detailed session resumption info
- Changed the return type of hasSessionResumption from boolean to an object with properties isResumption and hasSNI
- Updated early return conditions to return { isResumption: false, hasSNI: false } when buffer is too short or invalid
- Modified corresponding documentation to reflect the new return type
## 2025-03-11 - 3.41.1 - fix(SniHandler)
Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection.
- Changed SniHandler.hasSessionResumption to return an object with 'isResumption' and 'hasSNI' flags.
- Adjusted PortProxy logic to only terminate connections when a session ticket is detected without an accompanying SNI (when allowSessionTicket is false).
- Enhanced debug logging to clearly differentiate between session resumption scenarios with and without SNI.
## 2025-03-11 - 3.41.0 - feat(PortProxy/TLS)
Add allowSessionTicket option to control TLS session ticket handling
- Introduce 'allowSessionTicket' flag (default true) in PortProxy settings to enable or disable TLS session resumption via session tickets.
- Update SniHandler with a new hasSessionResumption method to detect session ticket and PSK extensions in ClientHello messages.
- Force connection cleanup during renegotiation and initial handshake when allowSessionTicket is set to false and a session ticket is detected.
## 2025-03-11 - 3.40.0 - feat(SniHandler)
Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes
- Introduce a session cache mechanism to store and retrieve cached SNI values based on client IP (and optionally client random) to better handle tab reactivation scenarios.
- Implement functions to initialize, update, and clean up the session cache for TLS ClientHello messages.
- Enhance SNI extraction logic to check for tab reactivation handshakes and to return cached SNI for resumed connections or 0-RTT scenarios.
- Update PSK extension handling to safely skip over obfuscated ticket age bytes.
## 2025-03-11 - 3.39.0 - feat(PortProxy)
Add domain-specific NetworkProxy integration support to PortProxy
- Introduced new properties 'useNetworkProxy' and 'networkProxyPort' in domain configurations.
- Updated forwardToNetworkProxy to accept an optional custom proxy port parameter.
- Enhanced TLS handshake processing to extract SNI and, if a matching domain config specifies NetworkProxy usage, forward the connection using the domain-specific port.
- Refined connection routing logic to check for domain-specific NetworkProxy settings before falling back to default behavior.
## 2025-03-11 - 3.38.2 - fix(core)
No code changes detected; bumping patch version for consistency.
## 2025-03-11 - 3.38.1 - fix(PortProxy) ## 2025-03-11 - 3.38.1 - fix(PortProxy)
Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging. Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging.

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "3.38.1", "version": "3.41.6",
"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.38.1', version: '3.41.6',
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

@ -11,6 +11,10 @@ export interface IDomainConfig {
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
// Allow domain-specific timeout override // Allow domain-specific timeout override
connectionTimeout?: number; // Connection timeout override (ms) connectionTimeout?: number; // Connection timeout override (ms)
// NetworkProxy integration options for this specific domain
useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain
networkProxyPort?: number; // Override default NetworkProxy port for this domain
} }
/** Port proxy settings including global allowed port ranges */ /** Port proxy settings including global allowed port ranges */
@ -47,6 +51,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
enableDetailedLogging?: boolean; // Enable detailed connection logging enableDetailedLogging?: boolean; // Enable detailed connection logging
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
// Rate limiting and security // Rate limiting and security
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
@ -232,6 +237,8 @@ export class PortProxy {
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
? settingsArg.allowSessionTicket : true,
// Rate limiting defaults // Rate limiting defaults
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
@ -452,12 +459,14 @@ export class PortProxy {
* @param socket - The incoming client socket * @param socket - The incoming client socket
* @param record - The connection record * @param record - The connection record
* @param initialData - Initial data chunk (TLS ClientHello) * @param initialData - Initial data chunk (TLS ClientHello)
* @param customProxyPort - Optional custom port for NetworkProxy (for domain-specific settings)
*/ */
private forwardToNetworkProxy( private forwardToNetworkProxy(
connectionId: string, connectionId: string,
socket: plugins.net.Socket, socket: plugins.net.Socket,
record: IConnectionRecord, record: IConnectionRecord,
initialData: Buffer initialData: Buffer,
customProxyPort?: number
): void { ): void {
// Ensure NetworkProxy is initialized // Ensure NetworkProxy is initialized
if (!this.networkProxy) { if (!this.networkProxy) {
@ -475,7 +484,8 @@ export class PortProxy {
); );
} }
const proxyPort = this.networkProxy.getListeningPort(); // Use the custom port if provided, otherwise use the default NetworkProxy port
const proxyPort = customProxyPort || this.networkProxy.getListeningPort();
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
if (this.settings.enableDetailedLogging) { if (this.settings.enableDetailedLogging) {
@ -928,6 +938,41 @@ export class PortProxy {
destPort: record.incoming.localPort || 0 destPort: record.incoming.localPort || 0
}; };
// Check for session tickets if allowSessionTicket is disabled
if (this.settings.allowSessionTicket === false) {
// Analyze for session resumption attempt (session ticket or PSK)
const resumptionInfo = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging);
if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging
// Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
console.log(
`[${connectionId}] Session resumption detected in renegotiation. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`allowSessionTicket: ${this.settings.allowSessionTicket}`
);
// Block if there's session resumption without SNI
if (!resumptionInfo.hasSNI) {
console.log(
`[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
this.initiateCleanupOnce(record, 'session_ticket_blocked');
return;
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session resumption with SNI detected in renegotiation. ` +
`Allowing connection since SNI is present.`
);
}
}
}
}
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
@ -963,6 +1008,9 @@ export class PortProxy {
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) {
console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`);
}
} }
} }
@ -1486,10 +1534,13 @@ export class PortProxy {
); );
} }
// Check if this connection should be forwarded directly to NetworkProxy based on port // Check if this connection should be forwarded directly to NetworkProxy
const shouldUseNetworkProxy = this.settings.useNetworkProxy && // First check port-based forwarding settings
let shouldUseNetworkProxy = this.settings.useNetworkProxy &&
this.settings.useNetworkProxy.includes(localPort); this.settings.useNetworkProxy.includes(localPort);
// We'll look for domain-specific settings after SNI extraction
if (shouldUseNetworkProxy) { if (shouldUseNetworkProxy) {
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly // For NetworkProxy ports, we want to capture the TLS handshake and forward directly
let initialDataReceived = false; let initialDataReceived = false;
@ -1527,11 +1578,117 @@ 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;
// Forward directly to NetworkProxy without SNI processing // Check for TLS ClientHello with either no SNI or session tickets
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
// Extract SNI first
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging);
const hasSNI = !!extractedSNI;
// Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
// Always log for debugging purposes
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(
`[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
this.incrementTerminationStat('incoming', 'session_ticket_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
}
// 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(
`[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
`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;
}
}
// Try to extract SNI for domain-specific NetworkProxy handling
const connInfo = {
sourceIp: remoteIP,
sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '',
destPort: socket.localPort || 0
};
// Extract SNI to check for domain-specific NetworkProxy settings
const serverName = SniHandler.processTlsPacket(
chunk,
connInfo,
this.settings.enableTlsDebugLogging
);
if (serverName) {
// If we got an SNI, check for domain-specific NetworkProxy settings
const domainConfig = this.settings.domainConfigs.find((config) =>
config.domains.some((d) => plugins.minimatch(serverName, d))
);
// Save domain config and SNI in connection record
connectionRecord.domainConfig = domainConfig;
connectionRecord.lockedDomain = serverName;
// Use domain-specific NetworkProxy port if configured
if (domainConfig?.useNetworkProxy) {
const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort;
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
);
}
// Forward to NetworkProxy with domain-specific port
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk, networkProxyPort);
return;
}
}
// Forward directly to NetworkProxy without domain-specific settings
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk); this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
} else { } else {
// If not TLS, use normal direct connection // If not TLS, use normal direct connection
@ -1658,6 +1815,29 @@ export class PortProxy {
// Save domain config in connection record // Save domain config in connection record
connectionRecord.domainConfig = domainConfig; connectionRecord.domainConfig = domainConfig;
// Check if this domain should use NetworkProxy (domain-specific setting)
if (domainConfig?.useNetworkProxy && this.networkProxy) {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
);
}
const networkProxyPort = domainConfig.networkProxyPort || this.settings.networkProxyPort;
if (initialChunk && connectionRecord.isTLS) {
// For TLS connections with initial chunk, forward to NetworkProxy
this.forwardToNetworkProxy(
connectionId,
socket,
connectionRecord,
initialChunk,
networkProxyPort // Pass the domain-specific NetworkProxy port if configured
);
return; // Skip normal connection setup
}
}
// IP validation is skipped if allowedIPs is empty // IP validation is skipped if allowedIPs is empty
if (domainConfig) { if (domainConfig) {
const effectiveAllowedIPs: string[] = [ const effectiveAllowedIPs: string[] = [
@ -1801,6 +1981,22 @@ export class PortProxy {
initialDataReceived = true; initialDataReceived = 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 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 = '';
@ -1813,6 +2009,46 @@ export class PortProxy {
); );
} }
// Check for session tickets if allowSessionTicket is disabled
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
// Analyze for session resumption attempt
const resumptionInfo = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
if (resumptionInfo.isResumption) {
// Always log resumption attempt for easier debugging
// Try to extract SNI for logging
const extractedSNI = SniHandler.extractSNI(chunk, this.settings.enableTlsDebugLogging);
console.log(
`[${connectionId}] Session resumption detected in SNI handler. ` +
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
`SNI value: ${extractedSNI || 'None'}, ` +
`allowSessionTicket: ${this.settings.allowSessionTicket}`
);
// Block if there's session resumption without SNI
if (!resumptionInfo.hasSNI) {
console.log(
`[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` +
`Terminating connection to force new TLS handshake.`
);
if (connectionRecord.incomingTerminationReason === null) {
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
this.incrementTerminationStat('incoming', 'session_ticket_blocked');
}
socket.end();
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
return;
} else {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Session resumption with SNI detected in SNI handler. ` +
`Allowing connection since SNI is present.`
);
}
}
}
}
// Create connection info object for SNI extraction // Create connection info object for SNI extraction
const connInfo = { const connInfo = {
sourceIp: remoteIP, sourceIp: remoteIP,

View File

@ -3,8 +3,8 @@ import { Buffer } from 'buffer';
/** /**
* SNI (Server Name Indication) handler for TLS connections. * SNI (Server Name Indication) handler for TLS connections.
* Provides robust extraction of SNI values from TLS ClientHello messages * Provides robust extraction of SNI values from TLS ClientHello messages
* with support for fragmented packets, TLS 1.3 resumption, and Chrome-specific * with support for fragmented packets, TLS 1.3 resumption, Chrome-specific
* connection behaviors. * connection behaviors, and tab hibernation/reactivation scenarios.
*/ */
export class SniHandler { export class SniHandler {
// TLS record types and constants // TLS record types and constants
@ -15,13 +15,142 @@ 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
private static sessionCache: Map<
string,
{
sni: string;
timestamp: number;
clientRandom?: Buffer;
}
> = new Map();
// Longer timeout for session cache (24 hours by default)
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
// Cleanup interval for session cache (run every hour)
private static sessionCleanupInterval: NodeJS.Timeout | null = null;
/**
* Initialize the session cache cleanup mechanism.
* This should be called during application startup.
*/
public static initSessionCacheCleanup(): void {
if (this.sessionCleanupInterval === null) {
this.sessionCleanupInterval = setInterval(() => {
this.cleanupSessionCache();
}, 60 * 60 * 1000); // Run every hour
}
}
/**
* Clean up expired entries from the session cache
*/
private static cleanupSessionCache(): void {
const now = Date.now();
const expiredKeys: string[] = [];
this.sessionCache.forEach((session, key) => {
if (now - session.timestamp > this.sessionCacheTimeout) {
expiredKeys.push(key);
}
});
expiredKeys.forEach((key) => {
this.sessionCache.delete(key);
});
}
/**
* Create a client identity key for session tracking
* Uses source IP and optional client random for uniqueness
*
* @param sourceIp - Client IP address
* @param clientRandom - Optional TLS client random value
* @returns A string key for the session cache
*/
private static createClientKey(sourceIp: string, clientRandom?: Buffer): string {
if (clientRandom) {
// If we have the client random, use it for more precise tracking
return `${sourceIp}:${clientRandom.toString('hex')}`;
}
// Fall back to just IP-based tracking
return sourceIp;
}
/**
* Store SNI information in the session cache
*
* @param sourceIp - Client IP address
* @param sni - The extracted SNI value
* @param clientRandom - Optional TLS client random value
*/
private static cacheSession(sourceIp: string, sni: string, clientRandom?: Buffer): void {
const key = this.createClientKey(sourceIp, clientRandom);
this.sessionCache.set(key, {
sni,
timestamp: Date.now(),
clientRandom,
});
}
/**
* Retrieve SNI information from the session cache
*
* @param sourceIp - Client IP address
* @param clientRandom - Optional TLS client random value
* @returns The cached SNI or undefined if not found
*/
private static getCachedSession(sourceIp: string, clientRandom?: Buffer): string | undefined {
// Try with client random first for precision
if (clientRandom) {
const preciseKey = this.createClientKey(sourceIp, clientRandom);
const preciseSession = this.sessionCache.get(preciseKey);
if (preciseSession) {
return preciseSession.sni;
}
}
// Fall back to IP-only lookup
const ipKey = this.createClientKey(sourceIp);
const session = this.sessionCache.get(ipKey);
if (session) {
// Update the timestamp to keep the session alive
session.timestamp = Date.now();
return session.sni;
}
return undefined;
}
/**
* Extract the client random value from a ClientHello message
*
* @param buffer - The buffer containing the ClientHello
* @returns The 32-byte client random or undefined if extraction fails
*/
private static extractClientRandom(buffer: Buffer): Buffer | undefined {
try {
if (!this.isClientHello(buffer) || buffer.length < 46) {
return undefined;
}
// In a ClientHello message, the client random starts at position 11
// after record header (5 bytes), handshake type (1 byte),
// handshake length (3 bytes), and client version (2 bytes)
return buffer.slice(11, 11 + 32);
} catch (error) {
return undefined;
}
}
/** /**
* Checks if a buffer contains a TLS handshake message (record type 22) * Checks if a buffer contains a TLS handshake message (record type 22)
* @param buffer - The buffer to check * @param buffer - The buffer to check
@ -93,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}`);
@ -116,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}`);
} }
@ -153,6 +335,321 @@ export class SniHandler {
return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE; return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
} }
/**
* Checks if a ClientHello message contains session resumption indicators
* such as session tickets or PSK (Pre-Shared Key) extensions.
*
* @param buffer - The buffer containing a ClientHello message
* @param enableLogging - Whether to enable logging
* @returns Object containing details about session resumption and SNI presence
*/
public static hasSessionResumption(
buffer: Buffer,
enableLogging: boolean = false
): { isResumption: boolean; hasSNI: boolean } {
const log = (message: string) => {
if (enableLogging) {
console.log(`[Session Resumption] ${message}`);
}
};
if (!this.isClientHello(buffer)) {
return { isResumption: false, hasSNI: false };
}
try {
// Check for session ID presence first
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
pos += 32; // Skip client random
if (pos + 1 > buffer.length) return { isResumption: false, hasSNI: false };
const sessionIdLength = buffer[pos];
let hasNonEmptySessionId = sessionIdLength > 0;
if (hasNonEmptySessionId) {
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
}
// Continue to check for extensions
pos += 1 + sessionIdLength;
// Skip cipher suites
if (pos + 2 > buffer.length) return { isResumption: false, hasSNI: false };
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2 + cipherSuitesLength;
// Skip compression methods
if (pos + 1 > buffer.length) return { isResumption: false, hasSNI: false };
const compressionMethodsLength = buffer[pos];
pos += 1 + compressionMethodsLength;
// Check for extensions
if (pos + 2 > buffer.length) return { isResumption: false, hasSNI: false };
// Look for session resumption extensions
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
// Extensions end position
const extensionsEnd = pos + extensionsLength;
if (extensionsEnd > buffer.length) return { isResumption: false, hasSNI: false };
// Track resumption indicators
let hasSessionTicket = false;
let hasPSK = false;
let hasEarlyData = false;
// Iterate through extensions
while (pos + 4 <= extensionsEnd) {
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
log('Found session ticket extension');
hasSessionTicket = true;
// Check if session ticket has non-zero length (active ticket)
if (extensionLength > 0) {
log(`Session ticket has length ${extensionLength} - active ticket present`);
}
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
log('Found PSK extension (TLS 1.3 resumption mechanism)');
hasPSK = true;
} else if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
log('Found Early Data extension (TLS 1.3 0-RTT)');
hasEarlyData = true;
}
// Skip extension data
pos += extensionLength;
}
// Check if SNI is included
let hasSNI = false;
// Reset position and scan again for SNI extension
pos = 5 + 1 + 3 + 2; // Reset to after handshake type, length and client version
pos += 32; // Skip client random
if (pos + 1 <= buffer.length) {
const sessionIdLength = buffer[pos];
pos += 1 + sessionIdLength;
// Skip cipher suites
if (pos + 2 <= buffer.length) {
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2 + cipherSuitesLength;
// Skip compression methods
if (pos + 1 <= buffer.length) {
const compressionMethodsLength = buffer[pos];
pos += 1 + compressionMethodsLength;
// Check for extensions
if (pos + 2 <= buffer.length) {
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
// Extensions end position
const extensionsEnd = pos + extensionsLength;
if (extensionsEnd <= buffer.length) {
// Scan for SNI extension
while (pos + 4 <= extensionsEnd) {
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
// Check that the SNI extension actually has content
if (extensionLength > 0) {
hasSNI = true;
// Try to extract the actual SNI value for logging
try {
// Skip to server_name_list_length (2 bytes)
const tempPos = pos;
if (tempPos + 2 <= extensionsEnd) {
const nameListLength = (buffer[tempPos] << 8) + buffer[tempPos + 1];
// Skip server_name_list_length (2 bytes)
if (tempPos + 2 + 1 <= extensionsEnd) {
// Check name_type (should be 0 for hostname)
if (buffer[tempPos + 2] === 0) {
// Skip name_type (1 byte)
if (tempPos + 3 + 2 <= extensionsEnd) {
// Get name_length (2 bytes)
const nameLength = (buffer[tempPos + 3] << 8) + buffer[tempPos + 4];
// Extract the hostname
if (tempPos + 5 + nameLength <= extensionsEnd) {
const hostname = buffer
.slice(tempPos + 5, tempPos + 5 + nameLength)
.toString('utf8');
log(`Found SNI extension with server_name: ${hostname}`);
}
}
}
}
}
} catch (e) {
log(`Error extracting SNI value: ${e}`);
log('Found SNI extension with length: ' + extensionLength);
}
} else {
log('Found empty SNI extension, treating as no SNI');
}
break;
}
// Skip extension data
pos += extensionLength;
}
}
}
}
}
}
// Consider it a resumption if any resumption mechanism is present
const isResumption =
hasSessionTicket || hasPSK || hasEarlyData || (hasNonEmptySessionId && !hasPSK); // Legacy resumption
if (isResumption) {
log(
'Session resumption detected: ' +
(hasSessionTicket ? 'session ticket, ' : '') +
(hasPSK ? 'PSK, ' : '') +
(hasEarlyData ? 'early data, ' : '') +
(hasNonEmptySessionId ? 'session ID' : '') +
(hasSNI ? ', with SNI' : ', without SNI')
);
}
// Return an object with both flags
// For clarity: connections should be blocked if they have session resumption without SNI
if (isResumption) {
log(
`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${
hasSessionTicket ? 'session ticket, ' : ''
}${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${
hasNonEmptySessionId ? 'session ID' : ''
}`
);
}
return {
isResumption,
hasSNI,
};
} catch (error) {
log(`Error checking for session resumption: ${error}`);
return { isResumption: false, hasSNI: false };
}
}
/**
* Detects characteristics of a tab reactivation TLS handshake
* These often have specific patterns in Chrome and other browsers
*
* @param buffer - The buffer containing a ClientHello message
* @param enableLogging - Whether to enable logging
* @returns true if this appears to be a tab reactivation handshake
*/
public static isTabReactivationHandshake(
buffer: Buffer,
enableLogging: boolean = false
): boolean {
const log = (message: string) => {
if (enableLogging) {
console.log(`[Tab Reactivation] ${message}`);
}
};
if (!this.isClientHello(buffer)) {
return false;
}
try {
// Check for session ID presence (tab reactivation often has a session ID)
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
pos += 32; // Skip client random
if (pos + 1 > buffer.length) return false;
const sessionIdLength = buffer[pos];
// Non-empty session ID is a good indicator
if (sessionIdLength > 0) {
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
// Skip to extensions
pos += 1 + sessionIdLength;
// Skip cipher suites
if (pos + 2 > buffer.length) return false;
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2 + cipherSuitesLength;
// Skip compression methods
if (pos + 1 > buffer.length) return false;
const compressionMethodsLength = buffer[pos];
pos += 1 + compressionMethodsLength;
// Check for extensions
if (pos + 2 > buffer.length) return false;
// Look for specific extensions that indicate tab reactivation
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
// Extensions end position
const extensionsEnd = pos + extensionsLength;
if (extensionsEnd > buffer.length) return false;
// Tab reactivation often has session tickets but no SNI
let hasSessionTicket = false;
let hasSNI = false;
let hasPSK = false;
// Iterate through extensions
while (pos + 4 <= extensionsEnd) {
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
hasSessionTicket = true;
} else if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
hasSNI = true;
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
hasPSK = true;
}
// Skip extension data
pos += extensionLength;
}
// Pattern for tab reactivation: session identifier + (ticket or PSK) but no SNI
if ((hasSessionTicket || hasPSK) && !hasSNI) {
log('Detected tab reactivation pattern: session resumption without SNI');
return true;
}
}
} catch (error) {
log(`Error checking for tab reactivation: ${error}`);
}
return false;
}
/** /**
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message. * Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
* Implements robust parsing with support for session resumption edge cases. * Implements robust parsing with support for session resumption edge cases.
@ -523,7 +1020,11 @@ export class SniHandler {
pos += identityLength; pos += identityLength;
// Skip obfuscated ticket age (4 bytes) // Skip obfuscated ticket age (4 bytes)
if (pos + 4 <= identitiesEnd) {
pos += 4; pos += 4;
} else {
break;
}
// Try to parse the identity as UTF-8 // Try to parse the identity as UTF-8
try { try {
@ -535,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]}`);
@ -581,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}`);
@ -673,6 +1172,7 @@ export class SniHandler {
* 4. Fragmented ClientHello messages * 4. Fragmented ClientHello messages
* 5. TLS 1.3 Early Data (0-RTT) * 5. TLS 1.3 Early Data (0-RTT)
* 6. Chrome's connection racing behaviors * 6. Chrome's connection racing behaviors
* 7. Tab reactivation patterns with session cache
* *
* @param buffer - The buffer containing the TLS ClientHello message * @param buffer - The buffer containing the TLS ClientHello message
* @param connectionInfo - Optional connection information for fragment handling * @param connectionInfo - Optional connection information for fragment handling
@ -695,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) {
@ -718,42 +1235,75 @@ export class SniHandler {
const standardSni = this.extractSNI(processBuffer, enableLogging); const standardSni = this.extractSNI(processBuffer, enableLogging);
if (standardSni) { if (standardSni) {
log(`Found standard SNI: ${standardSni}`); log(`Found standard SNI: ${standardSni}`);
// If we extracted a standard SNI, cache it for future use
if (connectionInfo?.sourceIp) {
const clientRandom = this.extractClientRandom(processBuffer);
this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
log(`Cached SNI for future reference: ${standardSni}`);
}
return standardSni; return standardSni;
} }
// Check for TLS 1.3 early data (0-RTT) // Check for session resumption when standard SNI extraction fails
const hasEarly = this.hasEarlyData(processBuffer, enableLogging); // This may help in chained proxy scenarios
if (hasEarly) {
log('TLS 1.3 Early Data detected, using special handling');
// In 0-RTT, Chrome often relies on server remembering the SNI from previous sessions
// We could implement session tracking here if necessary
}
// 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
if (connectionInfo?.sourceIp) {
const clientRandom = this.extractClientRandom(processBuffer);
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
}
return pskSni; return pskSni;
} }
// Special handling for Chrome connection racing // If session resumption has SNI in a non-standard location,
// Chrome often opens multiple connections in parallel with different // we need to apply heuristics
// characteristics to improve performance if (connectionInfo?.sourceIp) {
// Here we would look for specific patterns in ClientHello that indicate const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
// it's part of a connection race if (cachedSni) {
log(`Using cached SNI for session resumption: ${cachedSni}`);
// Detect if this is likely a secondary connection in a race return cachedSni;
// by examining the cipher suites and extensions }
// This would require session state tracking across connections }
}
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;
} }
@ -763,7 +1313,7 @@ export class SniHandler {
* *
* The method uses connection tracking to handle fragmented ClientHello * The method uses connection tracking to handle fragmented ClientHello
* messages and various TLS 1.3 behaviors, including Chrome's connection * messages and various TLS 1.3 behaviors, including Chrome's connection
* racing patterns. * racing patterns and tab reactivation behaviors.
* *
* @param buffer - The buffer containing TLS data * @param buffer - The buffer containing TLS data
* @param connectionInfo - Connection metadata (IPs and ports) * @param connectionInfo - Connection metadata (IPs and ports)
@ -794,7 +1344,7 @@ export class SniHandler {
connectionInfo.timestamp = Date.now(); connectionInfo.timestamp = Date.now();
} }
// Check if this is a TLS handshake // Check if this is a TLS handshake or application data
if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) { if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
log('Not a TLS handshake or application data packet'); log('Not a TLS handshake or application data packet');
return undefined; return undefined;
@ -804,20 +1354,27 @@ export class SniHandler {
const connectionId = this.createConnectionId(connectionInfo); const connectionId = this.createConnectionId(connectionInfo);
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`); log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
// Handle special case: if we already have a cached SNI from a previous // Handle application data with cached SNI (for connection racing)
// connection from the same client IP within a short time window, if (this.isTlsApplicationData(buffer)) {
// this might be a connection racing situation // First check if explicit cachedSni was provided
if (cachedSni && this.isTlsApplicationData(buffer)) { if (cachedSni) {
log(`Using cached SNI from connection racing: ${cachedSni}`); log(`Using provided cached SNI for application data: ${cachedSni}`);
return cachedSni; return cachedSni;
} }
// Try to extract SNI with full resumption support and fragment handling // Otherwise check our session cache
const sni = this.extractSNIWithResumptionSupport( const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
buffer, if (sessionCachedSni) {
connectionInfo, log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
enableLogging return sessionCachedSni;
); }
log('Application data packet without cached SNI, cannot determine hostname');
return undefined;
}
// For handshake messages, try the full extraction process
const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging);
if (sni) { if (sni) {
log(`Successfully extracted SNI: ${sni}`); log(`Successfully extracted SNI: ${sni}`);
@ -828,6 +1385,13 @@ export class SniHandler {
// If it is, but we couldn't get an SNI, it might be a fragment or // If it is, but we couldn't get an SNI, it might be a fragment or
// a connection race situation // a connection race situation
if (this.isClientHello(buffer)) { if (this.isClientHello(buffer)) {
// Check if we have a cached session for this IP
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
if (sessionCachedSni) {
log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
return sessionCachedSni;
}
log('Valid ClientHello detected, but no SNI extracted - might need more data'); log('Valid ClientHello detected, but no SNI extracted - might need more data');
} }