/** * TLS protocol detector */ // TLS detector doesn't need plugins imports import type { IProtocolDetector } from '../models/interfaces.js'; import type { IDetectionResult, IDetectionOptions, IConnectionInfo } from '../models/detection-types.js'; import { readUInt16BE } from '../utils/buffer-utils.js'; import { tlsVersionToString } from '../utils/parser-utils.js'; // Import from protocols import { TlsRecordType, TlsHandshakeType, TlsExtensionType } from '../../protocols/tls/index.js'; // Import TLS utilities for SNI extraction from protocols import { SniExtraction } from '../../protocols/tls/sni/sni-extraction.js'; import { ClientHelloParser } from '../../protocols/tls/sni/client-hello-parser.js'; /** * TLS detector implementation */ export class TlsDetector implements IProtocolDetector { /** * Minimum bytes needed to identify TLS (record header) */ private static readonly MIN_TLS_HEADER_SIZE = 5; /** * Detect TLS protocol from buffer */ detect(buffer: Buffer, options?: IDetectionOptions): IDetectionResult | null { // Check if buffer is too small if (buffer.length < TlsDetector.MIN_TLS_HEADER_SIZE) { return null; } // Check if this is a TLS record if (!this.isTlsRecord(buffer)) { return null; } // Extract basic TLS info const recordType = buffer[0]; const tlsMajor = buffer[1]; const tlsMinor = buffer[2]; const recordLength = readUInt16BE(buffer, 3); // Initialize connection info const connectionInfo: IConnectionInfo = { protocol: 'tls', tlsVersion: tlsVersionToString(tlsMajor, tlsMinor) || undefined }; // If it's a handshake, try to extract more info if (recordType === TlsRecordType.HANDSHAKE && buffer.length >= 6) { const handshakeType = buffer[5]; // For ClientHello, extract SNI and other info if (handshakeType === TlsHandshakeType.CLIENT_HELLO) { // Check if we have the complete handshake const totalRecordLength = recordLength + 5; // Including TLS header if (buffer.length >= totalRecordLength) { // Extract SNI using existing logic const sni = SniExtraction.extractSNI(buffer); if (sni) { connectionInfo.domain = sni; connectionInfo.sni = sni; } // Parse ClientHello for additional info const parseResult = ClientHelloParser.parseClientHello(buffer); if (parseResult.isValid) { // Extract ALPN if present const alpnExtension = parseResult.extensions.find( ext => ext.type === TlsExtensionType.APPLICATION_LAYER_PROTOCOL_NEGOTIATION ); if (alpnExtension) { connectionInfo.alpn = this.parseAlpnExtension(alpnExtension.data); } // Store cipher suites if needed if (parseResult.cipherSuites && options?.extractFullHeaders) { connectionInfo.cipherSuites = this.parseCipherSuites(parseResult.cipherSuites); } } // Return complete result return { protocol: 'tls', connectionInfo, remainingBuffer: buffer.length > totalRecordLength ? buffer.subarray(totalRecordLength) : undefined, isComplete: true }; } else { // Incomplete handshake return { protocol: 'tls', connectionInfo, isComplete: false, bytesNeeded: totalRecordLength }; } } } // For other TLS record types, just return basic info return { protocol: 'tls', connectionInfo, isComplete: true, remainingBuffer: buffer.length > recordLength + 5 ? buffer.subarray(recordLength + 5) : undefined }; } /** * Check if buffer can be handled by this detector */ canHandle(buffer: Buffer): boolean { return buffer.length >= TlsDetector.MIN_TLS_HEADER_SIZE && this.isTlsRecord(buffer); } /** * Get minimum bytes needed for detection */ getMinimumBytes(): number { return TlsDetector.MIN_TLS_HEADER_SIZE; } /** * Check if buffer contains a valid TLS record */ private isTlsRecord(buffer: Buffer): boolean { const recordType = buffer[0]; // Check for valid record type const validTypes = [ TlsRecordType.CHANGE_CIPHER_SPEC, TlsRecordType.ALERT, TlsRecordType.HANDSHAKE, TlsRecordType.APPLICATION_DATA, TlsRecordType.HEARTBEAT ]; if (!validTypes.includes(recordType)) { return false; } // Check TLS version bytes (should be 0x03 0x0X) if (buffer[1] !== 0x03) { return false; } // Check record length is reasonable const recordLength = readUInt16BE(buffer, 3); if (recordLength > 16384) { // Max TLS record size return false; } return true; } /** * Parse ALPN extension data */ private parseAlpnExtension(data: Buffer): string[] { const protocols: string[] = []; if (data.length < 2) { return protocols; } const listLength = readUInt16BE(data, 0); let offset = 2; while (offset < Math.min(2 + listLength, data.length)) { const protoLength = data[offset]; offset++; if (offset + protoLength <= data.length) { const protocol = data.subarray(offset, offset + protoLength).toString('ascii'); protocols.push(protocol); offset += protoLength; } else { break; } } return protocols; } /** * Parse cipher suites */ private parseCipherSuites(cipherData: Buffer): number[] { const suites: number[] = []; for (let i = 0; i < cipherData.length - 1; i += 2) { const suite = readUInt16BE(cipherData, i); suites.push(suite); } return suites; } /** * Detect with context for fragmented data */ detectWithContext( buffer: Buffer, _context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number }, options?: IDetectionOptions ): IDetectionResult | null { // This method is deprecated - TLS detection should use the fragment manager // from the parent detector system, not maintain its own fragments return this.detect(buffer, options); } }