Skip to main content

wireforge_app/llmnr/
mod.rs

1//! LLMNR (Link-Local Multicast Name Resolution) parser — RFC 4795.
2//!
3//! LLMNR shares the DNS wire format. Unlike standard DNS, LLMNR queries
4//! always contain exactly one question and the TC (truncation) bit has
5//! different semantics (RFC 4795 §2.1.2).
6
7use crate::dns::parser::{DnsPacket, DnsQuestionIter, DnsRecordIter};
8
9/// LLMNR packet wrapper. Internally delegates to [`DnsPacket`] since
10/// LLMNR uses the identical DNS wire format (RFC 4795 §2.1).
11#[derive(Debug, Clone)]
12pub struct LlmnrPacket<'a> {
13    dns: DnsPacket<'a>,
14}
15
16impl<'a> LlmnrPacket<'a> {
17    pub fn new(buf: &'a [u8]) -> Option<Self> {
18        DnsPacket::new(buf).map(|dns| Self { dns })
19    }
20
21    pub fn is_query(&self) -> bool { !self.dns.qr() }
22    pub fn is_response(&self) -> bool { self.dns.qr() }
23
24    /// LLMNR queries must contain exactly one question (RFC 4795 §2.3).
25    pub fn is_valid_query(&self) -> bool {
26        !self.dns.qr() && self.dns.questions_count() == 1
27    }
28
29    /// The C (conflict) bit in LLMNR responses (RFC 4795 §2.5).
30    /// When set, multiple responders detected a name conflict.
31    pub fn has_conflict(&self) -> bool {
32        self.dns.qr() && (self.dns.flags() & 0x0040) != 0
33    }
34
35    pub fn questions(&self) -> DnsQuestionIter<'a> { self.dns.questions() }
36    pub fn answers(&self) -> DnsRecordIter<'a> { self.dns.answers() }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use crate::dns::builder::DnsPacketBuilder;
43    use crate::dns::types::{DnsClass, DnsType};
44
45    #[test]
46    fn parse_llmnr_query() {
47        let q = DnsPacketBuilder::query()
48            .add_question("wpad", DnsType::A, DnsClass::IN)
49            .build();
50        let p = LlmnrPacket::new(&q).unwrap();
51        assert!(p.is_query());
52        assert!(p.is_valid_query());
53    }
54
55    #[test]
56    fn parse_llmnr_response() {
57        let r = DnsPacketBuilder::response()
58            .add_raw_answer("wpad", DnsType::A, 30, &[10, 0, 0, 1])
59            .build();
60        assert!(LlmnrPacket::new(&r).unwrap().is_response());
61    }
62}