mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 19:00:27 +09:00
142 lines
4.0 KiB
Rust
142 lines
4.0 KiB
Rust
/// TLS record-layer parser.
|
|
///
|
|
/// Detects TLS by the 5-byte record header and extracts the SNI hostname
|
|
/// from a ClientHello handshake message. Most importantly, it identifies
|
|
/// TLS Application Data records (type 0x17) so that the rule engine can
|
|
/// skip scanning encrypted payload — the primary source of false positives.
|
|
|
|
/// Recognised TLS record content types.
|
|
const RT_CHANGE_CIPHER: u8 = 0x14;
|
|
const RT_ALERT: u8 = 0x15;
|
|
const RT_HANDSHAKE: u8 = 0x16;
|
|
const RT_APP_DATA: u8 = 0x17;
|
|
|
|
const HS_CLIENT_HELLO: u8 = 0x01;
|
|
const EXT_SNI: u16 = 0x0000;
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct TlsInfo {
|
|
/// True when the first record in `data` is Application Data (encrypted).
|
|
/// The rule engine MUST skip all content scanning for such payloads.
|
|
pub is_app_data: bool,
|
|
/// SNI hostname bytes extracted from a ClientHello, if present.
|
|
pub sni: Vec<u8>,
|
|
}
|
|
|
|
/// Try to parse `data` as a TLS record stream.
|
|
/// Returns `None` when `data` does not look like a TLS record.
|
|
pub fn detect_tls(data: &[u8]) -> Option<TlsInfo> {
|
|
if data.len() < 5 {
|
|
return None;
|
|
}
|
|
|
|
let record_type = data[0];
|
|
if !matches!(record_type, RT_CHANGE_CIPHER | RT_ALERT | RT_HANDSHAKE | RT_APP_DATA) {
|
|
return None;
|
|
}
|
|
|
|
// TLS major version must be 3.
|
|
if data[1] != 3 {
|
|
return None;
|
|
}
|
|
// Minor version 0-4 (SSL3..TLS1.3).
|
|
if data[2] > 4 {
|
|
return None;
|
|
}
|
|
|
|
if record_type == RT_APP_DATA {
|
|
return Some(TlsInfo { is_app_data: true, sni: Vec::new() });
|
|
}
|
|
|
|
let mut info = TlsInfo::default();
|
|
|
|
if record_type == RT_HANDSHAKE {
|
|
info.sni = extract_sni(data);
|
|
}
|
|
|
|
Some(info)
|
|
}
|
|
|
|
/// Extract the SNI hostname from a TLS ClientHello record.
|
|
/// Returns an empty Vec when SNI is absent or the record is malformed.
|
|
fn extract_sni(data: &[u8]) -> Vec<u8> {
|
|
// TLS record header: type(1) + version(2) + length(2) = 5 bytes
|
|
// Handshake header: type(1) + length(3) = 4 bytes
|
|
// ClientHello: version(2) + random(32) + session_id_len(1) + ...
|
|
if data.len() < 5 + 4 + 2 + 32 + 1 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let hs = &data[5..]; // start of Handshake layer
|
|
if hs[0] != HS_CLIENT_HELLO {
|
|
return Vec::new();
|
|
}
|
|
|
|
let hs_len = u24_be(&hs[1..4]) as usize;
|
|
if hs.len() < 4 + hs_len {
|
|
return Vec::new();
|
|
}
|
|
|
|
let ch = &hs[4..4 + hs_len]; // ClientHello body
|
|
// version(2) + random(32) = 34 bytes minimum
|
|
if ch.len() < 34 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let mut pos = 34usize; // skip version + random
|
|
|
|
// Session ID
|
|
if pos >= ch.len() { return Vec::new(); }
|
|
let sid_len = ch[pos] as usize;
|
|
pos += 1 + sid_len;
|
|
|
|
// Cipher suites
|
|
if pos + 2 > ch.len() { return Vec::new(); }
|
|
let cs_len = u16_be(&ch[pos..]) as usize;
|
|
pos += 2 + cs_len;
|
|
|
|
// Compression methods
|
|
if pos >= ch.len() { return Vec::new(); }
|
|
let cm_len = ch[pos] as usize;
|
|
pos += 1 + cm_len;
|
|
|
|
// Extensions
|
|
if pos + 2 > ch.len() { return Vec::new(); }
|
|
let ext_total = u16_be(&ch[pos..]) as usize;
|
|
pos += 2;
|
|
let ext_end = pos + ext_total;
|
|
if ext_end > ch.len() { return Vec::new(); }
|
|
|
|
while pos + 4 <= ext_end {
|
|
let ext_type = u16_be(&ch[pos..]);
|
|
let ext_len = u16_be(&ch[pos + 2..]) as usize;
|
|
pos += 4;
|
|
if pos + ext_len > ext_end { break; }
|
|
|
|
if ext_type == EXT_SNI && ext_len >= 5 {
|
|
// SNI list: list_len(2) + name_type(1) + name_len(2) + name
|
|
let list_len = u16_be(&ch[pos..]) as usize;
|
|
if list_len + 2 > ext_len { break; }
|
|
let name_type = ch[pos + 2];
|
|
let name_len = u16_be(&ch[pos + 3..]) as usize;
|
|
if name_type == 0 && pos + 5 + name_len <= ext_end {
|
|
return ch[pos + 5..pos + 5 + name_len].to_vec();
|
|
}
|
|
}
|
|
|
|
pos += ext_len;
|
|
}
|
|
|
|
Vec::new()
|
|
}
|
|
|
|
#[inline]
|
|
fn u16_be(b: &[u8]) -> u16 {
|
|
u16::from_be_bytes([b[0], b[1]])
|
|
}
|
|
|
|
#[inline]
|
|
fn u24_be(b: &[u8]) -> u32 {
|
|
(b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32
|
|
}
|