use super::TlsClientHelloInfo;
pub fn try_decode_tls_client_hello(data: &[u8], offset: usize) -> Option<TlsClientHelloInfo> {
let remaining = data.len().checked_sub(offset)?;
if remaining < 6 {
return None;
}
let o = offset;
if data[o] != 0x16 {
return None;
}
let record_len = u16::from_be_bytes([data[o + 3], data[o + 4]]) as usize;
if remaining < 5 + record_len || record_len < 4 {
return None;
}
let hs_offset = o + 5;
if data[hs_offset] != 0x01 {
return None;
}
let hs_len = ((data[hs_offset + 1] as usize) << 16)
| ((data[hs_offset + 2] as usize) << 8)
| (data[hs_offset + 3] as usize);
let ch_start = hs_offset + 4;
let ch_end = ch_start + hs_len;
if ch_end > data.len() {
return None;
}
let mut pos = ch_start;
pos += 2; pos += 32; if pos >= ch_end {
return Some(TlsClientHelloInfo {
sni: None,
header_range: (offset, ch_end.min(data.len())),
});
}
let sid_len = data[pos] as usize;
pos += 1 + sid_len;
if pos + 2 > ch_end {
return Some(TlsClientHelloInfo {
sni: None,
header_range: (offset, ch_end.min(data.len())),
});
}
let cs_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2 + cs_len;
if pos >= ch_end {
return Some(TlsClientHelloInfo {
sni: None,
header_range: (offset, ch_end.min(data.len())),
});
}
let cm_len = data[pos] as usize;
pos += 1 + cm_len;
if pos + 2 > ch_end {
return Some(TlsClientHelloInfo {
sni: None,
header_range: (offset, ch_end.min(data.len())),
});
}
let ext_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
let ext_end = pos + ext_len;
if ext_end > ch_end {
return Some(TlsClientHelloInfo {
sni: None,
header_range: (offset, ch_end.min(data.len())),
});
}
let sni = parse_sni_extension(data, pos, ext_end);
Some(TlsClientHelloInfo {
sni,
header_range: (offset, ch_end.min(data.len())),
})
}
fn parse_sni_extension(data: &[u8], mut pos: usize, ext_end: usize) -> Option<String> {
while pos + 4 <= ext_end {
let ext_type = u16::from_be_bytes([data[pos], data[pos + 1]]);
let ext_data_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if ext_type == 0x0000 {
if pos + 2 > ext_end || pos + 2 > data.len() {
return None;
}
let _sni_list_len = u16::from_be_bytes([data[pos], data[pos + 1]]);
pos += 2;
if pos >= ext_end || pos >= data.len() {
return None;
}
let sni_type = data[pos];
pos += 1;
if sni_type != 0x00 {
return None;
}
if pos + 2 > ext_end || pos + 2 > data.len() {
return None;
}
let name_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + name_len > data.len() {
return None;
}
return String::from_utf8(data[pos..pos + name_len].to_vec()).ok();
}
pos += ext_data_len;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn build_client_hello(sni: &str) -> Vec<u8> {
let sni_bytes = sni.as_bytes();
let sni_ext_data_len = 2 + 1 + 2 + sni_bytes.len(); let sni_ext_len = 4 + sni_ext_data_len; let extensions_len = sni_ext_len;
let session_id_len: usize = 0;
let cipher_suites_len: usize = 2; let compression_len: usize = 1;
let ch_body_len = 2 + 32 + 1 + session_id_len + 2 + cipher_suites_len + 1
+ compression_len
+ 2
+ extensions_len;
let mut pkt = Vec::new();
pkt.push(0x16); pkt.extend_from_slice(&[0x03, 0x01]); let record_len = (4 + ch_body_len) as u16;
pkt.extend_from_slice(&record_len.to_be_bytes());
pkt.push(0x01); let hs_len = ch_body_len as u32;
pkt.push((hs_len >> 16) as u8);
pkt.push((hs_len >> 8) as u8);
pkt.push(hs_len as u8);
pkt.extend_from_slice(&[0x03, 0x03]); pkt.extend_from_slice(&[0u8; 32]);
pkt.push(0);
pkt.extend_from_slice(&(cipher_suites_len as u16).to_be_bytes());
pkt.extend_from_slice(&[0x00, 0x2F]);
pkt.push(compression_len as u8);
pkt.push(0x00);
pkt.extend_from_slice(&(extensions_len as u16).to_be_bytes());
pkt.extend_from_slice(&[0x00, 0x00]); pkt.extend_from_slice(&(sni_ext_data_len as u16).to_be_bytes());
let sni_list_len = (1 + 2 + sni_bytes.len()) as u16;
pkt.extend_from_slice(&sni_list_len.to_be_bytes());
pkt.push(0x00); pkt.extend_from_slice(&(sni_bytes.len() as u16).to_be_bytes());
pkt.extend_from_slice(sni_bytes);
pkt
}
#[test]
fn test_extract_sni() {
let data = build_client_hello("example.com");
let result = try_decode_tls_client_hello(&data, 0).unwrap();
assert_eq!(result.sni.as_deref(), Some("example.com"));
}
#[test]
fn test_extract_sni_github() {
let data = build_client_hello("github.com");
let result = try_decode_tls_client_hello(&data, 0).unwrap();
assert_eq!(result.sni.as_deref(), Some("github.com"));
}
#[test]
fn test_not_handshake() {
let data = [0x17, 0x03, 0x03, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
assert!(try_decode_tls_client_hello(&data, 0).is_none());
}
#[test]
fn test_too_short() {
assert!(try_decode_tls_client_hello(&[0x16, 0x03], 0).is_none());
}
}