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
## 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)
Improve buffering and data handling during connection setup in PortProxy to prevent data loss

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
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.'
}

View File

@ -11,6 +11,8 @@ export class SniHandler {
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
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)
@ -178,6 +180,7 @@ export class SniHandler {
// Track if we found session tickets (for improved resumption handling)
let hasSessionTicket = false;
let hasPskExtension = false;
// Iterate through extensions
while (pos + 4 <= extensionsEnd) {
@ -275,15 +278,21 @@ export class SniHandler {
log('Found session ticket extension');
hasSessionTicket = true;
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 {
// Skip this extension
pos += extensionLength;
}
}
// Log if we found a session ticket but no SNI
if (hasSessionTicket) {
log('Session ticket present but no SNI found - possible resumption scenario');
// Log if we found session resumption indicators but no SNI
if (hasSessionTicket || hasPskExtension) {
log('Session resumption indicators present but no SNI found');
}
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
* session resumption edge cases more robustly than the standard extraction.
*
* This method is specifically designed for Chrome and other browsers that
* may send different ClientHello formats during session resumption.
* This method handles:
* 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 enableLogging - Whether to enable detailed debug logging
@ -312,6 +486,9 @@ export class SniHandler {
// First try the standard SNI extraction
const standardSni = this.extractSNI(buffer, enableLogging);
if (standardSni) {
if (enableLogging) {
console.log(`[SNI Extraction] Found standard SNI: ${standardSni}`);
}
return standardSni;
}
@ -322,8 +499,19 @@ export class SniHandler {
console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
}
// Additional handling could be implemented here for specific browser behaviors
// For now, this is a placeholder for future improvements
// Try to extract from PSK extension (TLS 1.3 resumption)
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;