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