use std::collections::HashMap;
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::time::timeout;
#[derive(Debug, Clone)]
pub struct LlmnrCapture {
pub protocol: String,
pub source_ip: String,
pub queried_name: String,
}
pub async fn capture_llmnr(listen_secs: u64) -> Vec<LlmnrCapture> {
let mut captures = Vec::new();
let socket = match UdpSocket::bind("0.0.0.0:5355").await {
Ok(s) => s,
Err(e) => {
eprintln!("[!] LLMNR: could not bind UDP 5355: {}", e);
return captures;
}
};
if let Err(e) = socket.join_multicast_v4(
"224.0.0.252".parse().unwrap(),
"0.0.0.0".parse().unwrap(),
) {
eprintln!("[!] LLMNR: could not join multicast group: {}", e);
}
eprintln!("[*] Passive: listening for LLMNR on UDP 5355 for {}s", listen_secs);
let mut buf = [0u8; 512];
let deadline = tokio::time::Instant::now() + Duration::from_secs(listen_secs);
let mut seen: HashMap<String, bool> = HashMap::new();
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
break;
}
match timeout(remaining, socket.recv_from(&mut buf)).await {
Ok(Ok((n, peer))) => {
if let Some(name) = parse_llmnr_query(&buf[..n]) {
let key = format!("{}:{}", peer.ip(), name);
if seen.insert(key, true).is_none() {
captures.push(LlmnrCapture {
protocol: "LLMNR".into(),
source_ip: peer.ip().to_string(),
queried_name: name,
});
}
}
}
Ok(Err(e)) => {
eprintln!("[!] LLMNR recv error: {}", e);
break;
}
Err(_) => break, }
}
captures
}
pub async fn capture_nbtns(listen_secs: u64) -> Vec<LlmnrCapture> {
let mut captures = Vec::new();
let socket = match UdpSocket::bind("0.0.0.0:137").await {
Ok(s) => s,
Err(e) => {
eprintln!("[!] NBT-NS: could not bind UDP 137 (may need root): {}", e);
return captures;
}
};
eprintln!("[*] Passive: listening for NBT-NS on UDP 137 for {}s", listen_secs);
let mut buf = [0u8; 512];
let deadline = tokio::time::Instant::now() + Duration::from_secs(listen_secs);
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
break;
}
match timeout(remaining, socket.recv_from(&mut buf)).await {
Ok(Ok((n, peer))) => {
if let Some(name) = parse_nbtns_query(&buf[..n]) {
captures.push(LlmnrCapture {
protocol: "NBT-NS".into(),
source_ip: peer.ip().to_string(),
queried_name: name,
});
}
}
Ok(Err(e)) => {
eprintln!("[!] NBT-NS recv error: {}", e);
break;
}
Err(_) => break,
}
}
captures
}
fn parse_llmnr_query(data: &[u8]) -> Option<String> {
if data.len() < 12 {
return None;
}
let flags = u16::from_be_bytes([data[2], data[3]]);
if flags & 0x8000 != 0 {
return None; }
let qdcount = u16::from_be_bytes([data[4], data[5]]);
if qdcount == 0 {
return None;
}
parse_dns_name(data, 12)
}
fn parse_nbtns_query(data: &[u8]) -> Option<String> {
if data.len() < 12 {
return None;
}
let flags = u16::from_be_bytes([data[2], data[3]]);
if flags & 0x8000 != 0 {
return None;
}
if data.len() < 13 + 33 {
return None;
}
let raw_len = data[12] as usize;
if raw_len != 32 || data.len() < 13 + raw_len {
return None;
}
let encoded = &data[13..13 + raw_len];
let name = decode_nbt_name(encoded);
Some(name.trim().to_string())
}
fn parse_dns_name(data: &[u8], mut offset: usize) -> Option<String> {
let mut labels = Vec::new();
let mut jumped = false;
let mut safety = 0;
loop {
if safety > 20 || offset >= data.len() {
break;
}
safety += 1;
let len = data[offset] as usize;
if len == 0 {
break;
}
if len & 0xC0 == 0xC0 {
if offset + 1 >= data.len() {
break;
}
let ptr = ((len & 0x3F) << 8) | data[offset + 1] as usize;
offset = ptr;
jumped = true;
continue;
}
offset += 1;
if offset + len > data.len() {
break;
}
labels.push(String::from_utf8_lossy(&data[offset..offset + len]).into_owned());
offset += len;
let _ = jumped; }
if labels.is_empty() {
None
} else {
Some(labels.join("."))
}
}
fn decode_nbt_name(encoded: &[u8]) -> String {
let mut name = String::new();
for chunk in encoded.chunks(2) {
if chunk.len() < 2 {
break;
}
let c = (((chunk[0] as u8 - b'A') << 4) | (chunk[1] as u8 - b'A')) as char;
if c.is_ascii() {
name.push(c);
}
}
name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llmnr_capture_creation() {
let capture = LlmnrCapture {
protocol: "LLMNR".to_string(),
source_ip: "192.168.1.100".to_string(),
queried_name: "SERVER01".to_string(),
};
assert_eq!(capture.protocol, "LLMNR");
assert_eq!(capture.source_ip, "192.168.1.100");
assert_eq!(capture.queried_name, "SERVER01");
}
#[test]
fn test_nbtns_capture_creation() {
let capture = LlmnrCapture {
protocol: "NBT-NS".to_string(),
source_ip: "192.168.1.50".to_string(),
queried_name: "WORKSTATION".to_string(),
};
assert_eq!(capture.protocol, "NBT-NS");
assert_eq!(capture.source_ip, "192.168.1.50");
}
#[test]
fn test_llmnr_capture_cloneable() {
let capture = LlmnrCapture {
protocol: "LLMNR".to_string(),
source_ip: "10.0.0.1".to_string(),
queried_name: "test".to_string(),
};
let cloned = capture.clone();
assert_eq!(capture.source_ip, cloned.source_ip);
}
#[test]
fn test_parse_llmnr_query_valid_packet() {
let mut packet = vec![
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
packet.extend_from_slice(&[4, b't', b'e', b's', b't', 0]);
let result = parse_llmnr_query(&packet);
assert_eq!(result, Some("test".to_string()), "Should parse 'test' domain");
}
#[test]
fn test_parse_llmnr_query_response_ignored() {
let mut packet = vec![
0x00, 0x01,
0x80, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
];
packet.extend_from_slice(&[4, b't', b'e', b's', b't', 0]);
let result = parse_llmnr_query(&packet);
assert_eq!(result, None, "Should ignore response packets");
}
#[test]
fn test_parse_llmnr_query_empty_questions() {
let packet = vec![
0x00, 0x01,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
];
let result = parse_llmnr_query(&packet);
assert_eq!(result, None, "Should return None for zero questions");
}
#[test]
fn test_parse_llmnr_query_too_short() {
let packet = vec![0x00, 0x01, 0x00, 0x00];
let result = parse_llmnr_query(&packet);
assert_eq!(result, None, "Should reject packets < 12 bytes");
}
#[test]
fn test_parse_llmnr_query_empty() {
let result = parse_llmnr_query(&[]);
assert_eq!(result, None, "Should handle empty packet");
}
#[test]
fn test_parse_llmnr_query_multipart_domain() {
let mut packet = vec![
0x00, 0x01,
0x00, 0x00,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
];
packet.extend_from_slice(&[6, b's', b'e', b'r', b'v', b'e', b'r']);
packet.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e']);
packet.extend_from_slice(&[3, b'c', b'o', b'm', 0]);
let result = parse_llmnr_query(&packet);
assert_eq!(result, Some("server.example.com".to_string()), "Should parse multi-label domain");
}
#[test]
fn test_parse_nbtns_query_valid_packet() {
let mut packet = vec![
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x20, ];
packet.extend_from_slice(&[b'A'; 33]);
assert_eq!(packet.len(), 46, "Packet must be at least 46 bytes");
let result = parse_nbtns_query(&packet);
assert!(result.is_some(), "Should parse valid NBT-NS packet");
}
#[test]
fn test_parse_nbtns_query_response_ignored() {
let mut packet = vec![
0x00, 0x01,
0x80, 0x00, 0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x20,
];
packet.extend_from_slice(&[b'A'; 32]);
let result = parse_nbtns_query(&packet);
assert_eq!(result, None, "Should ignore NBT-NS responses");
}
#[test]
fn test_parse_nbtns_query_wrong_name_length() {
let mut packet = vec![
0x00, 0x01,
0x00, 0x00,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x10, ];
packet.extend_from_slice(&[b'A'; 16]);
let result = parse_nbtns_query(&packet);
assert_eq!(result, None, "Should reject packets with wrong name length");
}
#[test]
fn test_parse_nbtns_query_truncated() {
let mut packet = vec![
0x00, 0x01,
0x00, 0x00,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x20, ];
packet.extend_from_slice(&[b'A'; 10]);
let result = parse_nbtns_query(&packet);
assert_eq!(result, None, "Should reject truncated packets");
}
#[test]
fn test_parse_nbtns_query_too_short() {
let packet = vec![0u8; 40];
let result = parse_nbtns_query(&packet);
assert_eq!(result, None, "Should reject packets < 46 bytes");
}
#[test]
fn test_parse_dns_name_single_label() {
let mut data = vec![0u8; 20];
data[0] = 4; data[1..5].copy_from_slice(b"test");
data[5] = 0;
let result = parse_dns_name(&data, 0);
assert_eq!(result, Some("test".to_string()));
}
#[test]
fn test_parse_dns_name_multiple_labels() {
let mut data = vec![0u8; 50];
let mut offset = 0;
data[offset] = 3; offset += 1;
data[offset..offset+3].copy_from_slice(b"abc");
offset += 3;
data[offset] = 5; offset += 1;
data[offset..offset+5].copy_from_slice(b"local");
offset += 5;
data[offset] = 0;
let result = parse_dns_name(&data, 0);
assert_eq!(result, Some("abc.local".to_string()));
}
#[test]
fn test_parse_dns_name_empty() {
let data = vec![0u8];
let result = parse_dns_name(&data, 0);
assert_eq!(result, None, "Should return None for empty domain");
}
#[test]
fn test_parse_dns_name_truncated_label() {
let mut data = vec![0u8; 20];
data[0] = 10; data[1..6].copy_from_slice(b"abcde");
let result = parse_dns_name(&data, 0);
assert!(result.is_some(), "Should handle partial reads");
}
#[test]
fn test_parse_dns_name_offset_out_of_bounds() {
let data = vec![0u8; 10];
let result = parse_dns_name(&data, 100); assert_eq!(result, None, "Should handle offset out of bounds");
}
#[test]
fn test_parse_dns_name_pointer_format() {
let mut data = vec![0u8; 50];
data[0] = 0xC0; data[1] = 12;
data[12] = 4; data[13..17].copy_from_slice(b"test");
data[17] = 0;
let result = parse_dns_name(&data, 0);
assert_eq!(result, Some("test".to_string()), "Should follow pointer");
}
#[test]
fn test_decode_nbt_name_ascii() {
let encoded = vec![b'A', b'B']; let result = decode_nbt_name(&encoded);
assert!(!result.is_empty());
}
#[test]
fn test_decode_nbt_name_empty() {
let encoded = vec![];
let result = decode_nbt_name(&encoded);
assert_eq!(result, "");
}
#[test]
fn test_decode_nbt_name_odd_length() {
let encoded = vec![b'A', b'B', b'C']; let result = decode_nbt_name(&encoded);
assert_eq!(result.len(), 1, "Should only decode complete pairs");
}
#[test]
fn test_decode_nbt_name_padded_spaces() {
let encoded = vec![b'C', b'A']; let result = decode_nbt_name(&encoded);
assert_eq!(result.len(), 1, "Should decode padded name");
}
#[test]
fn test_parse_llmnr_large_domain_name() {
let mut packet = vec![
0x00, 0x01,
0x00, 0x00,
0x00, 0x01,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
];
packet.extend_from_slice(&[8, b'i', b'n', b't', b'e', b'r', b'n', b'a', b'l']);
packet.extend_from_slice(&[4, b'c', b'o', b'r', b'p']);
packet.extend_from_slice(&[5, b'l', b'o', b'c', b'a', b'l', 0]);
let result = parse_llmnr_query(&packet);
assert_eq!(result, Some("internal.corp.local".to_string()));
}
#[test]
fn test_parse_dns_name_safety_limit() {
let mut data = vec![0u8; 50];
data[0] = 0xC0;
data[1] = 2;
data[2] = 0xC0;
data[3] = 0;
let result = parse_dns_name(&data, 0);
assert!(result.is_none() || result.as_ref().map_or(true, |s| s.is_empty()));
}
#[test]
fn test_nbtns_buffer_boundary() {
let mut packet = vec![0u8; 45];
packet[0] = 0x00;
packet[1] = 0x01; packet[2] = 0x00;
packet[3] = 0x00; packet[4] = 0x00;
packet[5] = 0x01; packet[6] = 0x00;
packet[7] = 0x00; packet[8] = 0x00;
packet[9] = 0x00; packet[10] = 0x00;
packet[11] = 0x00;
packet[12] = 0x20; packet[13..45].copy_from_slice(&[b'A'; 32]);
let result = parse_nbtns_query(&packet);
assert!(result.is_none(), "Should require 46 bytes (13 + 33)");
}
}