fix(snihandler): Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators.

This commit is contained in:
Philipp Kunz 2025-03-11 17:23:57 +00:00
parent 474134d29c
commit 36e4341315
3 changed files with 204 additions and 8 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-03-11 - 3.37.3 - fix(snihandler)
Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators.
- Defined TLS_PSK_EXTENSION_TYPE and TLS_PSK_KE_MODES_EXTENSION_TYPE constants.
- Added extractSNIFromPSKExtension method to handle ClientHello messages containing PSK identities.
- Improved logging to indicate when session resumption indicators (ticket or PSK) are present but no standard SNI is found.
- Enhanced extractSNIWithResumptionSupport to attempt PSK extraction if standard SNI extraction fails.
## 2025-03-11 - 3.37.2 - fix(PortProxy) ## 2025-03-11 - 3.37.2 - fix(PortProxy)
Improve buffering and data handling during connection setup in PortProxy to prevent data loss Improve buffering and data handling during connection setup in PortProxy to prevent data loss

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '3.37.2', version: '3.37.3',
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,8 @@ export class SniHandler {
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000; private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
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_KE_MODES_EXTENSION_TYPE = 0x002D; // PSK Key Exchange Modes
/** /**
* Checks if a buffer contains a TLS handshake message (record type 22) * Checks if a buffer contains a TLS handshake message (record type 22)
@ -178,6 +180,7 @@ export class SniHandler {
// Track if we found session tickets (for improved resumption handling) // Track if we found session tickets (for improved resumption handling)
let hasSessionTicket = false; let hasSessionTicket = false;
let hasPskExtension = false;
// Iterate through extensions // Iterate through extensions
while (pos + 4 <= extensionsEnd) { while (pos + 4 <= extensionsEnd) {
@ -275,15 +278,21 @@ export class SniHandler {
log('Found session ticket extension'); log('Found session ticket extension');
hasSessionTicket = true; hasSessionTicket = true;
pos += extensionLength; // Skip this extension pos += extensionLength; // Skip this extension
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
// TLS 1.3 PSK extension - mark for resumption support
log('Found PSK extension (TLS 1.3 resumption indicator)');
hasPskExtension = true;
// We'll skip the extension here and process it separately if needed
pos += extensionLength;
} else { } else {
// Skip this extension // Skip this extension
pos += extensionLength; pos += extensionLength;
} }
} }
// Log if we found a session ticket but no SNI // Log if we found session resumption indicators but no SNI
if (hasSessionTicket) { if (hasSessionTicket || hasPskExtension) {
log('Session ticket present but no SNI found - possible resumption scenario'); log('Session resumption indicators present but no SNI found');
} }
log('No SNI extension found in ClientHello'); log('No SNI extension found in ClientHello');
@ -294,12 +303,177 @@ export class SniHandler {
} }
} }
/**
* Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
*
* In TLS 1.3, when a client attempts to resume a session, it may include
* the server name in the PSK identity hint rather than in the SNI extension.
*
* @param buffer - The buffer containing the TLS ClientHello message
* @param enableLogging - Whether to enable detailed debug logging
* @returns The extracted server name or undefined if not found
*/
public static extractSNIFromPSKExtension(
buffer: Buffer,
enableLogging: boolean = false
): string | undefined {
const log = (message: string) => {
if (enableLogging) {
console.log(`[PSK-SNI Extraction] ${message}`);
}
};
try {
// Ensure this is a ClientHello
if (!this.isClientHello(buffer)) {
log('Not a ClientHello message');
return undefined;
}
// Find the start position of extensions
let pos = 5; // Start after record header
// Skip handshake type (1 byte)
pos += 1;
// Skip handshake length (3 bytes)
pos += 3;
// Skip client version (2 bytes)
pos += 2;
// Skip client random (32 bytes)
pos += 32;
// Skip session ID
if (pos + 1 > buffer.length) return undefined;
const sessionIdLength = buffer[pos];
pos += 1 + sessionIdLength;
// Skip cipher suites
if (pos + 2 > buffer.length) return undefined;
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2 + cipherSuitesLength;
// Skip compression methods
if (pos + 1 > buffer.length) return undefined;
const compressionMethodsLength = buffer[pos];
pos += 1 + compressionMethodsLength;
// Check if we have extensions
if (pos + 2 > buffer.length) {
log('No extensions present');
return undefined;
}
// Get extensions length
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
// Extensions end position
const extensionsEnd = pos + extensionsLength;
if (extensionsEnd > buffer.length) return undefined;
// Look for PSK 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_PSK_EXTENSION_TYPE) {
log('Found PSK extension');
// PSK extension structure:
// 2 bytes: identities list length
if (pos + 2 > extensionsEnd) break;
const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
// End of identities list
const identitiesEnd = pos + identitiesLength;
if (identitiesEnd > extensionsEnd) break;
// Process each PSK identity
while (pos + 2 <= identitiesEnd) {
// Identity length (2 bytes)
if (pos + 2 > identitiesEnd) break;
const identityLength = (buffer[pos] << 8) + buffer[pos + 1];
pos += 2;
if (pos + identityLength > identitiesEnd) break;
// Try to extract hostname from identity
// Chrome often embeds the hostname in the PSK identity
// This is a heuristic as there's no standard format
if (identityLength > 0) {
const identity = buffer.slice(pos, pos + identityLength);
// Skip identity bytes
pos += identityLength;
// Skip obfuscated ticket age (4 bytes)
pos += 4;
// Try to parse the identity as UTF-8
try {
const identityStr = identity.toString('utf8');
log(`PSK identity: ${identityStr}`);
// Check if the identity contains hostname hints
// Chrome often embeds the hostname in a known format
// Try to extract using common patterns
// 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 domainMatch = identityStr.match(domainPattern);
if (domainMatch && domainMatch[0]) {
log(`Found domain in PSK identity: ${domainMatch[0]}`);
return domainMatch[0];
}
// Pattern 2: Chrome sometimes uses a specific format with delimiters
// This is a heuristic approach since the format isn't standardized
const parts = identityStr.split('|');
if (parts.length > 1) {
for (const part of parts) {
if (part.includes('.') && !part.includes('/')) {
const possibleDomain = part.trim();
if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
return possibleDomain;
}
}
}
}
} catch (e) {
log('Failed to parse PSK identity as UTF-8');
}
}
}
} else {
// Skip this extension
pos += extensionLength;
}
}
log('No hostname found in PSK extension');
return undefined;
} catch (error) {
log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
/** /**
* Attempts to extract SNI from an initial ClientHello packet and handles * Attempts to extract SNI from an initial ClientHello packet and handles
* session resumption edge cases more robustly than the standard extraction. * session resumption edge cases more robustly than the standard extraction.
* *
* This method is specifically designed for Chrome and other browsers that * This method handles:
* may send different ClientHello formats during session resumption. * 1. Standard SNI extraction
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
* 3. Session ticket-based resumption
* *
* @param buffer - The buffer containing the TLS ClientHello message * @param buffer - The buffer containing the TLS ClientHello message
* @param enableLogging - Whether to enable detailed debug logging * @param enableLogging - Whether to enable detailed debug logging
@ -312,6 +486,9 @@ export class SniHandler {
// First try the standard SNI extraction // First try the standard SNI extraction
const standardSni = this.extractSNI(buffer, enableLogging); const standardSni = this.extractSNI(buffer, enableLogging);
if (standardSni) { if (standardSni) {
if (enableLogging) {
console.log(`[SNI Extraction] Found standard SNI: ${standardSni}`);
}
return standardSni; return standardSni;
} }
@ -322,8 +499,19 @@ export class SniHandler {
console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption'); console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
} }
// Additional handling could be implemented here for specific browser behaviors // Try to extract from PSK extension (TLS 1.3 resumption)
// For now, this is a placeholder for future improvements const pskSni = this.extractSNIFromPSKExtension(buffer, enableLogging);
if (pskSni) {
if (enableLogging) {
console.log(`[SNI Extraction] Extracted SNI from PSK extension: ${pskSni}`);
}
return pskSni;
}
// Could add more browser-specific heuristics here if needed
if (enableLogging) {
console.log('[SNI Extraction] Failed to extract SNI from resumption mechanisms');
}
} }
return undefined; return undefined;