Skip to main content

domain/zonetree/
types.rs

1//! Zone tree related types.
2
3use core::future::{Future, ready};
4use core::pin::Pin;
5use core::task::{Context, Poll};
6
7use alloc::boxed::Box;
8use alloc::sync::Arc;
9use alloc::vec;
10use alloc::vec::Vec;
11use core::ops;
12use std::collections::{HashMap, hash_map};
13
14use bytes::Bytes;
15use futures_util::stream;
16use serde::{Deserialize, Serialize};
17use tracing::trace;
18
19use super::traits::{ZoneDiff, ZoneDiffItem};
20use crate::base::name::Name;
21use crate::base::rdata::RecordData;
22use crate::base::record::Record;
23use crate::base::{Serial, ToName};
24use crate::base::{Ttl, iana::Rtype};
25use crate::rdata::ZoneRecordData;
26
27//------------ Type Aliases --------------------------------------------------
28
29/// A [`Bytes`] backed [`Name`].
30pub type StoredName = Name<Bytes>;
31
32/// A [`Bytes`] backed [`ZoneRecordData`].
33pub type StoredRecordData = ZoneRecordData<Bytes, StoredName>;
34
35/// A [`Bytes`] backed [`Record`].`
36pub type StoredRecord = Record<StoredName, StoredRecordData>;
37
38//------------ SharedRr ------------------------------------------------------
39
40/// A cheaply clonable resource record.
41///
42/// A [`Bytes`] backed resource record which is cheap to [`Clone`] because
43/// [`Bytes`] is cheap to clone.
44#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
45pub struct SharedRr {
46    ttl: Ttl,
47    data: StoredRecordData,
48}
49
50impl SharedRr {
51    /// Create a new [`SharedRr`] instance.
52    pub fn new(ttl: Ttl, data: StoredRecordData) -> Self {
53        SharedRr { ttl, data }
54    }
55
56    /// Gets the type of this resource record.
57    pub fn rtype(&self) -> Rtype {
58        self.data.rtype()
59    }
60
61    /// Gets the TTL of this resource record.
62    pub fn ttl(&self) -> Ttl {
63        self.ttl
64    }
65
66    /// Gets a reference to the data of this resource record.
67    pub fn data(&self) -> &StoredRecordData {
68        &self.data
69    }
70}
71
72impl From<StoredRecord> for SharedRr {
73    fn from(record: StoredRecord) -> Self {
74        SharedRr {
75            ttl: record.ttl(),
76            data: record.into_data(),
77        }
78    }
79}
80
81//------------ Rrset ---------------------------------------------------------
82
83/// A set of related resource records for use with [`Zone`]s.
84///
85/// This type should be used to create and edit one or more resource records
86/// for use with a [`Zone`]. RRset records should all have the same type and
87/// TTL but differing data, as defined by [RFC 9499 section 5.1.3].
88///
89/// [`Zone`]: crate::zonetree::Zone
90/// [RFC 9499 section 5.1.3]:
91///     https://datatracker.ietf.org/doc/html/rfc9499#section-5-1.3
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub struct Rrset {
94    rtype: Rtype,
95    ttl: Ttl,
96    data: Vec<StoredRecordData>,
97}
98
99impl Rrset {
100    /// Creates a new RRset.
101    pub fn new(rtype: Rtype, ttl: Ttl) -> Self {
102        Rrset {
103            rtype,
104            ttl,
105            data: Vec::new(),
106        }
107    }
108
109    /// Gets the common type of each record in the RRset.
110    pub fn rtype(&self) -> Rtype {
111        self.rtype
112    }
113
114    /// Gets the common TTL of each record in the RRset.
115    pub fn ttl(&self) -> Ttl {
116        self.ttl
117    }
118
119    /// Gets the data for each record in the RRset.
120    pub fn data(&self) -> &[StoredRecordData] {
121        &self.data
122    }
123
124    /// Returns true if this RRset has no resource records, false otherwise.
125    pub fn is_empty(&self) -> bool {
126        self.data.is_empty()
127    }
128
129    /// Gets the first RRset record, if any.
130    pub fn first(&self) -> Option<SharedRr> {
131        self.data.first().map(|data| SharedRr {
132            ttl: self.ttl,
133            data: data.clone(),
134        })
135    }
136
137    /// Changesthe TTL of every record in the RRset.
138    pub fn set_ttl(&mut self, ttl: Ttl) {
139        self.ttl = ttl;
140    }
141
142    /// Limits the TTL of every record in the RRSet.
143    ///
144    /// If the TTL currently exceeds the given limit it will be set to the
145    /// limit.
146    pub fn limit_ttl(&mut self, ttl: Ttl) {
147        if self.ttl > ttl {
148            self.ttl = ttl
149        }
150    }
151
152    /// Adds a resource record to the RRset.
153    ///
154    /// # Panics
155    ///
156    /// This function will panic if the provided record data is for a
157    /// different type than the RRset.
158    pub fn push_data(&mut self, data: StoredRecordData) {
159        assert_eq!(data.rtype(), self.rtype);
160        self.data.push(data);
161    }
162
163    /// Adds a resource record to the RRset, limiting the TTL to that of the
164    /// new record.
165    ///
166    /// See [`Self::limit_ttl`] and [`Self::push_data`].
167    pub fn push_record(&mut self, record: StoredRecord) {
168        self.limit_ttl(record.ttl());
169        self.push_data(record.into_data());
170    }
171
172    /// Converts this [`Rrset`] to an [`SharedRrset`].
173    pub fn into_shared(self) -> SharedRrset {
174        SharedRrset::new(self)
175    }
176}
177
178impl From<StoredRecord> for Rrset {
179    fn from(record: StoredRecord) -> Self {
180        Rrset {
181            rtype: record.rtype(),
182            ttl: record.ttl(),
183            data: vec![record.into_data()],
184        }
185    }
186}
187
188//------------ SharedRrset ---------------------------------------------------
189
190/// An RRset behind an [`Arc`] for use with [`Zone`]s.
191///
192/// See [`Rrset`] for more information.
193///
194/// [`Zone`]: crate::zonetree::Zone
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct SharedRrset(Arc<Rrset>);
197
198impl SharedRrset {
199    /// Creates a new RRset.
200    pub fn new(rrset: Rrset) -> Self {
201        SharedRrset(Arc::new(rrset))
202    }
203
204    /// Gets a reference to the inner [`Rrset`].
205    pub fn as_rrset(&self) -> &Rrset {
206        self.0.as_ref()
207    }
208}
209
210//--- Deref, AsRef, Borrow
211
212impl ops::Deref for SharedRrset {
213    type Target = Rrset;
214
215    fn deref(&self) -> &Self::Target {
216        self.as_rrset()
217    }
218}
219
220impl AsRef<Rrset> for SharedRrset {
221    fn as_ref(&self) -> &Rrset {
222        self.as_rrset()
223    }
224}
225
226//--- Deserialize and Serialize
227
228impl<'de> Deserialize<'de> for SharedRrset {
229    fn deserialize<D: serde::Deserializer<'de>>(
230        deserializer: D,
231    ) -> Result<Self, D::Error> {
232        Rrset::deserialize(deserializer).map(SharedRrset::new)
233    }
234}
235
236impl Serialize for SharedRrset {
237    fn serialize<S: serde::Serializer>(
238        &self,
239        serializer: S,
240    ) -> Result<S::Ok, S::Error> {
241        self.as_rrset().serialize(serializer)
242    }
243}
244
245//------------ ZoneCut -------------------------------------------------------
246
247/// The representation of a zone cut within a zone tree.
248#[derive(Clone, Debug)]
249pub struct ZoneCut {
250    /// The owner name where the zone cut occurs.
251    pub name: StoredName,
252
253    /// The NS record at the zone cut.
254    pub ns: SharedRrset,
255
256    /// The DS record at the zone cut (optional).
257    pub ds: Option<SharedRrset>,
258
259    /// Zero or more glue records at the zone cut.
260    pub glue: Vec<StoredRecord>,
261}
262
263//------------ InMemoryZoneDiffBuilder ----------------------------------------
264
265/// An [`InMemoryZoneDiff`] builder.
266///
267/// Removes are assumed to occur before adds.
268#[derive(Debug, Default)]
269pub struct InMemoryZoneDiffBuilder {
270    /// The records added to the Zone.
271    added: HashMap<(StoredName, Rtype), SharedRrset>,
272
273    /// The records removed from the Zone.
274    removed: HashMap<(StoredName, Rtype), SharedRrset>,
275}
276
277impl InMemoryZoneDiffBuilder {
278    /// Creates a new instance of the builder.
279    pub fn new() -> Self {
280        Default::default()
281    }
282
283    /// Record in the diff that a resource record was added.
284    pub fn add(
285        &mut self,
286        owner: StoredName,
287        rtype: Rtype,
288        rrset: SharedRrset,
289    ) {
290        self.added.insert((owner, rtype), rrset);
291    }
292
293    /// Record in the diff that a resource record was removed.
294    pub fn remove(
295        &mut self,
296        owner: StoredName,
297        rtype: Rtype,
298        rrset: SharedRrset,
299    ) {
300        self.removed.insert((owner, rtype), rrset);
301    }
302
303    /// Exchange this builder instnace for an immutable [`ZoneDiff`].
304    ///
305    /// The start serial should be the zone version to which the diffs should
306    /// be applied. The end serial denotes the zone version that results from
307    /// applying this diff.
308    ///
309    /// Note: No check is currently done that the start and end serials match
310    /// the SOA records in the removed and added records contained within the
311    /// diff.
312    pub fn build(self) -> Result<InMemoryZoneDiff, ZoneDiffError> {
313        InMemoryZoneDiff::new(self.added, self.removed)
314    }
315}
316
317//------------ InMemoryZoneDiff -----------------------------------------------
318
319/// The differences between one serial and another for a DNS zone.
320///
321/// Removes are assumed to occur before adds.
322#[derive(Clone, Debug)]
323pub struct InMemoryZoneDiff {
324    /// The serial number of the zone which was modified.
325    pub start_serial: Serial,
326
327    /// The serial number of the zone that resulted from the modifications.
328    pub end_serial: Serial,
329
330    /// The RRsets added to the zone.
331    pub added: Arc<HashMap<(StoredName, Rtype), SharedRrset>>,
332
333    /// The RRsets removed from the zone.
334    pub removed: Arc<HashMap<(StoredName, Rtype), SharedRrset>>,
335}
336
337impl InMemoryZoneDiff {
338    /// Creates a new immutable zone diff.
339    ///
340    /// Returns `Err(ZoneDiffError::MissingStartSoa)` If the removed records
341    /// do not include a zone SOA.
342    ///
343    /// Returns `Err(ZoneDiffError::MissingEndSoa)` If the added records do
344    /// not include a zone SOA.
345    ///
346    /// Returns Ok otherwise.
347    fn new(
348        added: HashMap<(Name<Bytes>, Rtype), SharedRrset>,
349        removed: HashMap<(Name<Bytes>, Rtype), SharedRrset>,
350    ) -> Result<Self, ZoneDiffError> {
351        // Determine the old and new SOA serials by looking at the added and
352        // removed records.
353        let start_serial = removed
354            .iter()
355            .find_map(|((_, rtype), rrset)| {
356                if *rtype == Rtype::SOA {
357                    if let Some(ZoneRecordData::Soa(soa)) =
358                        rrset.data().first()
359                    {
360                        return Some(soa.serial());
361                    }
362                }
363                None
364            })
365            .ok_or(ZoneDiffError::MissingStartSoa)?;
366
367        let end_serial = added
368            .iter()
369            .find_map(|((_, rtype), rrset)| {
370                if *rtype == Rtype::SOA {
371                    if let Some(ZoneRecordData::Soa(soa)) =
372                        rrset.data().first()
373                    {
374                        return Some(soa.serial());
375                    }
376                }
377                None
378            })
379            .ok_or(ZoneDiffError::MissingEndSoa)?;
380
381        if start_serial == end_serial || end_serial < start_serial {
382            trace!(
383                "Diff construction error: serial {start_serial} -> serial {end_serial}:\nremoved: {removed:#?}\nadded: {added:#?}\n"
384            );
385            return Err(ZoneDiffError::InvalidSerialRange);
386        }
387
388        trace!(
389            "Built diff from serial {start_serial} to serial {end_serial}"
390        );
391
392        Ok(Self {
393            start_serial,
394            end_serial,
395            added: added.into(),
396            removed: removed.into(),
397        })
398    }
399}
400
401//--- impl ZoneDiff
402
403impl<'a> ZoneDiffItem for (&'a (StoredName, Rtype), &'a SharedRrset) {
404    fn key(&self) -> &(StoredName, Rtype) {
405        self.0
406    }
407
408    fn value(&self) -> &SharedRrset {
409        self.1
410    }
411}
412
413impl ZoneDiff for InMemoryZoneDiff {
414    type Item<'a>
415        = (&'a (StoredName, Rtype), &'a SharedRrset)
416    where
417        Self: 'a;
418
419    type Stream<'a>
420        = futures_util::stream::Iter<
421        hash_map::Iter<'a, (StoredName, Rtype), SharedRrset>,
422    >
423    where
424        Self: 'a;
425
426    fn start_serial(
427        &self,
428    ) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
429        Box::pin(ready(self.start_serial))
430    }
431
432    fn end_serial(
433        &self,
434    ) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
435        Box::pin(ready(self.end_serial))
436    }
437
438    fn added(&self) -> Self::Stream<'_> {
439        stream::iter(self.added.iter())
440    }
441
442    fn removed(&self) -> Self::Stream<'_> {
443        stream::iter(self.removed.iter())
444    }
445
446    fn get_added(
447        &self,
448        name: impl ToName,
449        rtype: Rtype,
450    ) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
451        Box::pin(ready(self.added.get(&(name.to_name(), rtype))))
452    }
453
454    fn get_removed(
455        &self,
456        name: impl ToName,
457        rtype: Rtype,
458    ) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
459        Box::pin(ready(self.removed.get(&(name.to_name(), rtype))))
460    }
461}
462
463/// The item type used by [`EmptyZoneDiff`].
464pub struct EmptyZoneDiffItem;
465
466impl ZoneDiffItem for EmptyZoneDiffItem {
467    fn key(&self) -> &(StoredName, Rtype) {
468        unreachable!()
469    }
470
471    fn value(&self) -> &SharedRrset {
472        unreachable!()
473    }
474}
475
476/// The stream type used by [`EmptyZoneDiff`].
477#[derive(Debug)]
478pub struct EmptyZoneDiffStream;
479
480impl futures_util::stream::Stream for EmptyZoneDiffStream {
481    type Item = EmptyZoneDiffItem;
482
483    fn poll_next(
484        self: Pin<&mut Self>,
485        _cx: &mut Context<'_>,
486    ) -> Poll<Option<Self::Item>> {
487        Poll::Ready(None)
488    }
489}
490
491/// A [`ZoneDiff`] implementation that is always empty.
492///
493/// Useful when a [`ZoneDiff`] type is needed in a type declaration but for use
494/// by a type that does not support zone difference data.
495#[derive(Debug)]
496pub struct EmptyZoneDiff;
497
498impl ZoneDiff for EmptyZoneDiff {
499    type Item<'a>
500        = EmptyZoneDiffItem
501    where
502        Self: 'a;
503
504    type Stream<'a>
505        = EmptyZoneDiffStream
506    where
507        Self: 'a;
508
509    fn start_serial(
510        &self,
511    ) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
512        Box::pin(ready(Serial(0)))
513    }
514
515    fn end_serial(
516        &self,
517    ) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
518        Box::pin(ready(Serial(0)))
519    }
520
521    fn added(&self) -> Self::Stream<'_> {
522        EmptyZoneDiffStream
523    }
524
525    fn removed(&self) -> Self::Stream<'_> {
526        EmptyZoneDiffStream
527    }
528
529    fn get_added(
530        &self,
531        _name: impl ToName,
532        _rtype: Rtype,
533    ) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
534        Box::pin(ready(None))
535    }
536
537    fn get_removed(
538        &self,
539        _name: impl ToName,
540        _rtype: Rtype,
541    ) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
542        Box::pin(ready(None))
543    }
544}
545
546//------------ ZoneDiffError --------------------------------------------------
547
548/// Creating a [`ZoneDiff`] failed for some reason.
549#[derive(Copy, Clone, Debug, PartialEq, Eq)]
550pub enum ZoneDiffError {
551    /// Missing start SOA.
552    ///
553    /// A zone diff requires a starting SOA.
554    MissingStartSoa,
555
556    /// Missing end SOA.
557    ///
558    /// A zone diff requires a starting SOA.
559    MissingEndSoa,
560
561    /// End SOA serial is equal to or less than the start SOA serial.
562    InvalidSerialRange,
563}
564
565//--- Display
566
567impl core::fmt::Display for ZoneDiffError {
568    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
569        match self {
570            ZoneDiffError::MissingStartSoa => f.write_str("MissingStartSoa"),
571            ZoneDiffError::MissingEndSoa => f.write_str("MissingEndSoa"),
572            ZoneDiffError::InvalidSerialRange => {
573                f.write_str("InvalidSerialRange")
574            }
575        }
576    }
577}
578
579//------------ ZoneUpdate -----------------------------------------------------
580
581/// An update to be applied to a [`Zone`].
582///
583/// # Design
584///
585/// The variants of this enum are modelled after the way the AXFR and IXFR
586/// protocols represent updates to zones.
587///
588/// AXFR responses can be represented as a sequence of
589/// [`ZoneUpdate::AddRecord`]s.
590///
591/// IXFR responses can be represented as a sequence of batches, each
592/// consisting of:
593/// - [`ZoneUpdate::BeginBatchDelete`]
594/// - [`ZoneUpdate::DeleteRecord`]s _(zero or more)_
595/// - [`ZoneUpdate::BeginBatchAdd`]
596/// - [`ZoneUpdate::AddRecord`]s _(zero or more)_
597///
598/// Both AXFR and IXFR responses encoded using this enum are terminated by a
599/// final [`ZoneUpdate::Finished`].
600///
601/// # Use within this crate
602///  
603/// [`XfrResponseInterpreter`] can convert received XFR responses into
604/// sequences of [`ZoneUpdate`]s. These can then be consumed by a
605/// [`ZoneUpdater`] to effect changes to an existing [`Zone`].
606///
607/// # Future extensions
608///
609/// This enum is marked as `#[non_exhaustive]` to permit addition of more
610/// update operations in future, e.g. to support RFC 2136 Dynamic Updates
611/// operations.
612///
613/// [`XfrResponseInterpreter`]:
614///     crate::net::xfr::protocol::XfrResponseInterpreter
615/// [`Zone`]: crate::zonetree::zone::Zone
616/// [`ZoneUpdater`]: crate::zonetree::update::ZoneUpdater
617#[derive(Clone, Debug, PartialEq, Eq)]
618#[non_exhaustive]
619pub enum ZoneUpdate<R> {
620    /// Delete all records in the zone.
621    DeleteAllRecords,
622
623    /// Delete record R from the zone.
624    DeleteRecord(R),
625
626    /// Add record R to the zone.
627    AddRecord(R),
628
629    /// Start a batch delete for the version of the zone with the given SOA
630    /// record.
631    ///
632    /// If not already in batching mode, this signals the start of batching
633    /// mode. In batching mode one or more batches of updates will be
634    /// signalled, each consisting of the sequence:
635    ///
636    /// - ZoneUpdate::BeginBatchDelete
637    /// - ZoneUpdate::DeleteRecord (zero or more)
638    /// - ZoneUpdate::BeginBatchAdd
639    /// - ZoneUpdate::AddRecord (zero or more)
640    ///
641    /// Batching mode can only be terminated by `UpdateComplete` or
642    /// `UpdateIncomplete`.
643    ///
644    /// Batching mode makes updates more predictable for the receiver to work
645    /// with by limiting the updates that can be signalled next, enabling
646    /// receiver logic to be simpler and more efficient.
647    ///
648    /// The record must be a SOA record that matches the SOA record of the
649    /// zone version in which the subsequent [`ZoneUpdate::DeleteRecord`]s
650    /// should be deleted.
651    BeginBatchDelete(R),
652
653    /// Start a batch add for the version of the zone with the given SOA
654    /// record.
655    ///
656    /// This can only be signalled when already in batching mode, i.e. when
657    /// `BeginBatchDelete` has already been signalled.
658    ///
659    /// The record must be the SOA record to use for the new version of the
660    /// zone under which the subsequent [`ZoneUpdate::AddRecord`]s will be
661    /// added.
662    ///
663    /// See `BeginBatchDelete` for more information.
664    BeginBatchAdd(R),
665
666    /// In progress updates for the zone can now be finalized.
667    ///
668    /// This signals the end of a group of related changes for the given SOA
669    /// record of the zone.
670    ///
671    /// For example this could be used to trigger an atomic commit of a set of
672    /// related pending changes.
673    Finished(R),
674}
675
676//--- Display
677
678impl<R> core::fmt::Display for ZoneUpdate<R> {
679    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
680        match self {
681            ZoneUpdate::DeleteAllRecords => f.write_str("DeleteAllRecords"),
682            ZoneUpdate::DeleteRecord(_) => f.write_str("DeleteRecord"),
683            ZoneUpdate::AddRecord(_) => f.write_str("AddRecord"),
684            ZoneUpdate::BeginBatchDelete(_) => {
685                f.write_str("BeginBatchDelete")
686            }
687            ZoneUpdate::BeginBatchAdd(_) => f.write_str("BeginBatchAdd"),
688            ZoneUpdate::Finished(_) => f.write_str("Finished"),
689        }
690    }
691}