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:
parent
474134d29c
commit
36e4341315
@ -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
|
||||||
|
|
||||||
|
@ -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.'
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user