264 lines
9.7 KiB
TypeScript
264 lines
9.7 KiB
TypeScript
import { Buffer } from 'buffer';
|
|
import {
|
|
TlsRecordType,
|
|
TlsHandshakeType,
|
|
TlsExtensionType,
|
|
TlsUtils
|
|
} from '../utils/tls-utils.js';
|
|
import {
|
|
ClientHelloParser,
|
|
type LoggerFunction
|
|
} from './client-hello-parser.js';
|
|
import {
|
|
SniExtraction,
|
|
type ConnectionInfo
|
|
} from './sni-extraction.js';
|
|
|
|
/**
|
|
* SNI (Server Name Indication) handler for TLS connections.
|
|
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
* with support for fragmented packets, TLS 1.3 resumption, Chrome-specific
|
|
* connection behaviors, and tab hibernation/reactivation scenarios.
|
|
*
|
|
* This class retains the original API but leverages the new modular implementation
|
|
* for better maintainability and testability.
|
|
*/
|
|
export class SniHandler {
|
|
// Re-export constants for backward compatibility
|
|
private static readonly TLS_HANDSHAKE_RECORD_TYPE = TlsRecordType.HANDSHAKE;
|
|
private static readonly TLS_APPLICATION_DATA_TYPE = TlsRecordType.APPLICATION_DATA;
|
|
private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = TlsHandshakeType.CLIENT_HELLO;
|
|
private static readonly TLS_SNI_EXTENSION_TYPE = TlsExtensionType.SERVER_NAME;
|
|
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = TlsExtensionType.SESSION_TICKET;
|
|
private static readonly TLS_SNI_HOST_NAME_TYPE = 0; // NameType.HOST_NAME in RFC 6066
|
|
private static readonly TLS_PSK_EXTENSION_TYPE = TlsExtensionType.PRE_SHARED_KEY;
|
|
private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = TlsExtensionType.PSK_KEY_EXCHANGE_MODES;
|
|
private static readonly TLS_EARLY_DATA_EXTENSION_TYPE = TlsExtensionType.EARLY_DATA;
|
|
|
|
/**
|
|
* Checks if a buffer contains a TLS handshake message (record type 22)
|
|
* @param buffer - The buffer to check
|
|
* @returns true if the buffer starts with a TLS handshake record type
|
|
*/
|
|
public static isTlsHandshake(buffer: Buffer): boolean {
|
|
return TlsUtils.isTlsHandshake(buffer);
|
|
}
|
|
|
|
/**
|
|
* Checks if a buffer contains TLS application data (record type 23)
|
|
* @param buffer - The buffer to check
|
|
* @returns true if the buffer starts with a TLS application data record type
|
|
*/
|
|
public static isTlsApplicationData(buffer: Buffer): boolean {
|
|
return TlsUtils.isTlsApplicationData(buffer);
|
|
}
|
|
|
|
/**
|
|
* Creates a connection ID based on source/destination information
|
|
* Used to track fragmented ClientHello messages across multiple packets
|
|
*
|
|
* @param connectionInfo - Object containing connection identifiers (IP/port)
|
|
* @returns A string ID for the connection
|
|
*/
|
|
public static createConnectionId(connectionInfo: {
|
|
sourceIp?: string;
|
|
sourcePort?: number;
|
|
destIp?: string;
|
|
destPort?: number;
|
|
}): string {
|
|
return TlsUtils.createConnectionId(connectionInfo);
|
|
}
|
|
|
|
/**
|
|
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
|
* TLS record fragments that might span multiple TCP packets.
|
|
*
|
|
* @param buffer - The current buffer fragment
|
|
* @param connectionId - Unique identifier for the connection
|
|
* @param enableLogging - Whether to enable logging
|
|
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
|
|
*/
|
|
public static handleFragmentedClientHello(
|
|
buffer: Buffer,
|
|
connectionId: string,
|
|
enableLogging: boolean = false
|
|
): Buffer | undefined {
|
|
const logger = enableLogging ?
|
|
(message: string) => console.log(`[SNI Fragment] ${message}`) :
|
|
undefined;
|
|
|
|
return ClientHelloParser.handleFragmentedClientHello(buffer, connectionId, logger);
|
|
}
|
|
|
|
/**
|
|
* Checks if a buffer contains a TLS ClientHello message
|
|
* @param buffer - The buffer to check
|
|
* @returns true if the buffer appears to be a ClientHello message
|
|
*/
|
|
public static isClientHello(buffer: Buffer): boolean {
|
|
return TlsUtils.isClientHello(buffer);
|
|
}
|
|
|
|
/**
|
|
* 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 logger = enableLogging ?
|
|
(message: string) => console.log(`[Session Resumption] ${message}`) :
|
|
undefined;
|
|
|
|
return ClientHelloParser.hasSessionResumption(buffer, logger);
|
|
}
|
|
|
|
/**
|
|
* 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 logger = enableLogging ?
|
|
(message: string) => console.log(`[Tab Reactivation] ${message}`) :
|
|
undefined;
|
|
|
|
return ClientHelloParser.isTabReactivationHandshake(buffer, logger);
|
|
}
|
|
|
|
/**
|
|
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
|
|
* Implements robust parsing with support for session resumption edge cases.
|
|
*
|
|
* @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 extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
const logger = enableLogging ?
|
|
(message: string) => console.log(`[SNI Extraction] ${message}`) :
|
|
undefined;
|
|
|
|
return SniExtraction.extractSNI(buffer, logger);
|
|
}
|
|
|
|
/**
|
|
* 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 logger = enableLogging ?
|
|
(message: string) => console.log(`[PSK-SNI Extraction] ${message}`) :
|
|
undefined;
|
|
|
|
return SniExtraction.extractSNIFromPSKExtension(buffer, logger);
|
|
}
|
|
|
|
/**
|
|
* Checks if the buffer contains TLS 1.3 early data (0-RTT)
|
|
* @param buffer - The buffer to check
|
|
* @param enableLogging - Whether to enable logging
|
|
* @returns true if early data is detected
|
|
*/
|
|
public static hasEarlyData(buffer: Buffer, enableLogging: boolean = false): boolean {
|
|
// This functionality has been moved to ClientHelloParser
|
|
// We can implement it in terms of the parse result if needed
|
|
const logger = enableLogging ?
|
|
(message: string) => console.log(`[Early Data] ${message}`) :
|
|
undefined;
|
|
|
|
const parseResult = ClientHelloParser.parseClientHello(buffer, logger);
|
|
return parseResult.isValid && parseResult.hasEarlyData;
|
|
}
|
|
|
|
/**
|
|
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
* session resumption edge cases more robustly than the standard extraction.
|
|
*
|
|
* This method handles:
|
|
* 1. Standard SNI extraction
|
|
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
|
|
* 3. Session ticket-based resumption
|
|
* 4. Fragmented ClientHello messages
|
|
* 5. TLS 1.3 Early Data (0-RTT)
|
|
* 6. Chrome's connection racing behaviors
|
|
*
|
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
* @param connectionInfo - Optional connection information for fragment handling
|
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
* @returns The extracted server name or undefined if not found or more data needed
|
|
*/
|
|
public static extractSNIWithResumptionSupport(
|
|
buffer: Buffer,
|
|
connectionInfo?: {
|
|
sourceIp?: string;
|
|
sourcePort?: number;
|
|
destIp?: string;
|
|
destPort?: number;
|
|
},
|
|
enableLogging: boolean = false
|
|
): string | undefined {
|
|
const logger = enableLogging ?
|
|
(message: string) => console.log(`[SNI Extraction] ${message}`) :
|
|
undefined;
|
|
|
|
return SniExtraction.extractSNIWithResumptionSupport(
|
|
buffer,
|
|
connectionInfo as ConnectionInfo,
|
|
logger
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Main entry point for SNI extraction that handles all edge cases.
|
|
* This should be called for each TLS packet received from a client.
|
|
*
|
|
* The method uses connection tracking to handle fragmented ClientHello
|
|
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
* racing patterns and tab reactivation behaviors.
|
|
*
|
|
* @param buffer - The buffer containing TLS data
|
|
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
* @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
|
|
*/
|
|
public static processTlsPacket(
|
|
buffer: Buffer,
|
|
connectionInfo: {
|
|
sourceIp: string;
|
|
sourcePort: number;
|
|
destIp: string;
|
|
destPort: number;
|
|
timestamp?: number;
|
|
},
|
|
enableLogging: boolean = false,
|
|
cachedSni?: string
|
|
): string | undefined {
|
|
const logger = enableLogging ?
|
|
(message: string) => console.log(`[TLS Packet] ${message}`) :
|
|
undefined;
|
|
|
|
return SniExtraction.processTlsPacket(buffer, connectionInfo, logger, cachedSni);
|
|
}
|
|
} |