Skip to main content

stackforge_core/layer/dns/
rr.rs

1//! DNS Resource Record (RFC 1035 Section 4.1.3).
2//!
3//! ```text
4//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
5//! |                      NAME                       |
6//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
7//! |                      TYPE                       |
8//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
9//! |                     CLASS                       |
10//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
11//! |                      TTL                        |
12//! |                                                 |
13//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
14//! |                   RDLENGTH                      |
15//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
16//! |                     RDATA                       |
17//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
18//! ```
19
20use std::collections::HashMap;
21
22use super::rdata::DnsRData;
23use super::types;
24use crate::layer::field::FieldError;
25use crate::layer::field_ext::DnsName;
26
27/// A DNS Resource Record.
28#[derive(Debug, Clone, PartialEq)]
29pub struct DnsResourceRecord {
30    /// The owner name.
31    pub rrname: DnsName,
32    /// The RR type (e.g., A=1, AAAA=28).
33    pub rtype: u16,
34    /// The RR class (typically IN=1).
35    /// For mDNS, bit 15 is the cache-flush flag.
36    pub rclass: u16,
37    /// Time to live in seconds.
38    pub ttl: u32,
39    /// The parsed RDATA.
40    pub rdata: DnsRData,
41}
42
43impl DnsResourceRecord {
44    /// Create a new resource record with default class IN and TTL 0.
45    pub fn new(rrname: DnsName, rtype: u16, rdata: DnsRData) -> Self {
46        Self {
47            rrname,
48            rtype,
49            rclass: types::dns_class::IN,
50            ttl: 0,
51            rdata,
52        }
53    }
54
55    /// Parse a resource record from wire format.
56    ///
57    /// `packet` is the full DNS packet (needed for pointer decompression).
58    /// `offset` is the start of the resource record.
59    ///
60    /// Returns the parsed record and the number of bytes consumed from `offset`.
61    pub fn parse(packet: &[u8], offset: usize) -> Result<(Self, usize), FieldError> {
62        // Decode the owner name.
63        let (rrname, name_len) = DnsName::decode(packet, offset)?;
64        let fixed_start = offset + name_len;
65
66        // We need 10 bytes for type(2) + class(2) + ttl(4) + rdlength(2).
67        if fixed_start + 10 > packet.len() {
68            return Err(FieldError::BufferTooShort {
69                offset: fixed_start,
70                need: 10,
71                have: packet.len().saturating_sub(fixed_start),
72            });
73        }
74
75        let rtype = u16::from_be_bytes([packet[fixed_start], packet[fixed_start + 1]]);
76        let rclass = u16::from_be_bytes([packet[fixed_start + 2], packet[fixed_start + 3]]);
77        let ttl = u32::from_be_bytes([
78            packet[fixed_start + 4],
79            packet[fixed_start + 5],
80            packet[fixed_start + 6],
81            packet[fixed_start + 7],
82        ]);
83        let rdlength = u16::from_be_bytes([packet[fixed_start + 8], packet[fixed_start + 9]]);
84
85        let rdata_start = fixed_start + 10;
86        let rdata_end = rdata_start + rdlength as usize;
87
88        // Bounds-check the RDATA region.
89        if rdata_end > packet.len() {
90            return Err(FieldError::BufferTooShort {
91                offset: rdata_start,
92                need: rdlength as usize,
93                have: packet.len().saturating_sub(rdata_start),
94            });
95        }
96
97        let rdata = DnsRData::parse(rtype, packet, rdata_start, rdlength)?;
98
99        let total_consumed = name_len + 10 + rdlength as usize;
100        Ok((
101            Self {
102                rrname,
103                rtype,
104                rclass,
105                ttl,
106                rdata,
107            },
108            total_consumed,
109        ))
110    }
111
112    /// Build the resource record to wire format without compression.
113    pub fn build(&self) -> Vec<u8> {
114        let rdata_bytes = self.rdata.build();
115        let rdlength = rdata_bytes.len() as u16;
116
117        let mut out = self.rrname.encode();
118        out.extend_from_slice(&self.rtype.to_be_bytes());
119        out.extend_from_slice(&self.rclass.to_be_bytes());
120        out.extend_from_slice(&self.ttl.to_be_bytes());
121        out.extend_from_slice(&rdlength.to_be_bytes());
122        out.extend_from_slice(&rdata_bytes);
123        out
124    }
125
126    /// Build with DNS name compression.
127    pub fn build_compressed(
128        &self,
129        current_offset: usize,
130        compression_map: &mut HashMap<String, u16>,
131    ) -> Vec<u8> {
132        let mut out = self
133            .rrname
134            .encode_compressed(current_offset, compression_map);
135
136        out.extend_from_slice(&self.rtype.to_be_bytes());
137        out.extend_from_slice(&self.rclass.to_be_bytes());
138        out.extend_from_slice(&self.ttl.to_be_bytes());
139
140        // The RDATA offset is current_offset + name_bytes + 10 (type+class+ttl+rdlength).
141        let rdata_offset = current_offset + out.len() + 2; // +2 for rdlength field
142        let rdata_bytes = self.rdata.build_compressed(rdata_offset, compression_map);
143        let rdlength = rdata_bytes.len() as u16;
144
145        out.extend_from_slice(&rdlength.to_be_bytes());
146        out.extend_from_slice(&rdata_bytes);
147        out
148    }
149
150    /// Whether the mDNS cache-flush bit (bit 15 of rclass) is set.
151    pub fn cache_flush(&self) -> bool {
152        self.rclass & 0x8000 != 0
153    }
154
155    /// Get the actual class without the mDNS cache-flush bit.
156    pub fn actual_class(&self) -> u16 {
157        self.rclass & 0x7FFF
158    }
159
160    /// Set or clear the mDNS cache-flush bit.
161    pub fn set_cache_flush(&mut self, flush: bool) {
162        if flush {
163            self.rclass |= 0x8000;
164        } else {
165            self.rclass &= 0x7FFF;
166        }
167    }
168
169    /// Human-readable summary of this resource record.
170    pub fn summary(&self) -> String {
171        let type_name = types::dns_type_name(self.rtype);
172        let class_name = types::dns_class_name(self.actual_class());
173        let rdata_summary = self.rdata.summary();
174        format!(
175            "{} {} {} {}",
176            self.rrname, type_name, class_name, rdata_summary
177        )
178    }
179
180    /// Whether this is an OPT pseudo-record (EDNS0, RFC 6891).
181    pub fn is_opt(&self) -> bool {
182        self.rtype == types::rr_type::OPT
183    }
184
185    // ========================================================================
186    // OPT pseudo-record helpers (RFC 6891)
187    // ========================================================================
188
189    /// For OPT records, the class field encodes the requestor's UDP payload size.
190    pub fn opt_udp_size(&self) -> u16 {
191        self.rclass
192    }
193
194    /// For OPT records, the upper 8 bits of the TTL encode the extended RCODE.
195    pub fn opt_extended_rcode(&self) -> u8 {
196        ((self.ttl >> 24) & 0xFF) as u8
197    }
198
199    /// For OPT records, bits 16-23 of the TTL encode the EDNS version.
200    pub fn opt_version(&self) -> u8 {
201        ((self.ttl >> 16) & 0xFF) as u8
202    }
203
204    /// For OPT records, the DNSSEC OK (DO) flag is bit 15 of the lower 16 bits of the TTL.
205    pub fn opt_do_flag(&self) -> bool {
206        self.ttl & 0x8000 != 0
207    }
208}
209
210impl Default for DnsResourceRecord {
211    fn default() -> Self {
212        Self {
213            rrname: DnsName::root(),
214            rtype: types::rr_type::A,
215            rclass: types::dns_class::IN,
216            ttl: 0,
217            rdata: DnsRData::Unknown {
218                rtype: types::rr_type::A,
219                data: Vec::new(),
220            },
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use std::net::Ipv4Addr;
229
230    /// Helper: build a wire-format A record for "example.com" with address 93.184.216.34.
231    fn build_example_a_record() -> Vec<u8> {
232        let mut data = Vec::new();
233        // Name: example.com
234        data.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e']);
235        data.extend_from_slice(&[3, b'c', b'o', b'm']);
236        data.push(0); // root label
237        // Type A (1)
238        data.extend_from_slice(&[0x00, 0x01]);
239        // Class IN (1)
240        data.extend_from_slice(&[0x00, 0x01]);
241        // TTL: 300 seconds
242        data.extend_from_slice(&300u32.to_be_bytes());
243        // RDLENGTH: 4
244        data.extend_from_slice(&[0x00, 0x04]);
245        // RDATA: 93.184.216.34
246        data.extend_from_slice(&[93, 184, 216, 34]);
247        data
248    }
249
250    #[test]
251    fn test_parse_a_record() {
252        let data = build_example_a_record();
253        let (rr, consumed) = DnsResourceRecord::parse(&data, 0).unwrap();
254
255        assert_eq!(rr.rrname.labels, vec!["example", "com"]);
256        assert_eq!(rr.rtype, types::rr_type::A);
257        assert_eq!(rr.rclass, types::dns_class::IN);
258        assert_eq!(rr.ttl, 300);
259        assert_eq!(rr.rdata, DnsRData::A(Ipv4Addr::new(93, 184, 216, 34)));
260        assert_eq!(consumed, data.len());
261    }
262
263    #[test]
264    fn test_build_roundtrip_a_record() {
265        let rr = DnsResourceRecord {
266            rrname: DnsName::from_str_dotted("example.com").unwrap(),
267            rtype: types::rr_type::A,
268            rclass: types::dns_class::IN,
269            ttl: 3600,
270            rdata: DnsRData::A(Ipv4Addr::new(192, 168, 1, 1)),
271        };
272
273        let built = rr.build();
274        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
275
276        assert_eq!(consumed, built.len());
277        assert_eq!(parsed.rrname, rr.rrname);
278        assert_eq!(parsed.rtype, rr.rtype);
279        assert_eq!(parsed.rclass, rr.rclass);
280        assert_eq!(parsed.ttl, rr.ttl);
281        assert_eq!(parsed.rdata, rr.rdata);
282    }
283
284    #[test]
285    fn test_build_roundtrip_aaaa_record() {
286        let rr = DnsResourceRecord {
287            rrname: DnsName::from_str_dotted("ipv6.example.com").unwrap(),
288            rtype: types::rr_type::AAAA,
289            rclass: types::dns_class::IN,
290            ttl: 7200,
291            rdata: DnsRData::AAAA("2001:db8::1".parse().unwrap()),
292        };
293
294        let built = rr.build();
295        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
296
297        assert_eq!(consumed, built.len());
298        assert_eq!(parsed.rtype, types::rr_type::AAAA);
299        assert_eq!(parsed.rdata, rr.rdata);
300    }
301
302    #[test]
303    fn test_build_roundtrip_cname_record() {
304        let rr = DnsResourceRecord {
305            rrname: DnsName::from_str_dotted("www.example.com").unwrap(),
306            rtype: types::rr_type::CNAME,
307            rclass: types::dns_class::IN,
308            ttl: 600,
309            rdata: DnsRData::CNAME(DnsName::from_str_dotted("example.com").unwrap()),
310        };
311
312        let built = rr.build();
313        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
314
315        assert_eq!(consumed, built.len());
316        assert_eq!(parsed.rtype, types::rr_type::CNAME);
317        assert_eq!(
318            parsed.rdata,
319            DnsRData::CNAME(DnsName::from_str_dotted("example.com").unwrap())
320        );
321    }
322
323    #[test]
324    fn test_build_roundtrip_mx_record() {
325        let rr = DnsResourceRecord {
326            rrname: DnsName::from_str_dotted("example.com").unwrap(),
327            rtype: types::rr_type::MX,
328            rclass: types::dns_class::IN,
329            ttl: 3600,
330            rdata: DnsRData::MX {
331                preference: 10,
332                exchange: DnsName::from_str_dotted("mail.example.com").unwrap(),
333            },
334        };
335
336        let built = rr.build();
337        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
338
339        assert_eq!(consumed, built.len());
340        assert_eq!(parsed.rtype, types::rr_type::MX);
341        assert_eq!(parsed.rdata, rr.rdata);
342    }
343
344    #[test]
345    fn test_build_roundtrip_txt_record() {
346        let rr = DnsResourceRecord {
347            rrname: DnsName::from_str_dotted("example.com").unwrap(),
348            rtype: types::rr_type::TXT,
349            rclass: types::dns_class::IN,
350            ttl: 300,
351            rdata: DnsRData::TXT(vec![b"v=spf1 include:example.com ~all".to_vec()]),
352        };
353
354        let built = rr.build();
355        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
356
357        assert_eq!(consumed, built.len());
358        assert_eq!(parsed.rdata, rr.rdata);
359    }
360
361    #[test]
362    fn test_opt_record_helpers() {
363        // OPT pseudo-record:
364        //   NAME: root (.)
365        //   TYPE: OPT (41)
366        //   CLASS: UDP payload size (e.g., 4096)
367        //   TTL encodes: extended-rcode (8 bits) | version (8 bits) | DO flag + reserved (16 bits)
368        let rr = DnsResourceRecord {
369            rrname: DnsName::root(),
370            rtype: types::rr_type::OPT,
371            rclass: 4096, // UDP payload size
372            // TTL: extended_rcode=0, version=0, DO=1, rest=0
373            // DO flag is bit 15 of lower 16 bits => 0x0000_8000
374            ttl: 0x0000_8000,
375            rdata: DnsRData::OPT(vec![]),
376        };
377
378        assert!(rr.is_opt());
379        assert_eq!(rr.opt_udp_size(), 4096);
380        assert_eq!(rr.opt_extended_rcode(), 0);
381        assert_eq!(rr.opt_version(), 0);
382        assert!(rr.opt_do_flag());
383    }
384
385    #[test]
386    fn test_opt_record_extended_rcode_and_version() {
387        // TTL: extended_rcode=3, version=1, DO=0
388        // 0x03_01_0000
389        let rr = DnsResourceRecord {
390            rrname: DnsName::root(),
391            rtype: types::rr_type::OPT,
392            rclass: 1232,
393            ttl: 0x03_01_0000,
394            rdata: DnsRData::OPT(vec![]),
395        };
396
397        assert!(rr.is_opt());
398        assert_eq!(rr.opt_udp_size(), 1232);
399        assert_eq!(rr.opt_extended_rcode(), 3);
400        assert_eq!(rr.opt_version(), 1);
401        assert!(!rr.opt_do_flag());
402    }
403
404    #[test]
405    fn test_mdns_cache_flush() {
406        let mut rr = DnsResourceRecord::new(
407            DnsName::from_str_dotted("test.local").unwrap(),
408            types::rr_type::A,
409            DnsRData::A(Ipv4Addr::new(192, 168, 0, 1)),
410        );
411
412        assert!(!rr.cache_flush());
413        assert_eq!(rr.actual_class(), types::dns_class::IN);
414
415        rr.set_cache_flush(true);
416        assert!(rr.cache_flush());
417        assert_eq!(rr.actual_class(), types::dns_class::IN);
418        assert_eq!(rr.rclass, 0x8001);
419
420        rr.set_cache_flush(false);
421        assert!(!rr.cache_flush());
422        assert_eq!(rr.rclass, types::dns_class::IN);
423    }
424
425    #[test]
426    fn test_buffer_too_short_no_fixed_fields() {
427        // Name only, no type/class/ttl/rdlength
428        let data = vec![4, b't', b'e', b's', b't', 0];
429        let result = DnsResourceRecord::parse(&data, 0);
430        assert!(result.is_err());
431        match result.unwrap_err() {
432            FieldError::BufferTooShort { need, .. } => assert_eq!(need, 10),
433            other => panic!("Expected BufferTooShort, got {:?}", other),
434        }
435    }
436
437    #[test]
438    fn test_buffer_too_short_truncated_rdata() {
439        let mut data = Vec::new();
440        // Name: test
441        data.extend_from_slice(&[4, b't', b'e', b's', b't', 0]);
442        // Type A
443        data.extend_from_slice(&[0x00, 0x01]);
444        // Class IN
445        data.extend_from_slice(&[0x00, 0x01]);
446        // TTL
447        data.extend_from_slice(&[0x00, 0x00, 0x00, 0x3C]);
448        // RDLENGTH: 4 (but we won't provide 4 bytes of RDATA)
449        data.extend_from_slice(&[0x00, 0x04]);
450        // Only 2 bytes of RDATA instead of 4
451        data.extend_from_slice(&[192, 168]);
452
453        let result = DnsResourceRecord::parse(&data, 0);
454        assert!(result.is_err());
455        match result.unwrap_err() {
456            FieldError::BufferTooShort { need, have, .. } => {
457                assert_eq!(need, 4);
458                assert_eq!(have, 2);
459            },
460            other => panic!("Expected BufferTooShort, got {:?}", other),
461        }
462    }
463
464    #[test]
465    fn test_buffer_too_short_empty() {
466        let result = DnsResourceRecord::parse(&[], 0);
467        assert!(result.is_err());
468    }
469
470    #[test]
471    fn test_new_defaults() {
472        let rr = DnsResourceRecord::new(
473            DnsName::from_str_dotted("example.com").unwrap(),
474            types::rr_type::A,
475            DnsRData::A(Ipv4Addr::new(1, 2, 3, 4)),
476        );
477
478        assert_eq!(rr.rclass, types::dns_class::IN);
479        assert_eq!(rr.ttl, 0);
480    }
481
482    #[test]
483    fn test_default() {
484        let rr = DnsResourceRecord::default();
485        assert!(rr.rrname.is_root());
486        assert_eq!(rr.rtype, types::rr_type::A);
487        assert_eq!(rr.rclass, types::dns_class::IN);
488        assert_eq!(rr.ttl, 0);
489    }
490
491    #[test]
492    fn test_summary() {
493        let rr = DnsResourceRecord {
494            rrname: DnsName::from_str_dotted("example.com").unwrap(),
495            rtype: types::rr_type::A,
496            rclass: types::dns_class::IN,
497            ttl: 300,
498            rdata: DnsRData::A(Ipv4Addr::new(192, 168, 1, 1)),
499        };
500
501        let summary = rr.summary();
502        assert!(summary.contains("example.com."));
503        assert!(summary.contains("A"));
504        assert!(summary.contains("IN"));
505        assert!(summary.contains("192.168.1.1"));
506    }
507
508    #[test]
509    fn test_is_opt() {
510        let opt = DnsResourceRecord {
511            rrname: DnsName::root(),
512            rtype: types::rr_type::OPT,
513            rclass: 4096,
514            ttl: 0,
515            rdata: DnsRData::OPT(vec![]),
516        };
517        assert!(opt.is_opt());
518
519        let a = DnsResourceRecord::new(
520            DnsName::from_str_dotted("example.com").unwrap(),
521            types::rr_type::A,
522            DnsRData::A(Ipv4Addr::LOCALHOST),
523        );
524        assert!(!a.is_opt());
525    }
526
527    #[test]
528    fn test_parse_at_nonzero_offset() {
529        // Prepend some garbage bytes before the actual record.
530        let record_bytes = build_example_a_record();
531        let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; // 4 bytes of header
532        data.extend_from_slice(&record_bytes);
533
534        let (rr, consumed) = DnsResourceRecord::parse(&data, 4).unwrap();
535        assert_eq!(rr.rrname.labels, vec!["example", "com"]);
536        assert_eq!(rr.rdata, DnsRData::A(Ipv4Addr::new(93, 184, 216, 34)));
537        assert_eq!(consumed, record_bytes.len());
538    }
539
540    #[test]
541    fn test_build_compressed_produces_valid_output() {
542        let rr = DnsResourceRecord {
543            rrname: DnsName::from_str_dotted("example.com").unwrap(),
544            rtype: types::rr_type::A,
545            rclass: types::dns_class::IN,
546            ttl: 60,
547            rdata: DnsRData::A(Ipv4Addr::new(10, 0, 0, 1)),
548        };
549
550        let mut compression_map = HashMap::new();
551        let built = rr.build_compressed(0, &mut compression_map);
552
553        // The compressed output should be parseable (for A records, no name compression
554        // in RDATA, so the result is the same as uncompressed).
555        let (parsed, consumed) = DnsResourceRecord::parse(&built, 0).unwrap();
556        assert_eq!(consumed, built.len());
557        assert_eq!(parsed.rrname, rr.rrname);
558        assert_eq!(parsed.rdata, rr.rdata);
559    }
560
561    #[test]
562    fn test_build_compressed_reuses_names() {
563        let rr1 = DnsResourceRecord {
564            rrname: DnsName::from_str_dotted("example.com").unwrap(),
565            rtype: types::rr_type::A,
566            rclass: types::dns_class::IN,
567            ttl: 60,
568            rdata: DnsRData::A(Ipv4Addr::new(10, 0, 0, 1)),
569        };
570
571        let rr2 = DnsResourceRecord {
572            rrname: DnsName::from_str_dotted("example.com").unwrap(),
573            rtype: types::rr_type::A,
574            rclass: types::dns_class::IN,
575            ttl: 60,
576            rdata: DnsRData::A(Ipv4Addr::new(10, 0, 0, 2)),
577        };
578
579        let mut compression_map = HashMap::new();
580        let built1 = rr1.build_compressed(0, &mut compression_map);
581        let built2 = rr2.build_compressed(built1.len(), &mut compression_map);
582
583        // The second record's name should be compressed (pointer instead of full name).
584        let uncompressed2 = rr2.build();
585        assert!(built2.len() < uncompressed2.len());
586
587        // Both should be parseable from the combined buffer.
588        let mut packet = Vec::new();
589        packet.extend_from_slice(&built1);
590        packet.extend_from_slice(&built2);
591
592        let (parsed1, consumed1) = DnsResourceRecord::parse(&packet, 0).unwrap();
593        assert_eq!(parsed1.rrname.labels, vec!["example", "com"]);
594
595        let (parsed2, _consumed2) = DnsResourceRecord::parse(&packet, consumed1).unwrap();
596        assert_eq!(parsed2.rrname.labels, vec!["example", "com"]);
597        assert_eq!(parsed2.rdata, DnsRData::A(Ipv4Addr::new(10, 0, 0, 2)));
598    }
599
600    #[test]
601    fn test_parse_with_name_pointer() {
602        // Simulate a packet where the RR name uses a compression pointer.
603        let mut packet = Vec::new();
604        // Offset 0: "example.com" in wire format
605        packet.extend_from_slice(&[7, b'e', b'x', b'a', b'm', b'p', b'l', b'e']);
606        packet.extend_from_slice(&[3, b'c', b'o', b'm']);
607        packet.push(0); // root label -- 13 bytes total
608
609        // Offset 13: RR with pointer to offset 0
610        packet.extend_from_slice(&[0xC0, 0x00]); // pointer to offset 0
611        packet.extend_from_slice(&[0x00, 0x01]); // type A
612        packet.extend_from_slice(&[0x00, 0x01]); // class IN
613        packet.extend_from_slice(&120u32.to_be_bytes()); // TTL 120
614        packet.extend_from_slice(&[0x00, 0x04]); // rdlength 4
615        packet.extend_from_slice(&[10, 20, 30, 40]); // 10.20.30.40
616
617        let (rr, consumed) = DnsResourceRecord::parse(&packet, 13).unwrap();
618        assert_eq!(rr.rrname.labels, vec!["example", "com"]);
619        assert_eq!(rr.rtype, types::rr_type::A);
620        assert_eq!(rr.ttl, 120);
621        assert_eq!(rr.rdata, DnsRData::A(Ipv4Addr::new(10, 20, 30, 40)));
622        // pointer(2) + type(2) + class(2) + ttl(4) + rdlength(2) + rdata(4) = 16
623        assert_eq!(consumed, 16);
624    }
625}