mirror of
https://github.com/domcyrus/rustnet.git
synced 2026-01-05 21:40:01 -06:00
improve TLS parsing
This commit is contained in:
@@ -24,10 +24,10 @@ impl Default for CaptureConfig {
|
||||
Self {
|
||||
interface: None,
|
||||
promiscuous: true,
|
||||
snaplen: 200, // Limit packet size to keep more in buffer (like Sniffnet)
|
||||
buffer_size: 2_000_000, // 2MB buffer (same as Sniffnet)
|
||||
timeout_ms: 150, // 150ms timeout for UI responsiveness (like Sniffnet)
|
||||
filter: None, // Start without filter to ensure we see packets
|
||||
snaplen: 1514, // Limit packet size to keep more in buffer
|
||||
buffer_size: 20_000_000, // 20MB buffer
|
||||
timeout_ms: 150, // 150ms timeout for UI responsiveness
|
||||
filter: None, // Start without filter to ensure we see packets
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, S
|
||||
.snaplen(config.snaplen)
|
||||
.buffer_size(config.buffer_size)
|
||||
.timeout(config.timeout_ms)
|
||||
.immediate_mode(true); // Parse packets ASAP (like Sniffnet)
|
||||
.immediate_mode(true); // Parse packets ASAP
|
||||
|
||||
// Open the capture
|
||||
let mut cap = cap.open()?;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::network::types::{TlsInfo, TlsVersion};
|
||||
use log::debug;
|
||||
|
||||
pub fn is_tls_handshake(payload: &[u8]) -> bool {
|
||||
if payload.len() < 6 {
|
||||
if payload.len() < 5 {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,57 +16,87 @@ pub fn is_tls_handshake(payload: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
pub fn analyze_tls(payload: &[u8]) -> Option<TlsInfo> {
|
||||
if !is_tls_handshake(payload) || payload.len() < 9 {
|
||||
// Need at least 5 bytes for the TLS record header
|
||||
if payload.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut info = TlsInfo::new();
|
||||
|
||||
// Record layer version (may be legacy for TLS 1.3)
|
||||
// Check content type
|
||||
let content_type = payload[0];
|
||||
|
||||
if content_type != 0x16 {
|
||||
// Not a handshake record - still extract version
|
||||
let record_version = version_from_bytes(payload[1], payload[2]);
|
||||
info.version = record_version;
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
// Record layer version
|
||||
let record_version = version_from_bytes(payload[1], payload[2]);
|
||||
info.version = record_version;
|
||||
|
||||
// Get record length
|
||||
let record_length = u16::from_be_bytes([payload[3], payload[4]]) as usize;
|
||||
|
||||
// Validate record length
|
||||
if payload.len() < 5 + record_length {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip TLS record header (5 bytes)
|
||||
let handshake_data = &payload[5..5 + record_length];
|
||||
|
||||
if handshake_data.len() < 4 {
|
||||
// Sanity check
|
||||
if record_length > 16384 + 2048 {
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
// Calculate available data (handle fragmentation gracefully)
|
||||
let available_data = (payload.len() - 5).min(record_length);
|
||||
|
||||
if available_data < 4 {
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
// Skip TLS record header (5 bytes)
|
||||
let handshake_data = &payload[5..5 + available_data];
|
||||
|
||||
let handshake_type = handshake_data[0];
|
||||
|
||||
// Quick validation
|
||||
if !matches!(handshake_type, 0x00..=0x18 | 0xfe) {
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
let handshake_length =
|
||||
u32::from_be_bytes([0, handshake_data[1], handshake_data[2], handshake_data[3]]) as usize;
|
||||
|
||||
// Validate handshake length
|
||||
if handshake_data.len() < 4 + handshake_length {
|
||||
// Sanity check
|
||||
if handshake_length > 16384 {
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
// Calculate how much handshake data we actually have
|
||||
let handshake_available = (handshake_data.len() - 4).min(handshake_length);
|
||||
|
||||
if handshake_available == 0 {
|
||||
return Some(info);
|
||||
}
|
||||
|
||||
match handshake_type {
|
||||
0x01 => {
|
||||
// Client Hello
|
||||
// Client Hello - this is where SNI and ALPN are
|
||||
parse_client_hello(
|
||||
&handshake_data[4..4 + handshake_length],
|
||||
&handshake_data[4..4 + handshake_available],
|
||||
&mut info,
|
||||
record_version,
|
||||
);
|
||||
}
|
||||
0x02 => {
|
||||
// Server Hello
|
||||
parse_server_hello(&handshake_data[4..4 + handshake_length], &mut info);
|
||||
parse_server_hello(&handshake_data[4..4 + handshake_available], &mut info);
|
||||
}
|
||||
0x0b => {
|
||||
// Certificate
|
||||
parse_certificate(&handshake_data[4..4 + handshake_length], &mut info);
|
||||
_ => {
|
||||
// Other handshake types we don't parse
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if info.sni.is_some() || !info.alpn.is_empty() {
|
||||
debug!("TLS: Found SNI={:?}, ALPN={:?}", info.sni, info.alpn);
|
||||
}
|
||||
|
||||
Some(info)
|
||||
@@ -81,72 +112,84 @@ fn version_from_bytes(major: u8, minor: u8) -> Option<TlsVersion> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_client_hello(
|
||||
data: &[u8],
|
||||
info: &mut TlsInfo,
|
||||
record_version: Option<TlsVersion>,
|
||||
) -> Option<()> {
|
||||
if data.len() < 34 {
|
||||
return None;
|
||||
fn parse_client_hello(data: &[u8], info: &mut TlsInfo, record_version: Option<TlsVersion>) {
|
||||
// Need at least 2 bytes for version
|
||||
if data.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Client version (legacy, real version might be in supported_versions extension)
|
||||
// Client version
|
||||
let client_version = version_from_bytes(data[0], data[1]);
|
||||
info.version = client_version.or(record_version);
|
||||
|
||||
// Need at least 34 bytes for version + random
|
||||
if data.len() < 34 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip random (32 bytes)
|
||||
let mut offset = 34;
|
||||
|
||||
// Session ID
|
||||
// Session ID - be lenient with bounds
|
||||
if offset >= data.len() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
let session_id_len = data[offset] as usize;
|
||||
offset += 1 + session_id_len;
|
||||
|
||||
if offset >= data.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cipher suites
|
||||
if offset + 2 > data.len() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
let cipher_suites_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||
offset += 2 + cipher_suites_len;
|
||||
|
||||
if offset >= data.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compression methods
|
||||
if offset >= data.len() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
let compression_len = data[offset] as usize;
|
||||
offset += 1 + compression_len;
|
||||
|
||||
// Extensions
|
||||
if offset >= data.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extensions - this is what we really want
|
||||
if offset + 2 > data.len() {
|
||||
return Some(());
|
||||
return;
|
||||
}
|
||||
let extensions_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||
offset += 2;
|
||||
|
||||
if offset + extensions_len > data.len() {
|
||||
return Some(());
|
||||
// Parse whatever extension data we have
|
||||
if offset < data.len() {
|
||||
let available_ext_data = &data[offset..data.len().min(offset + extensions_len)];
|
||||
if !available_ext_data.is_empty() {
|
||||
parse_extensions(available_ext_data, info, true);
|
||||
}
|
||||
}
|
||||
|
||||
parse_extensions(&data[offset..offset + extensions_len], info, true);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn parse_server_hello(data: &[u8], info: &mut TlsInfo) -> Option<()> {
|
||||
if data.len() < 35 {
|
||||
return None;
|
||||
fn parse_server_hello(data: &[u8], info: &mut TlsInfo) {
|
||||
if data.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Server version
|
||||
let server_version = version_from_bytes(data[0], data[1]);
|
||||
info.version = server_version;
|
||||
|
||||
// For TLS 1.3, check if this is really TLS 1.2 (0x0303) which means we need to look at extensions
|
||||
if let Some(TlsVersion::Tls12) = server_version {
|
||||
// Will be updated by supported_versions extension if present
|
||||
info.version = server_version;
|
||||
} else {
|
||||
info.version = server_version;
|
||||
if data.len() < 34 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip random (32 bytes)
|
||||
@@ -154,148 +197,214 @@ fn parse_server_hello(data: &[u8], info: &mut TlsInfo) -> Option<()> {
|
||||
|
||||
// Session ID length
|
||||
if offset >= data.len() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
let session_id_len = data[offset] as usize;
|
||||
offset += 1 + session_id_len;
|
||||
|
||||
if offset >= data.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cipher suite (2 bytes)
|
||||
if offset + 2 > data.len() {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
info.cipher_suite = Some(u16::from_be_bytes([data[offset], data[offset + 1]]));
|
||||
let cipher = u16::from_be_bytes([data[offset], data[offset + 1]]);
|
||||
info.cipher_suite = Some(cipher);
|
||||
offset += 2;
|
||||
|
||||
// Compression method (1 byte)
|
||||
if offset >= data.len() {
|
||||
return;
|
||||
}
|
||||
offset += 1;
|
||||
|
||||
// Extensions (if present)
|
||||
if offset + 2 <= data.len() {
|
||||
let extensions_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||
offset += 2;
|
||||
|
||||
if offset + extensions_len <= data.len() {
|
||||
parse_extensions(&data[offset..offset + extensions_len], info, false);
|
||||
}
|
||||
// Extensions (optional)
|
||||
if offset + 2 > data.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
Some(())
|
||||
let extensions_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
|
||||
offset += 2;
|
||||
|
||||
// Parse whatever extension data we have
|
||||
if offset < data.len() {
|
||||
let available_ext_data = &data[offset..data.len().min(offset + extensions_len)];
|
||||
if !available_ext_data.is_empty() {
|
||||
parse_extensions(available_ext_data, info, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extensions(data: &[u8], info: &mut TlsInfo, is_client_hello: bool) -> Option<()> {
|
||||
fn parse_extensions(data: &[u8], info: &mut TlsInfo, is_client_hello: bool) {
|
||||
let mut offset = 0;
|
||||
|
||||
while offset + 4 <= data.len() {
|
||||
let ext_type = u16::from_be_bytes([data[offset], data[offset + 1]]);
|
||||
let ext_len = u16::from_be_bytes([data[offset + 2], data[offset + 3]]) as usize;
|
||||
|
||||
if offset + 4 + ext_len > data.len() {
|
||||
// Calculate how much extension data we actually have
|
||||
let available_ext_len = data.len().saturating_sub(offset + 4);
|
||||
let ext_data_len = ext_len.min(available_ext_len);
|
||||
|
||||
if ext_data_len > 0 {
|
||||
let ext_data = &data[offset + 4..offset + 4 + ext_data_len];
|
||||
|
||||
match ext_type {
|
||||
0x0000 if is_client_hello => {
|
||||
// SNI (Server Name Indication)
|
||||
if let Some(sni) = parse_sni_extension_resilient(ext_data) {
|
||||
info.sni = Some(sni);
|
||||
}
|
||||
}
|
||||
0x0010 => {
|
||||
// ALPN (Application-Layer Protocol Negotiation)
|
||||
if let Some(alpn) = parse_alpn_extension_resilient(ext_data) {
|
||||
if !alpn.is_empty() {
|
||||
info.alpn = alpn;
|
||||
}
|
||||
}
|
||||
}
|
||||
0x002b => {
|
||||
// Supported Versions
|
||||
if let Some(version) =
|
||||
parse_supported_versions_resilient(ext_data, is_client_hello)
|
||||
{
|
||||
info.version = Some(version);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Skip unknown extensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to next extension (use declared length, not actual)
|
||||
offset += 4 + ext_len;
|
||||
|
||||
// But stop if we've gone past available data
|
||||
if offset > data.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let ext_data = &data[offset + 4..offset + 4 + ext_len];
|
||||
|
||||
match ext_type {
|
||||
0x0000 => {
|
||||
// SNI (Server Name Indication)
|
||||
if is_client_hello {
|
||||
info.sni = parse_sni_extension(ext_data);
|
||||
}
|
||||
}
|
||||
0x0010 => {
|
||||
// ALPN (Application-Layer Protocol Negotiation)
|
||||
info.alpn = parse_alpn_extension(ext_data);
|
||||
}
|
||||
0x002b => {
|
||||
// Supported Versions
|
||||
if let Some(version) = parse_supported_versions_extension(ext_data, is_client_hello)
|
||||
{
|
||||
info.version = Some(version);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
offset += 4 + ext_len;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn parse_sni_extension(data: &[u8]) -> Option<String> {
|
||||
fn parse_sni_extension_resilient(data: &[u8]) -> Option<String> {
|
||||
if data.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Server name list length (2 bytes)
|
||||
let list_len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||
if data.len() < 2 + list_len {
|
||||
let _list_len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||
|
||||
// Check name type (should be 0x00 for hostname)
|
||||
if data[2] != 0x00 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 2;
|
||||
// Name length
|
||||
let name_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
||||
|
||||
while offset + 3 <= data.len() {
|
||||
let name_type = data[offset];
|
||||
let name_len = u16::from_be_bytes([data[offset + 1], data[offset + 2]]) as usize;
|
||||
// Extract whatever hostname data we have
|
||||
let available_len = data.len().saturating_sub(5);
|
||||
let actual_len = name_len.min(available_len);
|
||||
|
||||
if name_type == 0x00 {
|
||||
// host_name
|
||||
if offset + 3 + name_len <= data.len() {
|
||||
let hostname_bytes = &data[offset + 3..offset + 3 + name_len];
|
||||
if let Ok(hostname) = std::str::from_utf8(hostname_bytes) {
|
||||
return Some(hostname.to_string());
|
||||
}
|
||||
if actual_len > 0 {
|
||||
let hostname_bytes = &data[5..5 + actual_len];
|
||||
|
||||
// Try to parse as UTF-8
|
||||
if let Ok(hostname) = std::str::from_utf8(hostname_bytes) {
|
||||
// Basic validation - at least check it looks like a hostname
|
||||
if hostname.chars().all(|c| c.is_ascii_graphic() || c == '.') {
|
||||
let result = if actual_len < name_len {
|
||||
format!("{}[PARTIAL]", hostname)
|
||||
} else {
|
||||
hostname.to_string()
|
||||
};
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
offset += 3 + name_len;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_alpn_extension(data: &[u8]) -> Vec<String> {
|
||||
fn parse_alpn_extension_resilient(data: &[u8]) -> Option<Vec<String>> {
|
||||
if data.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut protocols = Vec::new();
|
||||
|
||||
if data.len() < 2 {
|
||||
return protocols;
|
||||
}
|
||||
|
||||
// ALPN extension length
|
||||
// ALPN list length
|
||||
let alpn_len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||
if data.len() < 2 + alpn_len {
|
||||
return protocols;
|
||||
}
|
||||
|
||||
let mut offset = 2;
|
||||
let list_end = 2 + alpn_len.min(data.len() - 2);
|
||||
|
||||
while offset < data.len() {
|
||||
while offset < list_end && offset < data.len() {
|
||||
let proto_len = data[offset] as usize;
|
||||
if offset + 1 + proto_len <= data.len() {
|
||||
if let Ok(proto) = std::str::from_utf8(&data[offset + 1..offset + 1 + proto_len]) {
|
||||
protocols.push(proto.to_string());
|
||||
offset += 1;
|
||||
|
||||
let available_len = list_end
|
||||
.saturating_sub(offset)
|
||||
.min(data.len().saturating_sub(offset));
|
||||
let actual_len = proto_len.min(available_len);
|
||||
|
||||
if actual_len > 0 {
|
||||
if let Ok(proto) = std::str::from_utf8(&data[offset..offset + actual_len]) {
|
||||
if actual_len < proto_len {
|
||||
protocols.push(format!("{}[PARTIAL]", proto));
|
||||
} else {
|
||||
protocols.push(proto.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
offset += 1 + proto_len;
|
||||
|
||||
offset += proto_len;
|
||||
|
||||
if offset >= data.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protocols
|
||||
if protocols.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(protocols)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_supported_versions_extension(data: &[u8], is_client_hello: bool) -> Option<TlsVersion> {
|
||||
fn parse_supported_versions_resilient(data: &[u8], is_client_hello: bool) -> Option<TlsVersion> {
|
||||
if is_client_hello {
|
||||
// Client sends a list of supported versions
|
||||
if data.len() < 1 {
|
||||
return None;
|
||||
}
|
||||
let list_len = data[0] as usize;
|
||||
if data.len() < 1 + list_len || list_len < 2 {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Return the first (highest priority) version
|
||||
version_from_bytes(data[1], data[2])
|
||||
let list_len = data[0] as usize;
|
||||
let mut offset = 1;
|
||||
let mut best_version: Option<TlsVersion> = None;
|
||||
|
||||
while offset + 1 < data.len() && offset < 1 + list_len {
|
||||
if let Some(version) = version_from_bytes(data[offset], data[offset + 1]) {
|
||||
best_version = match (best_version, version) {
|
||||
(None, v) => Some(v),
|
||||
(Some(v1), v2) => {
|
||||
// Simple comparison - return the higher version
|
||||
if version_to_priority(v2) > version_to_priority(v1) {
|
||||
Some(v2)
|
||||
} else {
|
||||
Some(v1)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
best_version
|
||||
} else {
|
||||
// Server sends a single selected version
|
||||
if data.len() < 2 {
|
||||
@@ -305,65 +414,13 @@ fn parse_supported_versions_extension(data: &[u8], is_client_hello: bool) -> Opt
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_certificate(data: &[u8], info: &mut TlsInfo) -> Option<()> {
|
||||
if data.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Certificate list length (3 bytes)
|
||||
let cert_list_len = u32::from_be_bytes([0, data[0], data[1], data[2]]) as usize;
|
||||
if data.len() < 3 + cert_list_len {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut offset = 3;
|
||||
|
||||
// Parse only the first certificate (server's certificate)
|
||||
if offset + 3 <= data.len() {
|
||||
let cert_len =
|
||||
u32::from_be_bytes([0, data[offset], data[offset + 1], data[offset + 2]]) as usize;
|
||||
offset += 3;
|
||||
|
||||
if offset + cert_len <= data.len() {
|
||||
let cert_data = &data[offset..offset + cert_len];
|
||||
parse_x509_certificate(cert_data, info);
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn parse_x509_certificate(cert_data: &[u8], info: &mut TlsInfo) {
|
||||
// This is a simplified X.509 parser that looks for CN and SAN
|
||||
// In production, you'd want to use a proper X.509 parsing library like x509-parser
|
||||
|
||||
// Look for common patterns in certificates
|
||||
// CN is typically preceded by the OID 2.5.4.3 (0x55, 0x04, 0x03)
|
||||
// SAN extension has OID 2.5.29.17 (0x55, 0x1D, 0x11)
|
||||
|
||||
// Search for Common Name
|
||||
let cn_oid = [0x55, 0x04, 0x03];
|
||||
if let Some(pos) = cert_data.windows(3).position(|w| w == cn_oid) {
|
||||
if pos + 5 < cert_data.len() {
|
||||
// Skip OID and tag
|
||||
let len = cert_data[pos + 4] as usize;
|
||||
if pos + 5 + len <= cert_data.len() {
|
||||
if let Ok(cn) = std::str::from_utf8(&cert_data[pos + 5..pos + 5 + len]) {
|
||||
info.certificate_cn = Some(cn.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for Subject Alternative Names
|
||||
// This is a simplified approach - real implementation would need proper ASN.1 parsing
|
||||
let san_oid = [0x55, 0x1D, 0x11];
|
||||
if let Some(pos) = cert_data.windows(3).position(|w| w == san_oid) {
|
||||
// SAN parsing is complex due to ASN.1 encoding
|
||||
// In production, use a proper X.509 library
|
||||
// This is just a placeholder
|
||||
info.certificate_san
|
||||
.push("Use x509-parser for proper SAN extraction".to_string());
|
||||
fn version_to_priority(version: TlsVersion) -> u8 {
|
||||
match version {
|
||||
TlsVersion::Ssl3 => 0,
|
||||
TlsVersion::Tls10 => 1,
|
||||
TlsVersion::Tls11 => 2,
|
||||
TlsVersion::Tls12 => 3,
|
||||
TlsVersion::Tls13 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,23 +429,34 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tls_version_parsing() {
|
||||
assert_eq!(version_from_bytes(0x03, 0x01), Some(TlsVersion::Tls10));
|
||||
assert_eq!(version_from_bytes(0x03, 0x02), Some(TlsVersion::Tls11));
|
||||
assert_eq!(version_from_bytes(0x03, 0x03), Some(TlsVersion::Tls12));
|
||||
assert_eq!(version_from_bytes(0x03, 0x04), Some(TlsVersion::Tls13));
|
||||
assert_eq!(version_from_bytes(0x02, 0x00), None);
|
||||
fn test_partial_sni_extraction() {
|
||||
// Simulate a truncated SNI extension
|
||||
let partial_sni = vec![
|
||||
0x00, 0x10, // List length: 16
|
||||
0x00, // Name type: host_name
|
||||
0x00, 0x0d, // Name length: 13
|
||||
b'e', b'x', b'a', b'm', b'p', // Only 5 bytes of "example.com"
|
||||
];
|
||||
|
||||
let result = parse_sni_extension_resilient(&partial_sni);
|
||||
assert!(result.is_some());
|
||||
let sni = result.unwrap();
|
||||
assert!(sni.starts_with("examp"));
|
||||
assert!(sni.contains("PARTIAL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_tls_handshake() {
|
||||
let valid_handshake = [0x16, 0x03, 0x03, 0x00, 0x50, 0x01];
|
||||
assert!(is_tls_handshake(&valid_handshake));
|
||||
fn test_partial_alpn_extraction() {
|
||||
// Simulate a truncated ALPN extension
|
||||
let partial_alpn = vec![
|
||||
0x00, 0x0e, // List length: 14
|
||||
0x08, b'h', b't', b't', b'p', // Only partial "http/1.1"
|
||||
];
|
||||
|
||||
let invalid_type = [0x17, 0x03, 0x03, 0x00, 0x50, 0x01];
|
||||
assert!(!is_tls_handshake(&invalid_type));
|
||||
|
||||
let too_short = [0x16, 0x03];
|
||||
assert!(!is_tls_handshake(&too_short));
|
||||
let result = parse_alpn_extension_resilient(&partial_alpn);
|
||||
assert!(result.is_some());
|
||||
let protocols = result.unwrap();
|
||||
assert!(!protocols.is_empty());
|
||||
assert!(protocols[0].contains("PARTIAL"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ fn merge_dpi_info(conn: &mut Connection, dpi_result: &DpiResult) {
|
||||
(ApplicationProtocol::Quic(old_info), ApplicationProtocol::Quic(new_info)) => {
|
||||
// Update only specific fields for QUIC
|
||||
old_info.connection_state = new_info.connection_state.clone();
|
||||
old_info.packet_type = new_info.packet_type.clone();
|
||||
if old_info.connection_id_hex.is_none() {
|
||||
old_info.connection_id_hex = new_info.connection_id_hex.clone();
|
||||
}
|
||||
@@ -184,6 +185,35 @@ fn merge_dpi_info(conn: &mut Connection, dpi_result: &DpiResult) {
|
||||
(_, ApplicationProtocol::Quic(_)) => {
|
||||
warn!("QUIC DPI info not found in existing connection");
|
||||
}
|
||||
(ApplicationProtocol::Dns(old_info), ApplicationProtocol::Dns(new_info)) => {
|
||||
// Merge DNS info
|
||||
if new_info.query_name.is_some() {
|
||||
old_info.query_name = new_info.query_name.clone();
|
||||
}
|
||||
if new_info.query_type.is_some() {
|
||||
old_info.query_type = new_info.query_type.clone();
|
||||
}
|
||||
old_info.response_ips.extend(new_info.response_ips.clone());
|
||||
old_info.is_response = new_info.is_response;
|
||||
}
|
||||
(ApplicationProtocol::Https(old_info), ApplicationProtocol::Https(new_info)) => {
|
||||
// Replace the entire HTTPS info
|
||||
if new_info.version.is_some() {
|
||||
old_info.version = new_info.version.clone();
|
||||
}
|
||||
if new_info.sni.is_some() {
|
||||
old_info.sni = new_info.sni.clone();
|
||||
}
|
||||
if !new_info.alpn.is_empty() {
|
||||
old_info.alpn = new_info.alpn.clone();
|
||||
}
|
||||
if new_info.cipher_suite.is_some() {
|
||||
old_info.cipher_suite = new_info.cipher_suite;
|
||||
}
|
||||
}
|
||||
(ApplicationProtocol::Ssh, ApplicationProtocol::Ssh) => {
|
||||
// No additional info to merge for SSH
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
"Unhandled application protocol in DPI merge: {:?}",
|
||||
|
||||
@@ -51,8 +51,8 @@ impl std::fmt::Display for ApplicationProtocol {
|
||||
if let Some(version) = &info.version_string {
|
||||
parts.push(&version);
|
||||
}
|
||||
let packet_type = info.packet_type.to_string();
|
||||
parts.push(&packet_type);
|
||||
let connection_state = info.connection_state.to_string();
|
||||
parts.push(&connection_state);
|
||||
if let Some(connection_id) = &info.connection_id_hex {
|
||||
parts.push(&connection_id);
|
||||
}
|
||||
@@ -134,10 +134,7 @@ pub struct TlsInfo {
|
||||
pub version: Option<TlsVersion>,
|
||||
pub sni: Option<String>,
|
||||
pub alpn: Vec<String>,
|
||||
#[allow(dead_code)]
|
||||
pub cipher_suite: Option<u16>,
|
||||
pub certificate_cn: Option<String>,
|
||||
pub certificate_san: Vec<String>,
|
||||
}
|
||||
|
||||
impl TlsInfo {
|
||||
@@ -147,8 +144,6 @@ impl TlsInfo {
|
||||
sni: None,
|
||||
alpn: Vec::new(),
|
||||
cipher_suite: None,
|
||||
certificate_cn: None,
|
||||
certificate_san: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/ui.rs
49
src/ui.rs
@@ -543,6 +543,24 @@ fn draw_connection_details(
|
||||
Span::raw(format!("{:?}", version)),
|
||||
]));
|
||||
}
|
||||
if let Some(sni) = &info.sni {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" SNI: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(sni.clone()),
|
||||
]));
|
||||
}
|
||||
if !info.alpn.is_empty() {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" ALPN: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(info.alpn.join(", ")),
|
||||
]));
|
||||
}
|
||||
if let Some(cipher) = info.cipher_suite {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" Cipher Suite: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(format!("0x{:04X}", cipher)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
crate::network::types::ApplicationProtocol::Dns(info) => {
|
||||
if let Some(query_type) = &info.query_type {
|
||||
@@ -551,6 +569,37 @@ fn draw_connection_details(
|
||||
Span::raw(format!("{:?}", query_type)),
|
||||
]));
|
||||
}
|
||||
if !info.response_ips.is_empty() {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" DNS Response IPs: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(format!("{:?}", info.response_ips)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
crate::network::types::ApplicationProtocol::Quic(info) => {
|
||||
if let Some(version) = info.version_string.as_ref() {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" QUIC Version: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(version.clone()),
|
||||
]));
|
||||
}
|
||||
if let Some(connection_id) = &info.connection_id_hex {
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" Connection ID: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(connection_id.clone()),
|
||||
]));
|
||||
}
|
||||
|
||||
let packet_type = info.packet_type.to_string();
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" Packet Type: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(packet_type),
|
||||
]));
|
||||
let connection_state = info.connection_state.to_string();
|
||||
details_text.push(Line::from(vec![
|
||||
Span::styled(" Connection State: ", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(connection_state),
|
||||
]));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user