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
}