Skip to main content

stackforge_core/layer/dns/
edns.rs

1//! EDNS0 (Extension Mechanisms for DNS) option types.
2//!
3//! Implements RFC 6891 (EDNS0), RFC 7871 (Client Subnet), RFC 7873 (Cookies),
4//! RFC 8914 (Extended DNS Errors), and other EDNS0 options.
5
6use crate::layer::field::FieldError;
7use crate::layer::field::MacAddress;
8
9/// EDNS0 option parsed from OPT record RDATA.
10#[derive(Debug, Clone, PartialEq)]
11pub enum EdnsOption {
12    /// NSID - Name Server Identifier (RFC 5001)
13    NSID(Vec<u8>),
14
15    /// DAU - DNSSEC Algorithm Understood (RFC 6975)
16    DAU(Vec<u8>),
17
18    /// DHU - DS Hash Understood (RFC 6975)
19    DHU(Vec<u8>),
20
21    /// N3U - NSEC3 Hash Understood (RFC 6975)
22    N3U(Vec<u8>),
23
24    /// Client Subnet (RFC 7871)
25    ClientSubnet {
26        family: u16, // 1=IPv4, 2=IPv6
27        source_prefix: u8,
28        scope_prefix: u8,
29        address: Vec<u8>,
30    },
31
32    /// DNS Cookie (RFC 7873)
33    Cookie {
34        client: Vec<u8>, // 8 bytes
35        server: Vec<u8>, // 0 or 8-32 bytes
36    },
37
38    /// Extended DNS Error (RFC 8914)
39    ExtendedDnsError { info_code: u16, extra_text: String },
40
41    /// Owner option (Apple mDNS)
42    Owner {
43        version: u8,
44        seq: u8,
45        primary_mac: MacAddress,
46    },
47
48    /// Unknown/unsupported option
49    Unknown { code: u16, data: Vec<u8> },
50}
51
52/// EDNS0 option codes
53pub mod option_code {
54    pub const LLQ: u16 = 1;
55    pub const UL: u16 = 2;
56    pub const NSID: u16 = 3;
57    pub const OWNER: u16 = 4;
58    pub const DAU: u16 = 5;
59    pub const DHU: u16 = 6;
60    pub const N3U: u16 = 7;
61    pub const CLIENT_SUBNET: u16 = 8;
62    pub const COOKIE: u16 = 10;
63    pub const TCP_KEEPALIVE: u16 = 11;
64    pub const PADDING: u16 = 12;
65    pub const CHAIN: u16 = 13;
66    pub const KEY_TAG: u16 = 14;
67    pub const EXTENDED_DNS_ERROR: u16 = 15;
68}
69
70/// Extended DNS Error info codes (RFC 8914)
71pub mod ede_code {
72    pub const OTHER: u16 = 0;
73    pub const UNSUPPORTED_DNSKEY_ALGORITHM: u16 = 1;
74    pub const UNSUPPORTED_DS_DIGEST_TYPE: u16 = 2;
75    pub const STALE_ANSWER: u16 = 3;
76    pub const FORGED_ANSWER: u16 = 4;
77    pub const DNSSEC_INDETERMINATE: u16 = 5;
78    pub const DNSSEC_BOGUS: u16 = 6;
79    pub const SIGNATURE_EXPIRED: u16 = 7;
80    pub const SIGNATURE_NOT_YET_VALID: u16 = 8;
81    pub const DNSKEY_MISSING: u16 = 9;
82    pub const RRSIGS_MISSING: u16 = 10;
83    pub const NO_ZONE_KEY_BIT_SET: u16 = 11;
84    pub const NSEC_MISSING: u16 = 12;
85    pub const CACHED_ERROR: u16 = 13;
86    pub const NOT_READY: u16 = 14;
87    pub const BLOCKED: u16 = 15;
88    pub const CENSORED: u16 = 16;
89    pub const FILTERED: u16 = 17;
90    pub const PROHIBITED: u16 = 18;
91    pub const STALE_NXDOMAIN_ANSWER: u16 = 19;
92    pub const NOT_AUTHORITATIVE: u16 = 20;
93    pub const NOT_SUPPORTED: u16 = 21;
94    pub const NO_REACHABLE_AUTHORITY: u16 = 22;
95    pub const NETWORK_ERROR: u16 = 23;
96    pub const INVALID_DATA: u16 = 24;
97}
98
99impl EdnsOption {
100    /// Parse a single EDNS0 option from wire format.
101    /// `data` contains the option data (after code and length).
102    pub fn parse(code: u16, data: &[u8]) -> Result<Self, FieldError> {
103        match code {
104            option_code::NSID => Ok(EdnsOption::NSID(data.to_vec())),
105
106            option_code::DAU => Ok(EdnsOption::DAU(data.to_vec())),
107
108            option_code::DHU => Ok(EdnsOption::DHU(data.to_vec())),
109
110            option_code::N3U => Ok(EdnsOption::N3U(data.to_vec())),
111
112            option_code::CLIENT_SUBNET => {
113                if data.len() < 4 {
114                    return Err(FieldError::BufferTooShort {
115                        offset: 0,
116                        need: 4,
117                        have: data.len(),
118                    });
119                }
120                let family = u16::from_be_bytes([data[0], data[1]]);
121                let source_prefix = data[2];
122                let scope_prefix = data[3];
123                let address = data[4..].to_vec();
124                Ok(EdnsOption::ClientSubnet {
125                    family,
126                    source_prefix,
127                    scope_prefix,
128                    address,
129                })
130            },
131
132            option_code::COOKIE => {
133                if data.len() < 8 {
134                    return Err(FieldError::BufferTooShort {
135                        offset: 0,
136                        need: 8,
137                        have: data.len(),
138                    });
139                }
140                let client = data[..8].to_vec();
141                let server = if data.len() > 8 {
142                    data[8..].to_vec()
143                } else {
144                    Vec::new()
145                };
146                Ok(EdnsOption::Cookie { client, server })
147            },
148
149            option_code::EXTENDED_DNS_ERROR => {
150                if data.len() < 2 {
151                    return Err(FieldError::BufferTooShort {
152                        offset: 0,
153                        need: 2,
154                        have: data.len(),
155                    });
156                }
157                let info_code = u16::from_be_bytes([data[0], data[1]]);
158                let extra_text = if data.len() > 2 {
159                    String::from_utf8_lossy(&data[2..]).into_owned()
160                } else {
161                    String::new()
162                };
163                Ok(EdnsOption::ExtendedDnsError {
164                    info_code,
165                    extra_text,
166                })
167            },
168
169            option_code::OWNER => {
170                if data.len() < 8 {
171                    return Err(FieldError::BufferTooShort {
172                        offset: 0,
173                        need: 8,
174                        have: data.len(),
175                    });
176                }
177                let version = data[0];
178                let seq = data[1];
179                let primary_mac =
180                    MacAddress::new([data[2], data[3], data[4], data[5], data[6], data[7]]);
181                Ok(EdnsOption::Owner {
182                    version,
183                    seq,
184                    primary_mac,
185                })
186            },
187
188            _ => Ok(EdnsOption::Unknown {
189                code,
190                data: data.to_vec(),
191            }),
192        }
193    }
194
195    /// Get the option code for this EDNS0 option.
196    #[must_use]
197    pub fn code(&self) -> u16 {
198        match self {
199            EdnsOption::NSID(_) => option_code::NSID,
200            EdnsOption::DAU(_) => option_code::DAU,
201            EdnsOption::DHU(_) => option_code::DHU,
202            EdnsOption::N3U(_) => option_code::N3U,
203            EdnsOption::ClientSubnet { .. } => option_code::CLIENT_SUBNET,
204            EdnsOption::Cookie { .. } => option_code::COOKIE,
205            EdnsOption::ExtendedDnsError { .. } => option_code::EXTENDED_DNS_ERROR,
206            EdnsOption::Owner { .. } => option_code::OWNER,
207            EdnsOption::Unknown { code, .. } => *code,
208        }
209    }
210
211    /// Serialize the option data (without the code and length header).
212    #[must_use]
213    pub fn build_data(&self) -> Vec<u8> {
214        match self {
215            EdnsOption::NSID(data) => data.clone(),
216            EdnsOption::DAU(data) => data.clone(),
217            EdnsOption::DHU(data) => data.clone(),
218            EdnsOption::N3U(data) => data.clone(),
219            EdnsOption::ClientSubnet {
220                family,
221                source_prefix,
222                scope_prefix,
223                address,
224            } => {
225                let mut out = Vec::with_capacity(4 + address.len());
226                out.extend_from_slice(&family.to_be_bytes());
227                out.push(*source_prefix);
228                out.push(*scope_prefix);
229                out.extend_from_slice(address);
230                out
231            },
232            EdnsOption::Cookie { client, server } => {
233                let mut out = Vec::with_capacity(client.len() + server.len());
234                out.extend_from_slice(client);
235                out.extend_from_slice(server);
236                out
237            },
238            EdnsOption::ExtendedDnsError {
239                info_code,
240                extra_text,
241            } => {
242                let mut out = Vec::with_capacity(2 + extra_text.len());
243                out.extend_from_slice(&info_code.to_be_bytes());
244                out.extend_from_slice(extra_text.as_bytes());
245                out
246            },
247            EdnsOption::Owner {
248                version,
249                seq,
250                primary_mac,
251            } => {
252                let mut out = vec![*version, *seq];
253                out.extend_from_slice(primary_mac.as_bytes());
254                out
255            },
256            EdnsOption::Unknown { data, .. } => data.clone(),
257        }
258    }
259
260    /// Build the complete TLV (code + length + data).
261    #[must_use]
262    pub fn build(&self) -> Vec<u8> {
263        let data = self.build_data();
264        let mut out = Vec::with_capacity(4 + data.len());
265        out.extend_from_slice(&self.code().to_be_bytes());
266        out.extend_from_slice(&(data.len() as u16).to_be_bytes());
267        out.extend_from_slice(&data);
268        out
269    }
270
271    /// Parse all EDNS0 options from the OPT record RDATA.
272    pub fn parse_all(data: &[u8]) -> Result<Vec<Self>, FieldError> {
273        let mut options = Vec::new();
274        let mut pos = 0;
275
276        while pos < data.len() {
277            if pos + 4 > data.len() {
278                return Err(FieldError::BufferTooShort {
279                    offset: pos,
280                    need: 4,
281                    have: data.len(),
282                });
283            }
284            let code = u16::from_be_bytes([data[pos], data[pos + 1]]);
285            let opt_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
286            pos += 4;
287
288            if pos + opt_len > data.len() {
289                return Err(FieldError::BufferTooShort {
290                    offset: pos,
291                    need: opt_len,
292                    have: data.len() - pos,
293                });
294            }
295
296            let option = Self::parse(code, &data[pos..pos + opt_len])?;
297            options.push(option);
298            pos += opt_len;
299        }
300
301        Ok(options)
302    }
303
304    /// Summary string for display.
305    #[must_use]
306    pub fn summary(&self) -> String {
307        match self {
308            EdnsOption::NSID(data) => format!("NSID: {:?}", String::from_utf8_lossy(data)),
309            EdnsOption::DAU(algs) => format!("DAU: {algs:?}"),
310            EdnsOption::DHU(algs) => format!("DHU: {algs:?}"),
311            EdnsOption::N3U(algs) => format!("N3U: {algs:?}"),
312            EdnsOption::ClientSubnet {
313                family,
314                source_prefix,
315                scope_prefix,
316                ..
317            } => {
318                format!(
319                    "ClientSubnet: family={family} source=/{source_prefix} scope=/{scope_prefix}"
320                )
321            },
322            EdnsOption::Cookie { client, server } => {
323                format!("Cookie: client={} server={}", hex(client), hex(server))
324            },
325            EdnsOption::ExtendedDnsError {
326                info_code,
327                extra_text,
328            } => {
329                format!("EDE: code={info_code} text={extra_text:?}")
330            },
331            EdnsOption::Owner {
332                version,
333                seq,
334                primary_mac,
335            } => {
336                format!("Owner: v={version} seq={seq} mac={primary_mac}")
337            },
338            EdnsOption::Unknown { code, data } => {
339                format!("Option({}): {} bytes", code, data.len())
340            },
341        }
342    }
343}
344
345fn hex(data: &[u8]) -> String {
346    data.iter().map(|b| format!("{b:02x}")).collect()
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_client_subnet_parse() {
355        // IPv4, source prefix /24, scope /0, address 192.168.1.0
356        let data = vec![0x00, 0x01, 24, 0, 192, 168, 1];
357        let opt = EdnsOption::parse(option_code::CLIENT_SUBNET, &data).unwrap();
358        match opt {
359            EdnsOption::ClientSubnet {
360                family,
361                source_prefix,
362                scope_prefix,
363                address,
364            } => {
365                assert_eq!(family, 1);
366                assert_eq!(source_prefix, 24);
367                assert_eq!(scope_prefix, 0);
368                assert_eq!(address, vec![192, 168, 1]);
369            },
370            _ => panic!("wrong variant"),
371        }
372    }
373
374    #[test]
375    fn test_client_subnet_roundtrip() {
376        let opt = EdnsOption::ClientSubnet {
377            family: 1,
378            source_prefix: 24,
379            scope_prefix: 0,
380            address: vec![192, 168, 1],
381        };
382        let built = opt.build();
383        assert_eq!(built[0..2], [0x00, 0x08]); // code = 8
384        assert_eq!(built[2..4], [0x00, 0x07]); // length = 7
385        let parsed = EdnsOption::parse(option_code::CLIENT_SUBNET, &built[4..]).unwrap();
386        assert_eq!(parsed, opt);
387    }
388
389    #[test]
390    fn test_cookie_parse() {
391        let mut data = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; // client
392        data.extend_from_slice(&[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18]); // server
393        let opt = EdnsOption::parse(option_code::COOKIE, &data).unwrap();
394        match &opt {
395            EdnsOption::Cookie { client, server } => {
396                assert_eq!(client.len(), 8);
397                assert_eq!(server.len(), 8);
398            },
399            _ => panic!("wrong variant"),
400        }
401    }
402
403    #[test]
404    fn test_extended_dns_error_parse() {
405        let mut data = vec![0x00, 0x06]; // DNSSEC Bogus
406        data.extend_from_slice(b"signature verification failed");
407        let opt = EdnsOption::parse(option_code::EXTENDED_DNS_ERROR, &data).unwrap();
408        match opt {
409            EdnsOption::ExtendedDnsError {
410                info_code,
411                extra_text,
412            } => {
413                assert_eq!(info_code, 6);
414                assert_eq!(extra_text, "signature verification failed");
415            },
416            _ => panic!("wrong variant"),
417        }
418    }
419
420    #[test]
421    fn test_dau_roundtrip() {
422        let opt = EdnsOption::DAU(vec![8, 10, 13, 14]); // RSA/SHA-256, RSA/SHA-512, ECDSA P256, ECDSA P384
423        let built = opt.build();
424        let parsed_opts = EdnsOption::parse_all(&built).unwrap();
425        assert_eq!(parsed_opts.len(), 1);
426        assert_eq!(parsed_opts[0], opt);
427    }
428
429    #[test]
430    fn test_parse_multiple_options() {
431        let opt1 = EdnsOption::DAU(vec![8, 10]);
432        let opt2 = EdnsOption::DHU(vec![1, 2]);
433        let mut data = opt1.build();
434        data.extend_from_slice(&opt2.build());
435
436        let parsed = EdnsOption::parse_all(&data).unwrap();
437        assert_eq!(parsed.len(), 2);
438        assert_eq!(parsed[0], opt1);
439        assert_eq!(parsed[1], opt2);
440    }
441
442    #[test]
443    fn test_unknown_option() {
444        let opt = EdnsOption::parse(9999, &[0x01, 0x02, 0x03]).unwrap();
445        match opt {
446            EdnsOption::Unknown { code, data } => {
447                assert_eq!(code, 9999);
448                assert_eq!(data, vec![1, 2, 3]);
449            },
450            _ => panic!("wrong variant"),
451        }
452    }
453}