Skip to main content

seer_core/dns/
records.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5use crate::error::{Result, SeerError};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "UPPERCASE")]
9pub enum RecordType {
10    A,
11    AAAA,
12    CNAME,
13    MX,
14    NS,
15    TXT,
16    SOA,
17    PTR,
18    SRV,
19    CAA,
20    NAPTR,
21    DNSKEY,
22    DS,
23    TLSA,
24    SSHFP,
25    ANY,
26}
27
28impl fmt::Display for RecordType {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            RecordType::A => write!(f, "A"),
32            RecordType::AAAA => write!(f, "AAAA"),
33            RecordType::CNAME => write!(f, "CNAME"),
34            RecordType::MX => write!(f, "MX"),
35            RecordType::NS => write!(f, "NS"),
36            RecordType::TXT => write!(f, "TXT"),
37            RecordType::SOA => write!(f, "SOA"),
38            RecordType::PTR => write!(f, "PTR"),
39            RecordType::SRV => write!(f, "SRV"),
40            RecordType::CAA => write!(f, "CAA"),
41            RecordType::NAPTR => write!(f, "NAPTR"),
42            RecordType::DNSKEY => write!(f, "DNSKEY"),
43            RecordType::DS => write!(f, "DS"),
44            RecordType::TLSA => write!(f, "TLSA"),
45            RecordType::SSHFP => write!(f, "SSHFP"),
46            RecordType::ANY => write!(f, "ANY"),
47        }
48    }
49}
50
51impl FromStr for RecordType {
52    type Err = SeerError;
53
54    fn from_str(s: &str) -> Result<Self> {
55        match s.to_uppercase().as_str() {
56            "A" => Ok(RecordType::A),
57            "AAAA" => Ok(RecordType::AAAA),
58            "CNAME" => Ok(RecordType::CNAME),
59            "MX" => Ok(RecordType::MX),
60            "NS" => Ok(RecordType::NS),
61            "TXT" => Ok(RecordType::TXT),
62            "SOA" => Ok(RecordType::SOA),
63            "PTR" => Ok(RecordType::PTR),
64            "SRV" => Ok(RecordType::SRV),
65            "CAA" => Ok(RecordType::CAA),
66            "NAPTR" => Ok(RecordType::NAPTR),
67            "DNSKEY" => Ok(RecordType::DNSKEY),
68            "DS" => Ok(RecordType::DS),
69            "TLSA" => Ok(RecordType::TLSA),
70            "SSHFP" => Ok(RecordType::SSHFP),
71            "ANY" | "*" => Ok(RecordType::ANY),
72            _ => Err(SeerError::InvalidRecordType(s.to_string())),
73        }
74    }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DnsRecord {
79    pub name: String,
80    pub record_type: RecordType,
81    pub ttl: u32,
82    pub data: RecordData,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(tag = "record_type", content = "value", rename_all = "UPPERCASE")]
87#[allow(clippy::upper_case_acronyms)]
88pub enum RecordData {
89    A {
90        address: String,
91    },
92    AAAA {
93        address: String,
94    },
95    CNAME {
96        target: String,
97    },
98    MX {
99        preference: u16,
100        exchange: String,
101    },
102    NS {
103        nameserver: String,
104    },
105    TXT {
106        text: String,
107    },
108    SOA {
109        mname: String,
110        rname: String,
111        serial: u32,
112        refresh: u32,
113        retry: u32,
114        expire: u32,
115        minimum: u32,
116    },
117    PTR {
118        target: String,
119    },
120    SRV {
121        priority: u16,
122        weight: u16,
123        port: u16,
124        target: String,
125    },
126    CAA {
127        flags: u8,
128        tag: String,
129        value: String,
130    },
131    DNSKEY {
132        flags: u16,
133        protocol: u8,
134        algorithm: u8,
135        public_key: String,
136    },
137    DS {
138        key_tag: u16,
139        algorithm: u8,
140        digest_type: u8,
141        digest: String,
142    },
143    TLSA {
144        cert_usage: u8,
145        selector: u8,
146        matching: u8,
147        /// Hex-encoded certificate association data (uppercase).
148        cert_data: String,
149    },
150    SSHFP {
151        algorithm: u8,
152        fingerprint_type: u8,
153        /// Hex-encoded fingerprint (uppercase).
154        fingerprint: String,
155    },
156    NAPTR {
157        order: u16,
158        preference: u16,
159        flags: String,
160        services: String,
161        regexp: String,
162        replacement: String,
163    },
164    Unknown {
165        raw: String,
166    },
167}
168
169impl fmt::Display for RecordData {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            RecordData::A { address } => write!(f, "{}", address),
173            RecordData::AAAA { address } => write!(f, "{}", address),
174            RecordData::CNAME { target } => write!(f, "{}", target),
175            RecordData::MX {
176                preference,
177                exchange,
178            } => write!(f, "{} {}", preference, exchange),
179            RecordData::NS { nameserver } => write!(f, "{}", nameserver),
180            RecordData::TXT { text } => write!(f, "\"{}\"", text),
181            RecordData::SOA {
182                mname,
183                rname,
184                serial,
185                refresh,
186                retry,
187                expire,
188                minimum,
189            } => write!(
190                f,
191                "{} {} {} {} {} {} {}",
192                mname, rname, serial, refresh, retry, expire, minimum
193            ),
194            RecordData::PTR { target } => write!(f, "{}", target),
195            RecordData::SRV {
196                priority,
197                weight,
198                port,
199                target,
200            } => write!(f, "{} {} {} {}", priority, weight, port, target),
201            RecordData::CAA { flags, tag, value } => write!(f, "{} {} \"{}\"", flags, tag, value),
202            RecordData::DNSKEY {
203                flags,
204                protocol,
205                algorithm,
206                public_key,
207            } => write!(f, "{} {} {} {}", flags, protocol, algorithm, public_key),
208            RecordData::DS {
209                key_tag,
210                algorithm,
211                digest_type,
212                digest,
213            } => write!(f, "{} {} {} {}", key_tag, algorithm, digest_type, digest),
214            RecordData::TLSA {
215                cert_usage,
216                selector,
217                matching,
218                cert_data,
219            } => write!(f, "{} {} {} {}", cert_usage, selector, matching, cert_data),
220            RecordData::SSHFP {
221                algorithm,
222                fingerprint_type,
223                fingerprint,
224            } => write!(f, "{} {} {}", algorithm, fingerprint_type, fingerprint),
225            RecordData::NAPTR {
226                order,
227                preference,
228                flags,
229                services,
230                regexp,
231                replacement,
232            } => write!(
233                f,
234                "{} {} \"{}\" \"{}\" \"{}\" {}",
235                order, preference, flags, services, regexp, replacement
236            ),
237            RecordData::Unknown { raw } => write!(f, "{}", raw),
238        }
239    }
240}
241
242impl DnsRecord {
243    pub fn format_short(&self) -> String {
244        format!("{}", self.data)
245    }
246
247    pub fn format_full(&self) -> String {
248        format!(
249            "{}\t{}\tIN\t{}\t{}",
250            self.name, self.ttl, self.record_type, self.data
251        )
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_record_type_from_str() {
261        assert_eq!("A".parse::<RecordType>().unwrap(), RecordType::A);
262        assert_eq!("aaaa".parse::<RecordType>().unwrap(), RecordType::AAAA);
263        assert_eq!("MX".parse::<RecordType>().unwrap(), RecordType::MX);
264        assert_eq!("*".parse::<RecordType>().unwrap(), RecordType::ANY);
265        assert!("INVALID".parse::<RecordType>().is_err());
266    }
267
268    #[test]
269    fn test_record_type_display() {
270        assert_eq!(RecordType::A.to_string(), "A");
271        assert_eq!(RecordType::AAAA.to_string(), "AAAA");
272        assert_eq!(RecordType::MX.to_string(), "MX");
273        assert_eq!(RecordType::SOA.to_string(), "SOA");
274    }
275
276    #[test]
277    fn test_dns_record_format_short() {
278        let record = DnsRecord {
279            name: "example.com".to_string(),
280            record_type: RecordType::A,
281            ttl: 300,
282            data: RecordData::A {
283                address: "1.2.3.4".to_string(),
284            },
285        };
286        assert_eq!(record.format_short(), "1.2.3.4");
287    }
288
289    #[test]
290    fn test_dns_record_format_full() {
291        let record = DnsRecord {
292            name: "example.com".to_string(),
293            record_type: RecordType::A,
294            ttl: 300,
295            data: RecordData::A {
296                address: "1.2.3.4".to_string(),
297            },
298        };
299        assert_eq!(record.format_full(), "example.com\t300\tIN\tA\t1.2.3.4");
300    }
301
302    #[test]
303    fn test_record_data_display() {
304        let mx = RecordData::MX {
305            preference: 10,
306            exchange: "mail.example.com".to_string(),
307        };
308        assert_eq!(format!("{}", mx), "10 mail.example.com");
309
310        let txt = RecordData::TXT {
311            text: "v=spf1 include:example.com".to_string(),
312        };
313        assert_eq!(format!("{}", txt), "\"v=spf1 include:example.com\"");
314
315        let srv = RecordData::SRV {
316            priority: 10,
317            weight: 5,
318            port: 443,
319            target: "server.example.com".to_string(),
320        };
321        assert_eq!(format!("{}", srv), "10 5 443 server.example.com");
322    }
323
324    #[test]
325    fn test_record_serialization_roundtrip() {
326        let record = DnsRecord {
327            name: "example.com".to_string(),
328            record_type: RecordType::A,
329            ttl: 300,
330            data: RecordData::A {
331                address: "1.2.3.4".to_string(),
332            },
333        };
334        let json = serde_json::to_string(&record).unwrap();
335        assert!(json.contains("\"A\""));
336        assert!(json.contains("1.2.3.4"));
337    }
338
339    #[test]
340    fn test_soa_display() {
341        let soa = RecordData::SOA {
342            mname: "ns1.example.com".to_string(),
343            rname: "admin.example.com".to_string(),
344            serial: 2024010101,
345            refresh: 3600,
346            retry: 900,
347            expire: 604800,
348            minimum: 86400,
349        };
350        let display = format!("{}", soa);
351        assert!(display.contains("ns1.example.com"));
352        assert!(display.contains("2024010101"));
353    }
354
355    #[test]
356    fn test_naptr_display() {
357        // dig-style: order preference "flags" "services" "regexp" replacement
358        let naptr = RecordData::NAPTR {
359            order: 100,
360            preference: 50,
361            flags: "s".to_string(),
362            services: "http+N2L+N2C+N2R".to_string(),
363            regexp: String::new(),
364            replacement: "www.example.com.".to_string(),
365        };
366        assert_eq!(
367            format!("{}", naptr),
368            "100 50 \"s\" \"http+N2L+N2C+N2R\" \"\" www.example.com."
369        );
370    }
371}