Skip to main content

packet_dissector_mdns/
lib.rs

1//! mDNS (Multicast DNS) dissector.
2//!
3//! mDNS reuses the DNS message format (RFC 1035) but operates over UDP
4//! port 5353 and redefines two bits that DNS would otherwise treat as part
5//! of the 16-bit CLASS field:
6//!
7//! - **Top bit of `qclass` in the Question Section** — the unicast-response
8//!   bit ("QU" bit); see RFC 6762, Section 18.12 and Section 5.4.
9//! - **Top bit of `rrclass` in the Resource Record Sections** — the
10//!   cache-flush bit; see RFC 6762, Section 18.13 and Section 10.2.
11//!
12//! Per RFC 6762, Section 10.2 the cache-flush reuse does not apply to
13//! pseudo-RRs (e.g., OPT), whose `rrclass` field continues to encode the
14//! EDNS0 UDP payload size as a full 16-bit value.
15//!
16//! ## References
17//! - RFC 6762 (mDNS): <https://www.rfc-editor.org/rfc/rfc6762>
18//! - RFC 1035 (DNS message format): <https://www.rfc-editor.org/rfc/rfc1035>
19//! - RFC 6891 (EDNS0 / OPT pseudo-RR): <https://www.rfc-editor.org/rfc/rfc6891>
20
21#![deny(missing_docs)]
22
23use packet_dissector_core::dissector::{DissectResult, Dissector};
24use packet_dissector_core::error::PacketError;
25use packet_dissector_core::field::FieldDescriptor;
26use packet_dissector_core::packet::DissectBuffer;
27use packet_dissector_dns::{DnsDissector, dissect_as_mdns};
28
29/// mDNS dissector.
30///
31/// Parses a Multicast DNS message by reusing the DNS (RFC 1035) message
32/// parser and applying the RFC 6762 reinterpretation of the top bit of the
33/// `qclass` / `rrclass` fields (see module-level docs for details). The
34/// layer is labelled "mDNS" and field names match the DNS dissector, plus
35/// `qu` on each question and `cache_flush` on each non-OPT resource record.
36pub struct MdnsDissector;
37
38impl Dissector for MdnsDissector {
39    fn name(&self) -> &'static str {
40        "Multicast Domain Name System"
41    }
42
43    fn short_name(&self) -> &'static str {
44        "mDNS"
45    }
46
47    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
48        DnsDissector.field_descriptors()
49    }
50
51    fn dissect<'pkt>(
52        &self,
53        data: &'pkt [u8],
54        buf: &mut DissectBuffer<'pkt>,
55        offset: usize,
56    ) -> Result<DissectResult, PacketError> {
57        // RFC 6762, Section 18 — <https://www.rfc-editor.org/rfc/rfc6762#section-18>
58        // Delegates to the shared DNS parser in mDNS mode, which splits the
59        // QU bit (questions) and cache-flush bit (RRs) from the 15-bit class.
60        dissect_as_mdns(data, buf, offset)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use packet_dissector_core::field::{Field, FieldValue};
68    use packet_dissector_core::packet::Layer;
69
70    // # RFC 6762 (mDNS) Coverage
71    //
72    // | RFC Section | Description                                        | Test                                 |
73    // |-------------|----------------------------------------------------|--------------------------------------|
74    // | 18.1        | ID (Query Identifier)                              | parse_mdns_query                     |
75    // | 18.2        | QR (Query/Response) bit                            | parse_mdns_query, parse_mdns_response|
76    // | 18.4        | AA (Authoritative Answer) bit                      | parse_mdns_response                  |
77    // | 18.12 / 5.4 | Top bit of qclass = unicast-response (QU) bit      | parse_mdns_qu_bit_set                |
78    // | 18.12 / 5.4 | QM question (top bit of qclass clear)              | parse_mdns_qm_bit_clear              |
79    // | 18.13/10.2  | Top bit of rrclass = cache-flush bit               | parse_mdns_cache_flush_bit_set       |
80    // | 18.13/10.2  | Non-flushed record (top bit of rrclass clear)      | parse_mdns_cache_flush_bit_clear     |
81    // | 10.2        | OPT pseudo-RR not subject to cache-flush reuse     | parse_mdns_opt_rrclass_unchanged     |
82    // | —           | Truncated packet                                   | parse_mdns_truncated                 |
83
84    /// Minimal mDNS query for _http._tcp.local PTR IN (no QU bit).
85    fn mdns_query_bytes() -> Vec<u8> {
86        vec![
87            0x00, 0x00, // Transaction ID (typically 0 for mDNS queries)
88            0x00, 0x00, // Flags: QR=0 (query), no RD
89            0x00, 0x01, // QDCOUNT = 1
90            0x00, 0x00, // ANCOUNT = 0
91            0x00, 0x00, // NSCOUNT = 0
92            0x00, 0x00, // ARCOUNT = 0
93            // QNAME: _http._tcp.local
94            0x05, b'_', b'h', b't', b't', b'p', // "_http"
95            0x04, b'_', b't', b'c', b'p', // "_tcp"
96            0x05, b'l', b'o', b'c', b'a', b'l', // "local"
97            0x00, // root label
98            0x00, 0x0c, // QTYPE = PTR (12)
99            0x00, 0x01, // QCLASS = IN (QU bit clear)
100        ]
101    }
102
103    /// Return the children of the first Object in the `questions` Array of
104    /// the first layer in `buf`.
105    fn first_question_children<'a, 'pkt>(
106        buf: &'a DissectBuffer<'pkt>,
107        layer: &'a Layer,
108    ) -> &'a [Field<'pkt>] {
109        let questions = buf.field_by_name(layer, "questions").expect("questions");
110        let arr = match &questions.value {
111            FieldValue::Array(r) => r.clone(),
112            other => panic!("expected Array, got {:?}", other),
113        };
114        let first = &buf.nested_fields(&arr)[0];
115        let obj = match &first.value {
116            FieldValue::Object(r) => r.clone(),
117            other => panic!("expected Object, got {:?}", other),
118        };
119        buf.nested_fields(&obj)
120    }
121
122    /// Return the children of the first Object in the named RR Array
123    /// (e.g., "answers").
124    fn first_rr_children<'a, 'pkt>(
125        buf: &'a DissectBuffer<'pkt>,
126        layer: &'a Layer,
127        section: &str,
128    ) -> &'a [Field<'pkt>] {
129        let arr_field = buf.field_by_name(layer, section).expect("section array");
130        let arr = match &arr_field.value {
131            FieldValue::Array(r) => r.clone(),
132            other => panic!("expected Array, got {:?}", other),
133        };
134        let first = &buf.nested_fields(&arr)[0];
135        let obj = match &first.value {
136            FieldValue::Object(r) => r.clone(),
137            other => panic!("expected Object, got {:?}", other),
138        };
139        buf.nested_fields(&obj)
140    }
141
142    fn child_by_name<'a, 'pkt>(children: &'a [Field<'pkt>], name: &str) -> Option<&'a Field<'pkt>> {
143        children.iter().find(|f| f.name() == name)
144    }
145
146    #[test]
147    fn parse_mdns_query() {
148        let data = mdns_query_bytes();
149        let mut buf = DissectBuffer::new();
150        let result = MdnsDissector.dissect(&data, &mut buf, 0).unwrap();
151
152        assert_eq!(result.bytes_consumed, data.len());
153        assert_eq!(buf.layers().len(), 1);
154
155        let layer = &buf.layers()[0];
156        assert_eq!(layer.name, "mDNS");
157
158        // RFC 6762, Section 18.1 — Transaction ID is 0 in multicast queries.
159        assert_eq!(
160            buf.field_by_name(layer, "id").unwrap().value,
161            FieldValue::U16(0)
162        );
163        // RFC 6762, Section 18.2 — QR = 0 for queries.
164        assert_eq!(
165            buf.field_by_name(layer, "qr").unwrap().value,
166            FieldValue::U8(0)
167        );
168        assert_eq!(
169            buf.field_by_name(layer, "qdcount").unwrap().value,
170            FieldValue::U16(1)
171        );
172    }
173
174    #[test]
175    fn parse_mdns_response() {
176        // mDNS response with one A record answer and the cache-flush bit set
177        // on the answer record (RFC 6762, Section 10.2 / Section 18.13).
178        let data: &[u8] = &[
179            0x00, 0x00, // Transaction ID
180            0x84, 0x00, // Flags: QR=1 (response), AA=1
181            0x00, 0x00, // QDCOUNT = 0
182            0x00, 0x01, // ANCOUNT = 1
183            0x00, 0x00, // NSCOUNT = 0
184            0x00, 0x00, // ARCOUNT = 0
185            // Answer: myhost.local A 192.168.1.10
186            0x06, b'm', b'y', b'h', b'o', b's', b't', // "myhost"
187            0x05, b'l', b'o', b'c', b'a', b'l', // "local"
188            0x00, // root label
189            0x00, 0x01, // TYPE = A
190            0x80, 0x01, // rrclass = 0x8001 = cache-flush bit set, class = IN (1)
191            0x00, 0x00, 0x00, 0x78, // TTL = 120
192            0x00, 0x04, // RDLENGTH = 4
193            0xc0, 0xa8, 0x01, 0x0a, // RDATA = 192.168.1.10
194        ];
195
196        let mut buf = DissectBuffer::new();
197        let result = MdnsDissector.dissect(data, &mut buf, 0).unwrap();
198
199        assert_eq!(result.bytes_consumed, data.len());
200        assert_eq!(buf.layers().len(), 1);
201        assert_eq!(buf.layers()[0].name, "mDNS");
202
203        let layer = &buf.layers()[0];
204        // RFC 6762, Section 18.2 — QR = 1 for responses.
205        assert_eq!(
206            buf.field_by_name(layer, "qr").unwrap().value,
207            FieldValue::U8(1)
208        );
209        // RFC 6762, Section 18.4 — AA = 1 in responses.
210        assert_eq!(
211            buf.field_by_name(layer, "aa").unwrap().value,
212            FieldValue::U8(1)
213        );
214        assert_eq!(
215            buf.field_by_name(layer, "ancount").unwrap().value,
216            FieldValue::U16(1)
217        );
218    }
219
220    #[test]
221    fn parse_mdns_qu_bit_set() {
222        // Query with top bit of qclass set — "QU" question requesting unicast.
223        // RFC 6762, Section 18.12 / Section 5.4.
224        let data: &[u8] = &[
225            0x00, 0x00, 0x00, 0x00, // ID, flags
226            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // QDCOUNT=1
227            // QNAME: host.local
228            0x04, b'h', b'o', b's', b't', 0x05, b'l', b'o', b'c', b'a', b'l', 0x00, 0x00,
229            0x01, // QTYPE = A
230            0x80, 0x01, // qclass: QU=1, class=IN(1)
231        ];
232        let mut buf = DissectBuffer::new();
233        MdnsDissector.dissect(data, &mut buf, 0).unwrap();
234        let layer = &buf.layers()[0];
235        let fields = first_question_children(&buf, layer);
236
237        // RFC 6762, Section 18.12 — the top bit is the QU (unicast-response) bit.
238        assert_eq!(
239            child_by_name(fields, "qu").map(|f| f.value.clone()),
240            Some(FieldValue::U8(1)),
241            "qu bit should be 1 when top bit of qclass is set"
242        );
243        // The class itself is the lower 15 bits.
244        assert_eq!(
245            child_by_name(fields, "class").map(|f| f.value.clone()),
246            Some(FieldValue::U16(1)),
247            "class should be 0x0001 (IN) after masking off QU bit"
248        );
249    }
250
251    #[test]
252    fn parse_mdns_qm_bit_clear() {
253        // Query without QU bit — "QM" question requesting multicast response.
254        let data = mdns_query_bytes();
255        let mut buf = DissectBuffer::new();
256        MdnsDissector.dissect(&data, &mut buf, 0).unwrap();
257        let layer = &buf.layers()[0];
258        let fields = first_question_children(&buf, layer);
259
260        assert_eq!(
261            child_by_name(fields, "qu").map(|f| f.value.clone()),
262            Some(FieldValue::U8(0))
263        );
264        assert_eq!(
265            child_by_name(fields, "class").map(|f| f.value.clone()),
266            Some(FieldValue::U16(1))
267        );
268    }
269
270    #[test]
271    fn parse_mdns_cache_flush_bit_set() {
272        // Response with cache-flush bit set on the answer.
273        // RFC 6762, Section 18.13 / Section 10.2.
274        let data: &[u8] = &[
275            0x00, 0x00, 0x84, 0x00, // ID, flags (QR=1, AA=1)
276            0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // ANCOUNT=1
277            // Answer: myhost.local A 192.168.1.10
278            0x06, b'm', b'y', b'h', b'o', b's', b't', 0x05, b'l', b'o', b'c', b'a', b'l', 0x00,
279            0x00, 0x01, // TYPE = A
280            0x80, 0x01, // rrclass: cache-flush=1, class=IN(1)
281            0x00, 0x00, 0x00, 0x78, // TTL=120
282            0x00, 0x04, 0xc0, 0xa8, 0x01, 0x0a, // RDLENGTH=4, 192.168.1.10
283        ];
284        let mut buf = DissectBuffer::new();
285        MdnsDissector.dissect(data, &mut buf, 0).unwrap();
286        let layer = &buf.layers()[0];
287        let fields = first_rr_children(&buf, layer, "answers");
288
289        // RFC 6762, Section 10.2 — the top bit is the cache-flush bit.
290        assert_eq!(
291            child_by_name(fields, "cache_flush").map(|f| f.value.clone()),
292            Some(FieldValue::U8(1)),
293            "cache_flush should be 1 when top bit of rrclass is set"
294        );
295        assert_eq!(
296            child_by_name(fields, "class").map(|f| f.value.clone()),
297            Some(FieldValue::U16(1)),
298            "class should be 0x0001 (IN) after masking off cache-flush bit"
299        );
300    }
301
302    #[test]
303    fn parse_mdns_cache_flush_bit_clear() {
304        // Response with cache-flush bit clear (shared record).
305        let data: &[u8] = &[
306            0x00, 0x00, 0x84, 0x00, //
307            0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, //
308            0x06, b'm', b'y', b'h', b'o', b's', b't', 0x05, b'l', b'o', b'c', b'a', b'l', 0x00,
309            0x00, 0x01, // TYPE = A
310            0x00, 0x01, // rrclass: cache-flush=0, class=IN(1)
311            0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x0a,
312        ];
313        let mut buf = DissectBuffer::new();
314        MdnsDissector.dissect(data, &mut buf, 0).unwrap();
315        let layer = &buf.layers()[0];
316        let fields = first_rr_children(&buf, layer, "answers");
317
318        assert_eq!(
319            child_by_name(fields, "cache_flush").map(|f| f.value.clone()),
320            Some(FieldValue::U8(0))
321        );
322        assert_eq!(
323            child_by_name(fields, "class").map(|f| f.value.clone()),
324            Some(FieldValue::U16(1))
325        );
326    }
327
328    #[test]
329    fn parse_mdns_opt_rrclass_unchanged() {
330        // RFC 6762, Section 10.2 — the cache-flush reinterpretation does NOT
331        // apply to pseudo-RRs such as OPT (RFC 6891). The OPT rrclass field
332        // continues to encode the EDNS0 UDP payload size as a full 16-bit
333        // value, so the dissector must not emit a `cache_flush` field on
334        // OPT and must preserve the full value in `udp_payload_size`.
335        let data: &[u8] = &[
336            0x00, 0x00, 0x84, 0x00, //
337            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ARCOUNT=1
338            // OPT pseudo-RR: name=root(0), type=OPT(41), rrclass=0x05A0 (payload 1440)
339            0x00, // root name
340            0x00, 0x29, // TYPE = 41 (OPT)
341            0x05, 0xa0, // rrclass = 1440 (UDP payload size, NOT a cache-flush+class)
342            0x00, 0x00, 0x00, 0x00, // extended-rcode/version/DO/Z
343            0x00, 0x00, // RDLENGTH = 0
344        ];
345        let mut buf = DissectBuffer::new();
346        MdnsDissector.dissect(data, &mut buf, 0).unwrap();
347        let layer = &buf.layers()[0];
348        let fields = first_rr_children(&buf, layer, "additionals");
349
350        // No cache_flush field on OPT records.
351        assert!(
352            child_by_name(fields, "cache_flush").is_none(),
353            "OPT pseudo-RR must not expose a cache_flush bit"
354        );
355        // udp_payload_size preserved as the full 16-bit value 0x05A0 = 1440.
356        assert_eq!(
357            child_by_name(fields, "udp_payload_size").map(|f| f.value.clone()),
358            Some(FieldValue::U16(1440))
359        );
360    }
361
362    #[test]
363    fn parse_mdns_truncated() {
364        let data: &[u8] = &[0x00, 0x00, 0x00]; // Only 3 bytes, need 12
365        let mut buf = DissectBuffer::new();
366        let err = MdnsDissector.dissect(data, &mut buf, 0).unwrap_err();
367        assert!(matches!(
368            err,
369            PacketError::Truncated {
370                expected: 12,
371                actual: 3
372            }
373        ));
374    }
375
376    #[test]
377    fn field_descriptors_match_dns() {
378        assert_eq!(
379            MdnsDissector.field_descriptors().len(),
380            DnsDissector.field_descriptors().len()
381        );
382    }
383
384    #[test]
385    fn name_and_short_name() {
386        assert_eq!(MdnsDissector.name(), "Multicast Domain Name System");
387        assert_eq!(MdnsDissector.short_name(), "mDNS");
388    }
389}