Skip to main content

hickory_proto/rr/rdata/
soa.rs

1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! start of authority record defining ownership and defaults for the zone
9
10use alloc::string::ToString;
11use core::fmt;
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    error::ProtoResult,
18    rr::{RData, RecordData, RecordType, domain::Name},
19    serialize::{
20        binary::{BinDecodable, BinDecoder, BinEncodable, BinEncoder, DecodeError, RDataEncoding},
21        txt::{ParseError, parse_ttl},
22    },
23};
24
25/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035)
26///
27/// ```text
28/// 3.3.13. SOA RDATA format
29///
30///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
31///     /                     MNAME                     /
32///     /                                               /
33///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
34///     /                     RNAME                     /
35///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
36///     |                    SERIAL                     |
37///     |                                               |
38///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
39///     |                    REFRESH                    |
40///     |                                               |
41///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
42///     |                     RETRY                     |
43///     |                                               |
44///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
45///     |                    EXPIRE                     |
46///     |                                               |
47///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
48///     |                    MINIMUM                    |
49///     |                                               |
50///     +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
51///
52/// where:
53///
54/// SOA records cause no additional section processing.
55///
56/// All times are in units of seconds.
57///
58/// Most of these fields are pertinent only for name server maintenance
59/// operations.  However, MINIMUM is used in all query operations that
60/// retrieve RRs from a zone.  Whenever a RR is sent in a response to a
61/// query, the TTL field is set to the maximum of the TTL field from the RR
62/// and the MINIMUM field in the appropriate SOA.  Thus MINIMUM is a lower
63/// bound on the TTL field for all RRs in a zone.  Note that this use of
64/// MINIMUM should occur when the RRs are copied into the response and not
65/// when the zone is loaded from a Zone File or via a zone transfer.  The
66/// reason for this provision is to allow future dynamic update facilities to
67/// change the SOA RR with known semantics.
68/// ```
69#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
70#[derive(Debug, PartialEq, Eq, Hash, Clone)]
71#[non_exhaustive]
72pub struct SOA {
73    /// The `domain-name` of the name server that was the original or primary source of data for
74    /// this zone, i.e. the Primary Name Server.
75    ///
76    /// ```text
77    /// MNAME           The <domain-name> of the name server that was the
78    ///                 original or primary source of data for this zone.
79    /// ```
80    pub mname: Name,
81
82    /// A `domain-name` which specifies the mailbox of the person responsible for this zone, i.e.
83    /// the responsible name.
84    ///
85    /// ```text
86    /// RNAME           A <domain-name> which specifies the mailbox of the
87    ///                 person responsible for this zone.
88    /// ```
89    pub rname: Name,
90
91    /// The unsigned 32 bit version number of the original copy of the zone. Zone transfers
92    /// preserve this value. This value wraps and should be compared using sequence space arithmetic.
93    ///
94    /// ```text
95    /// SERIAL          The unsigned 32 bit version number of the original copy
96    ///                 of the zone.  Zone transfers preserve this value.  This
97    ///                 value wraps and should be compared using sequence space
98    ///                 arithmetic.
99    /// ```
100    pub serial: u32,
101
102    /// A 32 bit time interval before the zone should be refreshed, in seconds.
103    ///
104    /// ```text
105    /// REFRESH         A 32 bit time interval before the zone should be
106    ///                 refreshed.
107    /// ```
108    pub refresh: i32,
109
110    /// A 32 bit time interval that should elapse before a failed refresh should be retried,
111    /// in seconds.
112    ///
113    /// ```text
114    /// RETRY           A 32 bit time interval that should elapse before a
115    ///                 failed refresh should be retried.
116    /// ```
117    pub retry: i32,
118
119    /// A 32 bit time value that specifies the upper limit on the time interval that can elapse
120    /// before the zone is no longer authoritative, in seconds
121    ///
122    /// ```text
123    /// EXPIRE          A 32 bit time value that specifies the upper limit on
124    ///                 the time interval that can elapse before the zone is no
125    ///                 longer authoritative.
126    /// ```
127    pub expire: i32,
128
129    /// The unsigned 32 bit minimum TTL field that should be exported with any RR from this zone.
130    ///
131    /// ```text
132    /// MINIMUM         The unsigned 32 bit minimum TTL field that should be
133    ///                 exported with any RR from this zone.
134    /// ```
135    pub minimum: u32,
136}
137
138impl SOA {
139    /// Creates a new SOA record data.
140    ///
141    /// # Arguments
142    ///
143    /// * `mname` - the name of the primary or authority for this zone.
144    /// * `rname` - the name of the responsible party for this zone, e.g. an email address.
145    /// * `serial` - the serial number of the zone, used for caching purposes.
146    /// * `refresh` - the amount of time to wait before a zone is resynched.
147    /// * `retry` - the minimum period to wait if there is a failure during refresh.
148    /// * `expire` - the time until this primary is no longer authoritative for the zone.
149    /// * `minimum` - no zone records should have time-to-live values less than this minimum.
150    ///
151    /// # Return value
152    ///
153    /// The newly created SOA record data.
154    pub fn new(
155        mname: Name,
156        rname: Name,
157        serial: u32,
158        refresh: i32,
159        retry: i32,
160        expire: i32,
161        minimum: u32,
162    ) -> Self {
163        Self {
164            mname,
165            rname,
166            serial,
167            refresh,
168            retry,
169            expire,
170            minimum,
171        }
172    }
173
174    /// Parse the RData from a set of Tokens
175    pub(crate) fn from_tokens<'i, I: Iterator<Item = &'i str>>(
176        mut tokens: I,
177        origin: Option<&Name>,
178    ) -> Result<Self, ParseError> {
179        let mname: Name = tokens
180            .next()
181            .ok_or_else(|| ParseError::MissingToken("mname".to_string()))
182            .and_then(|s| Name::parse(s, origin).map_err(ParseError::from))?;
183
184        let rname: Name = tokens
185            .next()
186            .ok_or_else(|| ParseError::MissingToken("rname".to_string()))
187            .and_then(|s| Name::parse(s, origin).map_err(ParseError::from))?;
188
189        let serial: u32 = tokens
190            .next()
191            .ok_or_else(|| ParseError::MissingToken("serial".to_string()))
192            .and_then(parse_ttl)?;
193
194        let refresh: i32 = tokens
195            .next()
196            .ok_or_else(|| ParseError::MissingToken("refresh".to_string()))
197            .and_then(parse_ttl)?
198            .try_into()
199            .map_err(|_e| ParseError::from("refresh outside i32 range"))?;
200
201        let retry: i32 = tokens
202            .next()
203            .ok_or_else(|| ParseError::MissingToken("retry".to_string()))
204            .and_then(parse_ttl)?
205            .try_into()
206            .map_err(|_e| ParseError::from("retry outside i32 range"))?;
207
208        let expire: i32 = tokens
209            .next()
210            .ok_or_else(|| ParseError::MissingToken("expire".to_string()))
211            .and_then(parse_ttl)?
212            .try_into()
213            .map_err(|_e| ParseError::from("expire outside i32 range"))?;
214
215        let minimum: u32 = tokens
216            .next()
217            .ok_or_else(|| ParseError::MissingToken("minimum".to_string()))
218            .and_then(parse_ttl)?;
219
220        Ok(Self::new(
221            mname, rname, serial, refresh, retry, expire, minimum,
222        ))
223    }
224
225    /// Increments the serial number by one
226    pub fn increment_serial(&mut self) {
227        self.serial += 1; // TODO: what to do on overflow?
228    }
229}
230
231impl BinEncodable for SOA {
232    /// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-6), DNSSEC Resource Records, March 2005
233    ///
234    /// This is accurate for all currently known name records.
235    ///
236    /// ```text
237    /// 6.2.  Canonical RR Form
238    ///
239    ///    For the purposes of DNS security, the canonical form of an RR is the
240    ///    wire format of the RR where:
241    ///
242    ///    ...
243    ///
244    ///    3.  if the type of the RR is NS, MD, MF, CNAME, SOA, MB, MG, MR, PTR,
245    ///        HINFO, MINFO, MX, HINFO, RP, AFSDB, RT, SIG, PX, NXT, NAPTR, KX,
246    ///        SRV, DNAME, A6, RRSIG, or (rfc6840 removes NSEC), all uppercase
247    ///        US-ASCII letters in the DNS names contained within the RDATA are replaced
248    ///        by the corresponding lowercase US-ASCII letters;
249    /// ```
250    fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
251        let mut encoder = encoder.with_rdata_behavior(RDataEncoding::StandardRecord);
252
253        self.mname.emit(&mut encoder)?;
254        self.rname.emit(&mut encoder)?;
255        encoder.emit_u32(self.serial)?;
256        encoder.emit_i32(self.refresh)?;
257        encoder.emit_i32(self.retry)?;
258        encoder.emit_i32(self.expire)?;
259        encoder.emit_u32(self.minimum)?;
260        Ok(())
261    }
262}
263
264impl<'r> BinDecodable<'r> for SOA {
265    fn read(decoder: &mut BinDecoder<'r>) -> Result<Self, DecodeError> {
266        Ok(Self {
267            mname: Name::read(decoder)?,
268            rname: Name::read(decoder)?,
269            serial: decoder.read_u32()?.unverified(/*any u32 is valid*/),
270            refresh: decoder.read_i32()?.unverified(/*any i32 is valid*/),
271            retry: decoder.read_i32()?.unverified(/*any i32 is valid*/),
272            expire: decoder.read_i32()?.unverified(/*any i32 is valid*/),
273            minimum: decoder.read_u32()?.unverified(/*any u32 is valid*/),
274        })
275    }
276}
277
278impl RecordData for SOA {
279    fn try_borrow(data: &RData) -> Option<&Self> {
280        match data {
281            RData::SOA(soa) => Some(soa),
282            _ => None,
283        }
284    }
285
286    fn record_type(&self) -> RecordType {
287        RecordType::SOA
288    }
289
290    fn into_rdata(self) -> RData {
291        RData::SOA(self)
292    }
293}
294
295/// [RFC 1033](https://tools.ietf.org/html/rfc1033), DOMAIN OPERATIONS GUIDE, November 1987
296///
297/// ```text
298/// SOA  (Start Of Authority)
299///
300/// <name>  [<ttl>]  [<class>]  SOA  <origin>  <person>  (
301///    <serial>
302///    <refresh>
303///    <retry>
304///    <expire>
305///    <minimum> )
306///
307/// The Start Of Authority record designates the start of a zone.  The
308/// zone ends at the next SOA record.
309///
310/// <name> is the name of the zone.
311///
312/// <origin> is the name of the host on which the master zone file
313/// resides.
314///
315/// <person> is a mailbox for the person responsible for the zone.  It is
316/// formatted like a mailing address but the at-sign that normally
317/// separates the user from the host name is replaced with a dot.
318///
319/// <serial> is the version number of the zone file.  It should be
320/// incremented anytime a change is made to data in the zone.
321///
322/// <refresh> is how long, in seconds, a secondary name server is to
323/// check with the primary name server to see if an update is needed.  A
324/// good value here would be one hour (3600).
325///
326/// <retry> is how long, in seconds, a secondary name server is to retry
327/// after a failure to check for a refresh.  A good value here would be
328/// 10 minutes (600).
329///
330/// <expire> is the upper limit, in seconds, that a secondary name server
331/// is to use the data before it expires for lack of getting a refresh.
332/// You want this to be rather large, and a nice value is 3600000, about
333/// 42 days.
334///
335/// <minimum> is the minimum number of seconds to be used for TTL values
336/// in RRs.  A minimum of at least a day is a good value here (86400).
337///
338/// There should only be one SOA record per zone.  A sample SOA record
339/// would look something like:
340///
341/// @   IN   SOA   SRI-NIC.ARPA.   HOSTMASTER.SRI-NIC.ARPA. (
342///     45         ;serial
343///     3600       ;refresh
344///     600        ;retry
345///     3600000    ;expire
346///     86400 )    ;minimum
347/// ```
348impl fmt::Display for SOA {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
350        write!(
351            f,
352            "{mname} {rname} {serial} {refresh} {retry} {expire} {min}",
353            mname = self.mname,
354            rname = self.rname,
355            serial = self.serial,
356            refresh = self.refresh,
357            retry = self.retry,
358            expire = self.expire,
359            min = self.minimum
360        )
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    #![allow(clippy::dbg_macro, clippy::print_stdout)]
367
368    use alloc::vec::Vec;
369    #[cfg(feature = "std")]
370    use std::println;
371
372    use crate::{rr::RecordDataDecodable, serialize::binary::Restrict};
373
374    use super::*;
375
376    #[test]
377    fn test() {
378        use core::str::FromStr;
379
380        let rdata = SOA::new(
381            Name::from_str("m.example.com.").unwrap(),
382            Name::from_str("r.example.com.").unwrap(),
383            1,
384            2,
385            3,
386            4,
387            5,
388        );
389
390        let mut bytes = Vec::new();
391        let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
392        assert!(rdata.emit(&mut encoder).is_ok());
393        let bytes = encoder.into_bytes();
394        let len = bytes.len() as u16;
395
396        #[cfg(feature = "std")]
397        println!("bytes: {bytes:?}");
398
399        let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes);
400        let read_rdata = SOA::read_data(&mut decoder, Restrict::new(len)).expect("Decoding error");
401        assert_eq!(rdata, read_rdata);
402    }
403
404    #[test]
405    fn test_parse() {
406        use core::str::FromStr;
407
408        let soa_tokens = vec![
409            "hickory-dns.org.",
410            "root.hickory-dns.org.",
411            "199609203",
412            "8h",
413            "120m",
414            "7d",
415            "24h",
416        ];
417
418        let parsed_soa = SOA::from_tokens(
419            soa_tokens.into_iter(),
420            Some(&Name::from_str("example.com.").unwrap()),
421        )
422        .expect("failed to parse tokens");
423
424        let expected_soa = SOA::new(
425            "hickory-dns.org.".parse().unwrap(),
426            "root.hickory-dns.org.".parse().unwrap(),
427            199609203,
428            28800,
429            7200,
430            604800,
431            86400,
432        );
433
434        assert_eq!(parsed_soa, expected_soa);
435    }
436}