2025-07-21 19:40:01 +00:00
|
|
|
/**
|
|
|
|
* 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';
|
2025-07-22 10:35:39 +00:00
|
|
|
import { readUInt16BE } from '../utils/buffer-utils.js';
|
2025-07-21 19:40:01 +00:00
|
|
|
import { tlsVersionToString } from '../utils/parser-utils.js';
|
|
|
|
|
2025-07-21 22:37:45 +00:00
|
|
|
// 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';
|
2025-07-21 19:40:01 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* TLS detector implementation
|
|
|
|
*/
|
|
|
|
export class TlsDetector implements IProtocolDetector {
|
|
|
|
/**
|
|
|
|
* Minimum bytes needed to identify TLS (record header)
|
|
|
|
*/
|
|
|
|
private static readonly MIN_TLS_HEADER_SIZE = 5;
|
|
|
|
|
2025-07-22 00:19:59 +00:00
|
|
|
|
2025-07-21 19:40:01 +00:00
|
|
|
/**
|
|
|
|
* 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
|
2025-07-21 22:37:45 +00:00
|
|
|
? buffer.subarray(totalRecordLength)
|
2025-07-21 19:40:01 +00:00
|
|
|
: 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
|
2025-07-21 22:37:45 +00:00
|
|
|
? buffer.subarray(recordLength + 5)
|
2025-07-21 19:40:01 +00:00
|
|
|
: 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) {
|
2025-07-21 22:37:45 +00:00
|
|
|
const protocol = data.subarray(offset, offset + protoLength).toString('ascii');
|
2025-07-21 19:40:01 +00:00
|
|
|
protocols.push(protocol);
|
|
|
|
offset += protoLength;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return protocols;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse cipher suites
|
|
|
|
*/
|
2025-07-22 00:19:59 +00:00
|
|
|
private parseCipherSuites(cipherData: Buffer): number[] {
|
2025-07-21 19:40:01 +00:00
|
|
|
const suites: number[] = [];
|
|
|
|
|
2025-07-22 00:19:59 +00:00
|
|
|
for (let i = 0; i < cipherData.length - 1; i += 2) {
|
|
|
|
const suite = readUInt16BE(cipherData, i);
|
2025-07-21 19:40:01 +00:00
|
|
|
suites.push(suite);
|
|
|
|
}
|
|
|
|
|
|
|
|
return suites;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-07-22 00:19:59 +00:00
|
|
|
* Detect with context for fragmented data
|
2025-07-21 19:40:01 +00:00
|
|
|
*/
|
2025-07-22 00:19:59 +00:00
|
|
|
detectWithContext(
|
|
|
|
buffer: Buffer,
|
2025-07-22 10:35:39 +00:00
|
|
|
_context: { sourceIp?: string; sourcePort?: number; destIp?: string; destPort?: number },
|
2025-07-21 19:40:01 +00:00
|
|
|
options?: IDetectionOptions
|
|
|
|
): IDetectionResult | null {
|
2025-07-22 10:35:39 +00:00
|
|
|
// 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);
|
2025-07-21 19:40:01 +00:00
|
|
|
}
|
|
|
|
}
|