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}