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}