From ada5e6bb56c6f459e6b9d940336065a2ef3423d2 Mon Sep 17 00:00:00 2001 From: Weilence Date: Mon, 28 Oct 2024 22:06:29 +0800 Subject: [PATCH 1/5] Add support for NAPTR record type --- src/base/zonefile_fmt.rs | 17 ++ src/rdata/mod.rs | 5 + src/rdata/naptr.rs | 545 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 src/rdata/naptr.rs diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index aa799ef36..3d25ca19a 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -347,4 +347,21 @@ mod test { record.display_zonefile(false).to_string() ); } + + #[test] + fn naptr_record() { + use crate::rdata::Naptr; + let record = create_record(Naptr::, &Name<[u8]>>::new( + 100, + 50, + "a".parse().unwrap(), + "z3950+N2L+N2C".parse().unwrap(), + "".parse().unwrap(), + Name::from_slice(b"\x09cidserver\x07example\x03com\x00").unwrap(), + )); + assert_eq!( + r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "" cidserver.example.com."#, + record.display_zonefile(false).to_string() + ); + } } diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index c5b4828fc..fe3facfed 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -132,4 +132,9 @@ rdata_types! { Zonemd, } } + naptr::{ + zone { + Naptr, + } + } } diff --git a/src/rdata/naptr.rs b/src/rdata/naptr.rs new file mode 100644 index 000000000..964f64e84 --- /dev/null +++ b/src/rdata/naptr.rs @@ -0,0 +1,545 @@ +//! Record data from [RFC 3403]: NAPTR records. +//! +//! This RFC defines the Naptr record type. +//! +//! [RFC 3403]: https://tools.ietf.org/html/rfc3403 + +use crate::base::{ + name::FlattenInto, + rdata::ComposeRecordData, + scan::{Scan, Scanner}, + wire::{Compose, Parse, ParseError}, + zonefile_fmt::{self, Formatter, ZonefileFmt}, + CanonicalOrd, CharStr, ParseRecordData, ParsedName, RecordData, Rtype, + ToName, +}; +use core::{cmp::Ordering, fmt, hash}; +#[cfg(feature = "serde")] +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder}; +use octseq::{Octets, OctetsFrom, OctetsInto, Parser}; + +//------------ Naptr --------------------------------------------------------- + +#[derive(Clone)] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound( + serialize = " + Octs: octseq::serde::SerializeOctets + AsRef<[u8]>, + Name: serde::Serialize, + ", + deserialize = " + Octs: FromBuilder + octseq::serde::DeserializeOctets<'de>, + ::Builder: + OctetsBuilder + EmptyBuilder + + AsRef<[u8]> + AsMut<[u8]>, + Name: serde::Deserialize<'de>, + ", + )) +)] +pub struct Naptr { + order: u16, + preference: u16, + flags: CharStr, + services: CharStr, + regexp: CharStr, + replacement: Name, +} + +impl Naptr<(), ()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::NAPTR; +} + +impl Naptr { + pub fn new( + order: u16, + preference: u16, + flags: CharStr, + services: CharStr, + regexp: CharStr, + replacement: Name, + ) -> Self { + Naptr { + order, + preference, + flags, + services, + regexp, + replacement, + } + } + + pub fn order(&self) -> u16 { + self.order + } + + pub fn preference(&self) -> u16 { + self.preference + } + + pub fn flags(&self) -> &CharStr { + &self.flags + } + + pub fn services(&self) -> &CharStr { + &self.services + } + + pub fn regexp(&self) -> &CharStr { + &self.regexp + } + + pub fn replacement(&self) -> &Name { + &self.replacement + } + + pub(crate) fn convert_octets( + self, + ) -> Result, TOcts::Error> + where + TOcts: OctetsFrom, + TName: OctetsFrom, + { + Ok(Naptr::new( + self.order, + self.preference, + self.flags.try_octets_into()?, + self.services.try_octets_into()?, + self.regexp.try_octets_into()?, + self.replacement.try_octets_into()?, + )) + } + + pub(crate) fn flatten( + self, + ) -> Result, TOcts::Error> + where + TOcts: OctetsFrom, + Name: FlattenInto, + { + Ok(Naptr::new( + self.order, + self.preference, + CharStr::try_octets_into(self.flags)?, + CharStr::try_octets_into(self.services)?, + CharStr::try_octets_into(self.regexp)?, + Name::try_flatten_into(self.replacement)?, + )) + } + + pub fn scan>( + scanner: &mut S, + ) -> Result { + Ok(Self::new( + u16::scan(scanner)?, + u16::scan(scanner)?, + scanner.scan_charstr()?, + scanner.scan_charstr()?, + scanner.scan_charstr()?, + scanner.scan_name()?, + )) + } +} + +impl> Naptr> { + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut octseq::Parser<'a, Src>, + ) -> Result { + Ok(Self::new( + u16::parse(parser)?, + u16::parse(parser)?, + CharStr::parse(parser)?, + CharStr::parse(parser)?, + CharStr::parse(parser)?, + ParsedName::parse(parser)?, + )) + } +} + +//--- OctetsFrom + +impl OctetsFrom> + for Naptr +where + Octs: OctetsFrom, + Name: OctetsFrom, +{ + type Error = Octs::Error; + + fn try_octets_from( + source: Naptr, + ) -> Result { + Ok(Naptr::new( + source.order, + source.preference, + CharStr::try_octets_from(source.flags)?, + CharStr::try_octets_from(source.services)?, + CharStr::try_octets_from(source.regexp)?, + Name::try_octets_from(source.replacement)?, + )) + } +} + +//--- FlattenInto + +impl FlattenInto> + for Naptr +where + TOcts: OctetsFrom, + Name: FlattenInto, +{ + type AppendError = TOcts::Error; + + fn try_flatten_into(self) -> Result, TOcts::Error> { + self.flatten() + } +} + +//--- PartialEq and Eq + +impl PartialEq> + for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &Naptr) -> bool { + self.order == other.order + && self.preference == other.preference + && self.flags.eq(&other.flags) + && self.services.eq(&other.services) + && self.regexp.eq(&other.regexp) + && self.replacement.name_eq(&other.replacement) + } +} + +impl, Name: ToName> Eq for Naptr {} + +//--- PartialOrd, Ord, and CanonicalOrd + +impl PartialOrd> + for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &Naptr, + ) -> Option { + match self.order.partial_cmp(&other.order) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.preference.partial_cmp(&other.preference) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.flags.partial_cmp(&other.flags) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.services.partial_cmp(&other.services) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.regexp.partial_cmp(&other.regexp) { + Some(Ordering::Equal) => {} + other => return other, + } + + Some(self.replacement.name_cmp(&other.replacement)) + } +} + +impl + CanonicalOrd> for Naptr +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + Name: ToName, + OtherName: ToName, +{ + fn canonical_cmp(&self, other: &Naptr) -> Ordering { + match self.order.cmp(&other.order) { + Ordering::Equal => {} + other => return other, + } + match self.preference.cmp(&other.preference) { + Ordering::Equal => {} + other => return other, + } + match self.flags.canonical_cmp(&other.flags) { + Ordering::Equal => {} + other => return other, + } + match self.services.canonical_cmp(&other.services) { + Ordering::Equal => {} + other => return other, + } + match self.regexp.canonical_cmp(&other.regexp) { + Ordering::Equal => {} + other => return other, + } + + self.replacement.name_cmp(&other.replacement) + } +} + +impl Ord for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn cmp(&self, other: &Self) -> Ordering { + match self.order.cmp(&other.order) { + Ordering::Equal => {} + other => return other, + } + match self.preference.cmp(&other.preference) { + Ordering::Equal => {} + other => return other, + } + match self.flags.cmp(&other.flags) { + Ordering::Equal => {} + other => return other, + } + match self.services.cmp(&other.services) { + Ordering::Equal => {} + other => return other, + } + match self.regexp.cmp(&other.regexp) { + Ordering::Equal => {} + other => return other, + } + + self.replacement.name_cmp(&other.replacement) + } +} + +//--- Hash + +impl hash::Hash for Naptr +where + Octs: AsRef<[u8]>, + Name: hash::Hash, +{ + fn hash(&self, state: &mut H) { + self.order.hash(state); + self.preference.hash(state); + self.flags.hash(state); + self.services.hash(state); + self.regexp.hash(state); + self.replacement.hash(state); + } +} + +//--- RecordData, ParseRecordData, ComposeRecordData + +impl RecordData for Naptr { + fn rtype(&self) -> Rtype { + Rtype::NAPTR + } +} + +impl<'a, Octs: Octets + ?Sized> ParseRecordData<'a, Octs> + for Naptr, ParsedName>> +{ + fn parse_rdata( + rtype: Rtype, + parser: &mut Parser<'a, Octs>, + ) -> Result, ParseError> { + if rtype == Naptr::RTYPE { + Self::parse(parser).map(Some) + } else { + Ok(None) + } + } +} + +impl ComposeRecordData for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn rdlen(&self, _compress: bool) -> Option { + Some( + 2 + 2 + + self.flags.compose_len() + + self.services.compose_len() + + self.regexp.compose_len() + + self.replacement.compose_len(), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.order.compose(target)?; + self.preference.compose(target)?; + self.flags.compose(target)?; + self.services.compose(target)?; + self.regexp.compose(target)?; + self.replacement.compose_canonical(target) + } + + fn compose_canonical_rdata< + Target: crate::base::wire::Composer + ?Sized, + >( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +//--- Display + +impl core::fmt::Display for Naptr +where + Octs: AsRef<[u8]>, + Name: fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!( + f, + "{} {} {} {} {} {}.", + self.order, + self.preference, + self.flags.display_quoted(), + self.services.display_quoted(), + self.regexp.display_quoted(), + self.replacement + ) + } +} + +//--- Debug + +impl core::fmt::Debug for Naptr +where + Octs: AsRef<[u8]>, + Name: fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("Naptr") + .field("order", &self.order) + .field("preference", &self.preference) + .field("flags", &self.flags) + .field("services", &self.services) + .field("regexp", &self.regexp) + .field("replacement", &self.replacement) + .finish() + } +} + +//--- ZonefileFmt + +impl ZonefileFmt for Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.order)?; + p.write_comment("order")?; + p.write_token(self.preference)?; + p.write_comment("preference")?; + p.write_token(self.flags.display_quoted())?; + p.write_comment("flags")?; + p.write_token(self.services.display_quoted())?; + p.write_comment("services")?; + p.write_token(self.regexp.display_quoted())?; + p.write_comment("regexp")?; + p.write_token(self.replacement.fmt_with_dot())?; + p.write_comment("replacement") + }) + } +} + +//============ Testing ======================================================= + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use bytes::Bytes; + + use super::*; + use crate::base::{ + rdata::test::{test_compose_parse, test_rdlen, test_scan}, + Name, + }; + use core::str::FromStr; + use std::vec::Vec; + + #[test] + #[allow(clippy::redundant_closure)] // lifetimes ... + fn naptr_compose_parse_scan() { + let rdata = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets("").unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Naptr::parse(parser)); + test_scan( + &[ + "100", + "50", + "a", + "z3950+N2L+N2C", + "", + "cidserver.example.com.", + ], + Naptr::scan, + &rdata, + ); + } + + #[test] + fn naptr_octets_into() { + let naptr: Naptr<&str, Name>> = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets("").unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + let naptr_bytes: Naptr> = + naptr.clone().octets_into(); + assert_eq!(naptr.order(), naptr_bytes.order()); + assert_eq!(naptr.preference(), naptr_bytes.preference()); + assert_eq!(naptr.flags(), naptr_bytes.flags()); + assert_eq!(naptr.services(), naptr_bytes.services()); + assert_eq!(naptr.regexp(), naptr_bytes.regexp()); + assert_eq!(naptr.replacement(), naptr_bytes.replacement()); + } + + #[test] + fn naptr_display() { + let naptr: Naptr<&str, Name>> = Naptr::new( + 100, + 50, + CharStr::from_octets("a").unwrap(), + CharStr::from_octets("z3950+N2L+N2C").unwrap(), + CharStr::from_octets("").unwrap(), + Name::>::from_str("cidserver.example.com.").unwrap(), + ); + assert_eq!( + format!("{}", naptr), + r#"100 50 "a" "z3950+N2L+N2C" "" cidserver.example.com."# + ); + } +} From 6842c51a8311a642d51a3591a0c2493b2dcadd70 Mon Sep 17 00:00:00 2001 From: Weilence Date: Thu, 31 Oct 2024 00:44:11 +0800 Subject: [PATCH 2/5] Fix review comments --- src/rdata/naptr.rs | 77 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/rdata/naptr.rs b/src/rdata/naptr.rs index 964f64e84..87a182a28 100644 --- a/src/rdata/naptr.rs +++ b/src/rdata/naptr.rs @@ -1,8 +1,8 @@ //! Record data from [RFC 3403]: NAPTR records. //! -//! This RFC defines the Naptr record type. +//! This RFC defines the NAPTR record type. //! -//! [RFC 3403]: https://tools.ietf.org/html/rfc3403 +//! [RFC 3403]: https://www.rfc-editor.org/info/rfc3403 use crate::base::{ name::FlattenInto, @@ -20,6 +20,15 @@ use octseq::{Octets, OctetsFrom, OctetsInto, Parser}; //------------ Naptr --------------------------------------------------------- +/// Naptr record data. +/// +/// The Naptr encodes DNS rules for URI delegation, allowing changes and redelegation. +/// It uses regex for string-to-domain name conversion, chosen for compactness and +/// expressivity in small DNS packets. +/// +/// The Naptr record type is defined in [RFC 3403, section 4.1][1]. +/// +/// [1]: https://tools.ietf.org/html/rfc3403#section-4.1 #[derive(Clone)] #[cfg_attr( feature = "serde", @@ -53,6 +62,7 @@ impl Naptr<(), ()> { } impl Naptr { + /// Creates a new Naptr record data from content. pub fn new( order: u16, preference: u16, @@ -71,31 +81,44 @@ impl Naptr { } } + /// The order of processing the records is from lowest to highest. + /// If two records have the same order value, they should be processed + /// according to their preference value and services field. pub fn order(&self) -> u16 { self.order } + /// The priority of the DDDS Algorithm, from lowest to highest. pub fn preference(&self) -> u16 { self.preference } + /// The flags controls aspects of the rewriting and interpretation of + /// the fields in the record. pub fn flags(&self) -> &CharStr { &self.flags } + /// The services specify the Service Parameters applicable to + /// this delegation path. pub fn services(&self) -> &CharStr { &self.services } + /// The regexp containing a substitution expression that is + /// applied to the original string held by the client in order to + /// construct the next domain name to lookup. pub fn regexp(&self) -> &CharStr { &self.regexp } + /// The replacement is the next domain name to query for, + /// depending on the potential values found in the flags field. pub fn replacement(&self) -> &Name { &self.replacement } - pub(crate) fn convert_octets( + pub(in crate::rdata) fn convert_octets( self, ) -> Result, TOcts::Error> where @@ -112,7 +135,7 @@ impl Naptr { )) } - pub(crate) fn flatten( + pub(in crate::rdata) fn flatten( self, ) -> Result, TOcts::Error> where @@ -288,7 +311,7 @@ where other => return other, } - self.replacement.name_cmp(&other.replacement) + self.replacement.lowercase_composed_cmp(&other.replacement) } } @@ -344,7 +367,7 @@ where impl RecordData for Naptr { fn rtype(&self) -> Rtype { - Rtype::NAPTR + Naptr::RTYPE } } @@ -370,11 +393,15 @@ where { fn rdlen(&self, _compress: bool) -> Option { Some( - 2 + 2 - + self.flags.compose_len() - + self.services.compose_len() - + self.regexp.compose_len() - + self.replacement.compose_len(), + (u16::COMPOSE_LEN + u16::COMPOSE_LEN) + .checked_add(self.flags.compose_len()) + .expect("flags too long") + .checked_add(self.services.compose_len()) + .expect("services too long") + .checked_add(self.regexp.compose_len()) + .expect("regexp too long") + .checked_add(self.replacement.compose_len()) + .expect("replacement too long"), ) } @@ -382,12 +409,8 @@ where &self, target: &mut Target, ) -> Result<(), Target::AppendError> { - self.order.compose(target)?; - self.preference.compose(target)?; - self.flags.compose(target)?; - self.services.compose(target)?; - self.regexp.compose(target)?; - self.replacement.compose_canonical(target) + self.compose_head(target)?; + self.replacement.compose(target) } fn compose_canonical_rdata< @@ -396,7 +419,25 @@ where &self, target: &mut Target, ) -> Result<(), Target::AppendError> { - self.compose_rdata(target) + self.compose_head(target)?; + self.replacement.compose_canonical(target) + } +} + +impl Naptr +where + Octs: AsRef<[u8]>, + Name: ToName, +{ + fn compose_head( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.order.compose(target)?; + self.preference.compose(target)?; + self.flags.compose(target)?; + self.services.compose(target)?; + self.regexp.compose(target) } } From b73314b16ebd7e7d707660710dcb0fe068168e6a Mon Sep 17 00:00:00 2001 From: Weilence Date: Fri, 1 Nov 2024 22:46:25 +0800 Subject: [PATCH 3/5] Fix review comments --- src/rdata/naptr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rdata/naptr.rs b/src/rdata/naptr.rs index 87a182a28..7ce36b04f 100644 --- a/src/rdata/naptr.rs +++ b/src/rdata/naptr.rs @@ -28,7 +28,7 @@ use octseq::{Octets, OctetsFrom, OctetsInto, Parser}; /// /// The Naptr record type is defined in [RFC 3403, section 4.1][1]. /// -/// [1]: https://tools.ietf.org/html/rfc3403#section-4.1 +/// [1]: https://www.rfc-editor.org/rfc/rfc3403#section-4.1 #[derive(Clone)] #[cfg_attr( feature = "serde", From 38c23b28b6827e3a63fc7a31780d14029ad5cf6e Mon Sep 17 00:00:00 2001 From: Weilence Date: Sat, 2 Nov 2024 11:21:37 +0800 Subject: [PATCH 4/5] Add regexp in test --- src/base/zonefile_fmt.rs | 4 ++-- src/rdata/naptr.rs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index 3d25ca19a..f19e5bcff 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -356,11 +356,11 @@ mod test { 50, "a".parse().unwrap(), "z3950+N2L+N2C".parse().unwrap(), - "".parse().unwrap(), + r#"!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i"#.parse().unwrap(), Name::from_slice(b"\x09cidserver\x07example\x03com\x00").unwrap(), )); assert_eq!( - r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "" cidserver.example.com."#, + r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#, record.display_zonefile(false).to_string() ); } diff --git a/src/rdata/naptr.rs b/src/rdata/naptr.rs index 7ce36b04f..44c8ba4bd 100644 --- a/src/rdata/naptr.rs +++ b/src/rdata/naptr.rs @@ -575,12 +575,13 @@ mod test { 50, CharStr::from_octets("a").unwrap(), CharStr::from_octets("z3950+N2L+N2C").unwrap(), - CharStr::from_octets("").unwrap(), + CharStr::from_octets(r#"!^urn:cid:.+@([^\.]+\.)(.*)$!\2!i"#) + .unwrap(), Name::>::from_str("cidserver.example.com.").unwrap(), ); assert_eq!( format!("{}", naptr), - r#"100 50 "a" "z3950+N2L+N2C" "" cidserver.example.com."# + r#"100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."# ); } } From 5b68b2791034aef7890dc562e775564ed8d16b5a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:47:30 +0100 Subject: [PATCH 5/5] Re-ordered to be more alphabetical. --- src/rdata/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index fe3facfed..1b3b4957c 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -105,6 +105,11 @@ rdata_types! { Ds, } } + naptr::{ + zone { + Naptr, + } + } nsec3::{ zone { Nsec3, @@ -132,9 +137,4 @@ rdata_types! { Zonemd, } } - naptr::{ - zone { - Naptr, - } - } }