Skip to main content

walnut_dns/rr/
zone.rs

1use std::{collections::BTreeMap, fs, io, path::Path};
2
3use hickory_proto::{
4    rr::{DNSClass, LowerName, RecordType, RrKey, rdata},
5    serialize::txt::Parser,
6};
7use rusqlite::{ToSql, types::FromSql};
8use tracing::{debug, error, info};
9
10use crate::{
11    authority::{Lookup, ZoneAuthority, ZoneInfo},
12    database::FromRow,
13};
14
15use super::{
16    Name, Record, SerialNumber, SqlName, TimeToLive, ZoneID,
17    rset::{Mismatch, RecordSet},
18};
19
20/// The authoratative nature of this zone.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
22pub enum ZoneType {
23    /// This is a primary nameserver, and holds the zone files of record.
24    Primary = 1,
25    /// This nameserver replicates the zone files of recrod.
26    Secondary = 2,
27    /// This nameserver provides exteranal zone data
28    External = 3,
29}
30
31impl From<ZoneType> for hickory_server::authority::ZoneType {
32    fn from(value: ZoneType) -> Self {
33        match value {
34            ZoneType::Primary => hickory_server::authority::ZoneType::Primary,
35            ZoneType::Secondary => hickory_server::authority::ZoneType::Secondary,
36            ZoneType::External => hickory_server::authority::ZoneType::External,
37        }
38    }
39}
40
41impl From<hickory_server::authority::ZoneType> for ZoneType {
42    fn from(value: hickory_server::authority::ZoneType) -> Self {
43        match value {
44            hickory_server::authority::ZoneType::Primary => ZoneType::Primary,
45            hickory_server::authority::ZoneType::Secondary => ZoneType::Secondary,
46            hickory_server::authority::ZoneType::External => ZoneType::External,
47            _ => panic!("Deprecated zone type"),
48        }
49    }
50}
51
52impl ToSql for ZoneType {
53    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
54        match self {
55            ZoneType::Primary => Ok(rusqlite::types::ToSqlOutput::Owned(
56                rusqlite::types::Value::Integer(1),
57            )),
58            ZoneType::Secondary => Ok(rusqlite::types::ToSqlOutput::Owned(
59                rusqlite::types::Value::Integer(2),
60            )),
61            ZoneType::External => Ok(rusqlite::types::ToSqlOutput::Owned(
62                rusqlite::types::Value::Integer(3),
63            )),
64        }
65    }
66}
67
68impl FromSql for ZoneType {
69    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
70        match value {
71            rusqlite::types::ValueRef::Integer(1) => Ok(ZoneType::Primary),
72            rusqlite::types::ValueRef::Integer(2) => Ok(ZoneType::Secondary),
73            rusqlite::types::ValueRef::Integer(3) => Ok(ZoneType::External),
74            rusqlite::types::ValueRef::Integer(i) => {
75                Err(rusqlite::types::FromSqlError::OutOfRange(i))
76            }
77            _ => Err(rusqlite::types::FromSqlError::InvalidType),
78        }
79    }
80}
81
82/// Represents a DNS Zone/Authority and associated records
83#[derive(Debug, Clone)]
84pub struct Zone {
85    id: ZoneID,
86    zone_type: ZoneType,
87    name: Name,
88    origin: LowerName,
89    allow_axfr: bool,
90    dns_class: DNSClass,
91    records: BTreeMap<RrKey, RecordSet>,
92}
93
94impl Zone {
95    /// Create a new empty zone with only a SOA record
96    ///
97    /// Creates a new DNS zone containing only the required SOA record.
98    /// Additional records can be added later using the zone's methods.
99    ///
100    /// # Arguments
101    ///
102    /// * `name` - The zone name (e.g., "example.com")
103    /// * `soa` - The Start of Authority record for this zone
104    /// * `zone_type` - The type of zone (Primary, Secondary, External)
105    /// * `allow_axfr` - Whether to allow zone transfers (AXFR)
106    ///
107    /// # Returns
108    ///
109    /// A new zone instance
110    pub fn empty(
111        name: Name,
112        soa: Record<rdata::SOA>,
113        zone_type: ZoneType,
114        allow_axfr: bool,
115    ) -> Self {
116        let mut records = BTreeMap::new();
117        records.insert(soa.rrkey(), RecordSet::from_record(name.clone(), soa));
118        let origin = (&name).into();
119
120        Self {
121            id: ZoneID::new(),
122            zone_type,
123            name,
124            origin,
125            allow_axfr,
126            dns_class: DNSClass::IN,
127            records,
128        }
129    }
130
131    /// Create a zone from an iterator of record sets
132    ///
133    /// Creates a new DNS zone from a collection of record sets. This is useful
134    /// when loading a zone from a file or database.
135    ///
136    /// # Arguments
137    ///
138    /// * `name` - The zone name
139    /// * `records` - An iterator of record sets to include in the zone
140    /// * `zone_type` - The type of zone (Primary, Secondary, External)
141    ///
142    /// # Returns
143    ///
144    /// A new zone instance containing the provided records
145    pub fn from_rrsets(
146        name: Name,
147        records: impl Iterator<Item = RecordSet>,
148        zone_type: ZoneType,
149    ) -> Self {
150        let mut rrsets = BTreeMap::new();
151        for record in records {
152            rrsets.insert(record.rrkey(), record);
153        }
154        let origin = (&name).into();
155
156        Self {
157            id: ZoneID::new(),
158            zone_type,
159            name,
160            origin,
161            allow_axfr: false,
162            dns_class: DNSClass::IN,
163            records: rrsets,
164        }
165    }
166}
167
168impl Zone {
169    /// Get the unique database identifier for this zone
170    ///
171    /// Returns the unique ID assigned to this zone when it was created.
172    /// This ID is used internally for database operations.
173    ///
174    /// # Returns
175    ///
176    /// The unique zone identifier
177    pub fn id(&self) -> ZoneID {
178        self.id
179    }
180
181    /// Get the name of this zone
182    ///
183    /// Returns the fully qualified domain name that this zone is authoritative for.
184    ///
185    /// # Returns
186    ///
187    /// A reference to the zone name
188    pub fn name(&self) -> &Name {
189        &self.name
190    }
191
192    /// Get the type of this zone
193    ///
194    /// Returns whether this zone is Primary (authoritative), Secondary (slave),
195    /// or External (forwarding).
196    ///
197    /// # Returns
198    ///
199    /// The zone type
200    pub fn zone_type(&self) -> ZoneType {
201        self.zone_type
202    }
203
204    /// Check if zone transfers are allowed
205    ///
206    /// Returns whether this zone permits AXFR (zone transfer) requests.
207    ///
208    /// # Returns
209    ///
210    /// `true` if zone transfers are allowed
211    pub fn allow_axfr(&self) -> bool {
212        self.allow_axfr
213    }
214
215    /// Set whether to allow zone transfers
216    ///
217    /// Enables or disables AXFR (zone transfer) requests for this zone.
218    ///
219    /// # Arguments
220    ///
221    /// * `allow_axfr` - Whether to allow zone transfers
222    ///
223    /// # Returns
224    ///
225    /// A mutable reference to this zone for method chaining
226    pub fn set_allow_axfr(&mut self, allow_axfr: bool) -> &mut Self {
227        self.allow_axfr = allow_axfr;
228        self
229    }
230
231    /// Set the DNS class of this zone
232    ///
233    /// Updates the DNS class (typically IN for Internet) for this zone.
234    ///
235    /// # Arguments
236    ///
237    /// * `dns_class` - The new DNS class
238    ///
239    /// # Returns
240    ///
241    /// A mutable reference to this zone for method chaining
242    pub fn set_dns_class(&mut self, dns_class: DNSClass) -> &mut Self {
243        self.dns_class = dns_class;
244        self
245    }
246
247    /// Get an iterator over all records in this zone
248    ///
249    /// Returns an iterator that yields all DNS records in the zone,
250    /// including signed records (with RRSIG signatures).
251    ///
252    /// # Returns
253    ///
254    /// An iterator over all records in the zone
255    pub fn records(&self) -> impl Iterator<Item = &Record> {
256        self.records
257            .values()
258            .flat_map(|rrset| rrset.signed_records())
259    }
260
261    /// Replace a record set in this zone
262    ///
263    /// Replaces an existing record set with a new one, or inserts the new
264    /// record set if no matching one exists.
265    ///
266    /// # Arguments
267    ///
268    /// * `rrset` - The record set to insert or replace
269    ///
270    /// # Returns
271    ///
272    /// The previously existing record set, or None if this is a new insertion
273    pub fn replace(&mut self, rrset: RecordSet) -> Option<RecordSet> {
274        let key = rrset.rrkey();
275        self.records.insert(key, rrset)
276    }
277
278    /// Check if this zone is empty
279    ///
280    /// Returns true if the zone contains no record sets. Note that a zone
281    /// with only a SOA record is not considered empty.
282    ///
283    /// # Returns
284    ///
285    /// `true` if the zone contains no record sets
286    pub fn is_empty(&self) -> bool {
287        self.records.is_empty()
288    }
289}
290
291impl FromRow for Zone {
292    fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
293        let id = row.get("id")?;
294        let name: Name = row.get::<_, SqlName>("name")?.into();
295        let zone_type = row.get("zone_type")?;
296        let allow_axfr = row.get("allow_axfr")?;
297        let dns_class = row.get::<_, u16>("dns_class")?.into();
298        let origin = (&name).into();
299
300        Ok(Zone {
301            id,
302            name,
303            origin,
304            zone_type,
305            allow_axfr,
306            dns_class,
307            records: Default::default(),
308        })
309    }
310}
311
312impl ZoneInfo for Zone {
313    fn name(&self) -> &Name {
314        &self.name
315    }
316
317    fn origin(&self) -> &LowerName {
318        &self.origin
319    }
320
321    fn zone_type(&self) -> ZoneType {
322        self.zone_type
323    }
324
325    fn is_axfr_allowed(&self) -> bool {
326        self.allow_axfr
327    }
328
329    fn dns_class(&self) -> DNSClass {
330        self.dns_class
331    }
332
333    fn soa(&self) -> Option<&Record> {
334        let rrset = self
335            .records
336            .get(&RrKey::new(self.name().into(), RecordType::SOA))?;
337
338        rrset.records().next()
339    }
340
341    /// The serial number of this zone, from the SOA record.
342    fn serial(&self) -> SerialNumber {
343        self.soa()
344            .and_then(|soa| soa.rdata().as_soa())
345            .map_or(SerialNumber::ZERO, |soa| SerialNumber::from(soa.serial()))
346    }
347
348    /// The minimum TTL for this zone, from the SOA record.
349    fn minimum_ttl(&self) -> TimeToLive {
350        self.soa()
351            .and_then(|soa| soa.rdata().as_soa())
352            .map_or(TimeToLive::ZERO, |soa| TimeToLive::from(soa.minimum()))
353    }
354
355    /// Increment the serial number of this zone's SOA record.
356    fn increment_soa_serial(&mut self) -> SerialNumber {
357        let Some(rrset) = self
358            .records
359            .get_mut(&RrKey::new(self.name().into(), RecordType::SOA))
360        else {
361            return SerialNumber::ZERO;
362        };
363
364        let Some(soa_data) = rrset
365            .records_mut()
366            .next()
367            .and_then(|record| record.rdata_mut().as_soa_mut())
368        else {
369            return SerialNumber::ZERO;
370        };
371
372        soa_data.increment_serial();
373        SerialNumber::from(soa_data.serial())
374    }
375}
376
377impl Lookup for Zone {
378    fn get(&self, key: &RrKey) -> Option<&RecordSet> {
379        self.records.get(key)
380    }
381
382    fn get_mut(&mut self, key: &RrKey) -> Option<&mut RecordSet> {
383        self.records.get_mut(key)
384    }
385
386    fn keys(&self) -> impl Iterator<Item = &RrKey> {
387        self.records.keys()
388    }
389
390    fn records(&self) -> impl Iterator<Item = &RecordSet> {
391        self.records.values()
392    }
393
394    fn records_mut(&mut self) -> impl Iterator<Item = &mut RecordSet> {
395        self.records.values_mut()
396    }
397
398    fn records_reversed(&self) -> impl Iterator<Item = &RecordSet> {
399        self.records.values().rev()
400    }
401
402    fn upsert(&mut self, record: Record, serial: SerialNumber) -> Result<bool, Mismatch> {
403        if record.dns_class() != self.dns_class() {
404            return Err(Mismatch("DNS Class"));
405        }
406
407        fn is_nsec(upsert_type: RecordType, occupied_type: RecordType) -> bool {
408            // NSEC is always allowed
409            upsert_type == RecordType::NSEC
410                || upsert_type == RecordType::NSEC3
411                || occupied_type == RecordType::NSEC
412                || occupied_type == RecordType::NSEC3
413        }
414
415        fn label_does_not_allow_multiple(
416            upsert_type: RecordType,
417            occupied_type: RecordType,
418            check_type: RecordType,
419        ) -> bool {
420            // it's a CNAME/ANAME but there's a record that's not a CNAME/ANAME at this location
421            (upsert_type == check_type && occupied_type != check_type) ||
422                // it's a different record, but there is already a CNAME/ANAME here
423                (upsert_type != check_type && occupied_type == check_type)
424        }
425
426        let start_range_key = RrKey::new(record.name().into(), RecordType::Unknown(u16::MIN));
427        let end_range_key = RrKey::new(record.name().into(), RecordType::Unknown(u16::MAX));
428
429        let multiple_records_at_label_disallowed = self
430            .records
431            .range(&start_range_key..&end_range_key)
432            // remember CNAME can be the only record at a particular label
433            .any(|(key, _)| {
434                !is_nsec(record.record_type(), key.record_type)
435                    && label_does_not_allow_multiple(
436                        record.record_type(),
437                        key.record_type,
438                        RecordType::CNAME,
439                    )
440            });
441
442        if multiple_records_at_label_disallowed {
443            // consider making this an error?
444            return Ok(false);
445        }
446
447        let rrset = self
448            .records
449            .entry(record.rrkey())
450            .or_insert_with(|| RecordSet::new(record.name().clone(), record.record_type()));
451
452        rrset.insert(record, serial)
453    }
454
455    fn range<T, R>(&self, range: R) -> impl Iterator<Item = (&RrKey, &RecordSet)>
456    where
457        T: Ord + ?Sized,
458        RrKey: std::borrow::Borrow<T> + Ord,
459        R: std::ops::RangeBounds<T>,
460    {
461        self.records.range(range)
462    }
463
464    fn remove(&mut self, key: &RrKey) -> Option<RecordSet> {
465        self.records.remove(key)
466    }
467
468    fn replace(&mut self, rrset: RecordSet) -> Option<RecordSet> {
469        self.records.insert(rrset.rrkey(), rrset)
470    }
471}
472
473impl From<Zone> for ZoneAuthority<Zone> {
474    fn from(value: Zone) -> Self {
475        ZoneAuthority::new(value)
476    }
477}
478
479impl Zone {
480    /// Load a zone from a DNS zone file
481    ///
482    /// Reads and parses a DNS zone file, creating a new zone instance
483    /// with the records found in the file.
484    ///
485    /// # Arguments
486    ///
487    /// * `origin` - The zone origin (root name for the zone)
488    /// * `path` - Path to the zone file to read
489    /// * `zone_type` - The type of zone to create
490    ///
491    /// # Returns
492    ///
493    /// A new zone instance loaded from the file
494    ///
495    /// # Errors
496    ///
497    /// Returns an error if the file cannot be read or parsed
498    pub fn read_from_file(
499        origin: Name,
500        path: impl AsRef<Path>,
501        zone_type: ZoneType,
502    ) -> io::Result<Self> {
503        let zone_path = path.as_ref();
504        info!("loading zone file: {:?}", zone_path);
505
506        let buf = fs::read_to_string(zone_path).inspect_err(|e| {
507            error!("failed to read {}: {:?}", zone_path.display(), e);
508        })?;
509
510        let (origin, records) = Parser::new(buf, Some(zone_path.to_path_buf()), Some(origin))
511            .parse()
512            .map_err(|e| {
513                error!("failed to parse {}: {:?}", zone_path.display(), e);
514                io::Error::other(e)
515            })?;
516
517        info!(
518            "zone file loaded: {} with {} records",
519            origin,
520            records.len()
521        );
522        debug!("zone: {:#?}", records);
523
524        let records = records.into_values().map(Into::into);
525        let zone = Zone::from_rrsets(origin, records, zone_type);
526        Ok(zone)
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use hickory_proto::rr::{RecordType, rdata::A};
534
535    fn create_test_name() -> Name {
536        Name::from_utf8("test.example.com").unwrap()
537    }
538
539    fn create_test_soa() -> Record<rdata::SOA> {
540        let name = create_test_name();
541        let soa = rdata::SOA::new(
542            name.clone(),
543            Name::from_utf8("admin.example.com").unwrap(),
544            1,
545            3600,
546            1800,
547            604800,
548            86400,
549        );
550        Record::from_rdata(name, TimeToLive::from(3600), soa)
551    }
552
553    #[test]
554    fn test_zone_type_conversions() {
555        // Test to hickory_server::authority::ZoneType
556        assert_eq!(
557            hickory_server::authority::ZoneType::from(ZoneType::Primary),
558            hickory_server::authority::ZoneType::Primary
559        );
560        assert_eq!(
561            hickory_server::authority::ZoneType::from(ZoneType::Secondary),
562            hickory_server::authority::ZoneType::Secondary
563        );
564        assert_eq!(
565            hickory_server::authority::ZoneType::from(ZoneType::External),
566            hickory_server::authority::ZoneType::External
567        );
568
569        // Test from hickory_server::authority::ZoneType
570        assert_eq!(
571            ZoneType::from(hickory_server::authority::ZoneType::Primary),
572            ZoneType::Primary
573        );
574        assert_eq!(
575            ZoneType::from(hickory_server::authority::ZoneType::Secondary),
576            ZoneType::Secondary
577        );
578        assert_eq!(
579            ZoneType::from(hickory_server::authority::ZoneType::External),
580            ZoneType::External
581        );
582    }
583
584    #[test]
585    fn test_zone_type_debug() {
586        assert!(format!("{:?}", ZoneType::Primary).contains("Primary"));
587        assert!(format!("{:?}", ZoneType::Secondary).contains("Secondary"));
588        assert!(format!("{:?}", ZoneType::External).contains("External"));
589    }
590
591    #[test]
592    fn test_zone_creation() {
593        let name = create_test_name();
594        let soa = create_test_soa();
595        let zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
596
597        assert_eq!(zone.name(), &name);
598        assert_eq!(zone.zone_type(), ZoneType::Primary);
599        assert_eq!(zone.dns_class(), DNSClass::IN);
600        assert!(!zone.allow_axfr());
601        assert_eq!(zone.records().count(), 1); // SOA record
602        assert!(!zone.is_empty()); // Has SOA record
603    }
604
605    #[test]
606    fn test_zone_with_allow_axfr() {
607        let name = create_test_name();
608        let soa = create_test_soa();
609
610        let mut zone = Zone::empty(
611            name.clone(),
612            soa,
613            ZoneType::Primary,
614            true, // allow_axfr
615        );
616
617        assert_eq!(zone.name(), &name);
618        assert_eq!(zone.zone_type(), ZoneType::Primary);
619        assert!(zone.allow_axfr());
620
621        // Test set_allow_axfr
622        zone.set_allow_axfr(false);
623        assert!(!zone.allow_axfr());
624    }
625
626    #[test]
627    fn test_zone_upsert_record() {
628        let name = create_test_name();
629        let soa = create_test_soa();
630        let mut zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
631
632        // Add A record
633        let a_record = Record::from_rdata(name, TimeToLive::from(300), A::new(192, 168, 1, 1))
634            .into_record_rdata();
635
636        let result = zone.upsert(a_record.clone(), SerialNumber::from(1));
637        assert!(result.is_ok());
638        assert!(result.unwrap());
639        assert_eq!(zone.records().count(), 2); // SOA + A record
640    }
641
642    #[test]
643    fn test_zone_remove_record() {
644        let name = create_test_name();
645        let soa = create_test_soa();
646        let mut zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
647
648        // Add A record
649        let a_record = Record::from_rdata(name, TimeToLive::from(300), A::new(192, 168, 1, 1))
650            .into_record_rdata();
651
652        zone.upsert(a_record.clone(), SerialNumber::from(1))
653            .unwrap();
654        assert_eq!(zone.records().count(), 2);
655
656        // Remove A record (using the key)
657        let key = a_record.rrkey();
658        let removed = zone.remove(&key);
659        assert!(removed.is_some());
660        assert_eq!(zone.records().count(), 1); // Only SOA remains
661    }
662
663    #[test]
664    fn test_zone_get_by_key() {
665        let name = create_test_name();
666        let soa = create_test_soa();
667        let mut zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
668
669        // Add A record
670        let a_record =
671            Record::from_rdata(name.clone(), TimeToLive::from(300), A::new(192, 168, 1, 1))
672                .into_record_rdata();
673
674        zone.upsert(a_record.clone(), SerialNumber::from(1))
675            .unwrap();
676
677        // Test get by key
678        let key = a_record.rrkey();
679        let result = zone.get(&key);
680        assert!(result.is_some());
681
682        let rrset = result.unwrap();
683        assert_eq!(rrset.record_type(), RecordType::A);
684        assert_eq!(rrset.len(), 1);
685    }
686
687    #[test]
688    fn test_zone_soa_access() {
689        let name = create_test_name();
690        let soa = create_test_soa();
691        let zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
692
693        // Test SOA access
694        let soa_record = zone.soa().unwrap();
695        assert_eq!(soa_record.record_type(), RecordType::SOA);
696
697        // Test serial number
698        let serial = zone.serial();
699        assert_eq!(serial.get(), 1); // From our test SOA
700    }
701
702    #[test]
703    fn test_zone_is_empty() {
704        let name = create_test_name();
705        let soa = create_test_soa();
706        let zone = Zone::empty(name, soa, ZoneType::Primary, false);
707
708        // Zone with only SOA is not considered empty
709        assert!(!zone.is_empty());
710    }
711
712    #[test]
713    fn test_zone_iterators() {
714        let name = create_test_name();
715        let soa = create_test_soa();
716        let mut zone = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
717
718        // Add A record
719        let a_record = Record::from_rdata(name, TimeToLive::from(300), A::new(192, 168, 1, 1))
720            .into_record_rdata();
721
722        zone.upsert(a_record, SerialNumber::from(1)).unwrap();
723
724        // Test records iterator
725        assert_eq!(zone.records().count(), 2); // SOA + A
726    }
727
728    #[test]
729    fn test_zone_properties() {
730        let name = create_test_name();
731        let soa = create_test_soa();
732        let zone1 = Zone::empty(name.clone(), soa, ZoneType::Primary, false);
733
734        // Test that zones can be inspected (no Clone trait)
735        assert_eq!(zone1.name(), &name);
736        assert_eq!(zone1.zone_type(), ZoneType::Primary);
737        assert_eq!(zone1.records().count(), 1);
738    }
739
740    #[test]
741    fn test_zone_id_uniqueness() {
742        let name = create_test_name();
743        let zone1 = Zone::empty(name.clone(), create_test_soa(), ZoneType::Primary, false);
744        let zone2 = Zone::empty(name, create_test_soa(), ZoneType::Primary, false);
745
746        assert_ne!(zone1.id(), zone2.id());
747    }
748}