From 66d6061ffce6633bc4f534b9907f22610398c1c4 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 19 Feb 2024 11:51:51 +0100 Subject: [PATCH] drop most zone_file entry types instead use the record types in zone files the main difference between e.g. zone_file::A and record::A was that the latter had a TTL filed and the former didn't to eliminate code duplication we make the `ZoneFile` API use the `record` types and discard the zone_file entry types --- .../src/resolver/dns/scenarios.rs | 12 +- .../src/resolver/dnssec/scenarios.rs | 14 +- packages/dns-test/examples/explore.rs | 10 +- packages/dns-test/src/lib.rs | 9 +- packages/dns-test/src/name_server.rs | 53 +- packages/dns-test/src/record.rs | 519 ++++++++++++++++-- packages/dns-test/src/trust_anchor.rs | 2 +- packages/dns-test/src/tshark.rs | 6 +- packages/dns-test/src/zone_file.rs | 451 ++------------- 9 files changed, 587 insertions(+), 489 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index b3c4da7f..f696691a 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -2,7 +2,7 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, Dnssec, Recurse}; use dns_test::name_server::NameServer; -use dns_test::record::RecordType; +use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -18,9 +18,9 @@ fn can_resolve() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .a(needle_fqdn.clone(), expected_ipv4_addr); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let nameservers_ns = nameservers_ns.start()?; eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); @@ -75,8 +75,8 @@ fn nxdomain() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.start()?; com_ns.referral( diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 554e7417..fd9d2676 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -2,7 +2,7 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, Dnssec, Recurse}; use dns_test::name_server::NameServer; -use dns_test::record::RecordType; +use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -12,7 +12,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - ns.a(ns.fqdn().clone(), ns.ipv4_addr()); + ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); let ns = ns.sign()?; let root_ksk = ns.key_signing_key().clone(); @@ -61,9 +61,9 @@ fn can_validate_with_delegation() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .a(needle_fqdn.clone(), expected_ipv4_addr); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let nameservers_ns = nameservers_ns.sign()?; let nameservers_ds = nameservers_ns.ds().clone(); let nameservers_ns = nameservers_ns.start()?; @@ -76,7 +76,7 @@ fn can_validate_with_delegation() -> Result<()> { nameservers_ns.fqdn().clone(), nameservers_ns.ipv4_addr(), ) - .ds(nameservers_ds); + .add(nameservers_ds); let com_ns = com_ns.sign()?; let com_ds = com_ns.ds().clone(); let com_ns = com_ns.start()?; @@ -85,7 +85,7 @@ fn can_validate_with_delegation() -> Result<()> { root_ns .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .ds(com_ds); + .add(com_ds); let root_ns = root_ns.sign()?; let root_ksk = root_ns.key_signing_key().clone(); let root_zsk = root_ns.zone_signing_key().clone(); diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 3ed9b9d5..e6d9919e 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -2,7 +2,7 @@ use std::sync::mpsc; use dns_test::client::Client; use dns_test::name_server::NameServer; -use dns_test::record::RecordType; +use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -19,8 +19,8 @@ fn main() -> Result<()> { let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.sign()?; let nameservers_ds = nameservers_ns.ds().clone(); let nameservers_ns = nameservers_ns.start()?; @@ -31,14 +31,14 @@ fn main() -> Result<()> { nameservers_ns.fqdn().clone(), nameservers_ns.ipv4_addr(), ) - .ds(nameservers_ds); + .add(nameservers_ds); let com_ns = com_ns.sign()?; let com_ds = com_ns.ds().clone(); let com_ns = com_ns.start()?; root_ns .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .ds(com_ds); + .add(com_ds); let root_ns = root_ns.sign()?; let root_ksk = root_ns.key_signing_key().clone(); let root_zsk = root_ns.zone_signing_key().clone(); diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 4f8b5c01..48c81c00 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -10,9 +10,6 @@ pub use crate::fqdn::FQDN; pub use crate::resolver::Resolver; pub use crate::trust_anchor::TrustAnchor; -pub type Error = Box; -pub type Result = core::result::Result; - pub mod client; mod container; mod fqdn; @@ -23,6 +20,12 @@ mod trust_anchor; pub mod tshark; pub mod zone_file; +pub type Error = Box; +pub type Result = core::result::Result; + +// TODO maybe this should be a TLS variable that each unit test (thread) can override +const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day + #[derive(Clone)] pub enum Implementation { Unbound, diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index f907c8b6..fd8bec05 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -2,9 +2,10 @@ use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; +use crate::record::{self, Record, SoaSettings, DS, SOA}; use crate::tshark::Tshark; -use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; -use crate::{Implementation, Result, FQDN}; +use crate::zone_file::{self, ZoneFile}; +use crate::{Implementation, Result, DEFAULT_TTL, FQDN}; pub struct NameServer { container: Container, @@ -34,18 +35,16 @@ impl NameServer { let ns_count = ns_count(); let nameserver = primary_ns(ns_count); - let soa = zone_file::SOA { + let soa = SOA { zone: zone.clone(), + ttl: DEFAULT_TTL, nameserver: nameserver.clone(), admin: admin_ns(ns_count), settings: SoaSettings::default(), }; - let mut zone_file = ZoneFile::new(zone.clone(), soa); + let mut zone_file = ZoneFile::new(soa); - zone_file.entry(zone_file::NS { - zone, - nameserver: nameserver.clone(), - }); + zone_file.add(Record::ns(zone, nameserver.clone())); let image = implementation.into(); Ok(Self { @@ -61,15 +60,9 @@ impl NameServer { self } - /// Adds an A record pair to the zone file - pub fn a(&mut self, fqdn: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self { - self.zone_file.entry(zone_file::A { fqdn, ipv4_addr }); - self - } - - /// Adds a DS record to the zone file - pub fn ds(&mut self, ds: DS) -> &mut Self { - self.zone_file.entry(ds); + /// Adds a record to the name server's zone file + pub fn add(&mut self, record: impl Into) -> &mut Self { + self.zone_file.add(record); self } @@ -89,19 +82,19 @@ impl NameServer { container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; - let zone = &zone_file.origin; + let zone = zone_file.origin(); let zsk_keygen = format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}"); let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?; let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key"); - let zsk: DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?; + let zsk: zone_file::DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?; let ksk_keygen = format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}"); let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?; let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key"); - let ksk: DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?; + let ksk: zone_file::DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?; // -n = use NSEC3 instead of NSEC // -p = set the opt-out flag on all nsec3 rrs @@ -120,15 +113,17 @@ impl NameServer { container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; let signed_zone_file = container.stdout(&["cat", &zone_file_path])?; + let ttl = zone_file.soa.ttl; Ok(NameServer { container, zone_file, state: Signed { ds, - ksk, signed_zone_file, - zsk, + // inherit SOA's TTL value + ksk: ksk.with_ttl(ttl), + zsk: zsk.with_ttl(ttl), }, }) } @@ -144,7 +139,7 @@ impl NameServer { // for PID file container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp(&zone_file_path(), &zone_file.to_string())?; @@ -183,7 +178,7 @@ impl NameServer { // for PID file container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; let child = container.spawn(&["nsd", "-d"])?; @@ -194,11 +189,11 @@ impl NameServer { }) } - pub fn key_signing_key(&self) -> &DNSKEY { + pub fn key_signing_key(&self) -> &record::DNSKEY { &self.state.ksk } - pub fn zone_signing_key(&self) -> &DNSKEY { + pub fn zone_signing_key(&self) -> &record::DNSKEY { &self.state.zsk } @@ -256,7 +251,7 @@ impl NameServer { } pub fn zone(&self) -> &FQDN { - &self.zone_file.origin + self.zone_file.origin() } pub fn fqdn(&self) -> &FQDN { @@ -268,8 +263,8 @@ pub struct Stopped; pub struct Signed { ds: DS, - zsk: DNSKEY, - ksk: DNSKEY, + zsk: record::DNSKEY, + ksk: record::DNSKEY, signed_zone_file: String, } diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index e8086916..c16a49f8 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -1,24 +1,29 @@ //! Text representation of DNS records -use core::array; use core::result::Result as CoreResult; use core::str::FromStr; +use core::{array, fmt}; +use std::fmt::Write; use std::net::Ipv4Addr; -use crate::{Error, Result, FQDN}; +use crate::{Error, Result, DEFAULT_TTL, FQDN}; #[allow(clippy::upper_case_acronyms)] #[derive(Debug, PartialEq)] pub enum RecordType { A, + DS, NS, SOA, + // excluded because cannot appear in RRSIG.type_covered + // RRSIG, } impl RecordType { pub fn as_str(&self) -> &'static str { match self { RecordType::A => "A", + RecordType::DS => "DS", RecordType::SOA => "SOA", RecordType::NS => "NS", } @@ -31,6 +36,7 @@ impl FromStr for RecordType { fn from_str(input: &str) -> CoreResult { let record_type = match input { "A" => Self::A, + "DS" => Self::DS, "SOA" => Self::SOA, "NS" => Self::NS, _ => return Err(format!("unknown record type: {input}").into()), @@ -40,14 +46,59 @@ impl FromStr for RecordType { } } +impl fmt::Display for RecordType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + RecordType::A => "A", + RecordType::DS => "DS", + RecordType::NS => "NS", + RecordType::SOA => "SOA", + }; + + f.write_str(s) + } +} + #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + DS(DS), + NS(NS), RRSIG(RRSIG), SOA(SOA), } +impl From for Record { + fn from(v: DS) -> Self { + Self::DS(v) + } +} + +impl From for Record { + fn from(v: A) -> Self { + Self::A(v) + } +} + +impl From for Record { + fn from(v: NS) -> Self { + Self::NS(v) + } +} + +impl From for Record { + fn from(v: RRSIG) -> Self { + Self::RRSIG(v) + } +} + +impl From for Record { + fn from(v: SOA) -> Self { + Self::SOA(v) + } +} + impl Record { pub fn try_into_a(self) -> CoreResult { if let Self::A(v) = self { @@ -68,6 +119,24 @@ impl Record { pub fn is_soa(&self) -> bool { matches!(self, Self::SOA(..)) } + + pub fn a(fqdn: FQDN, ipv4_addr: Ipv4Addr) -> Self { + A { + fqdn, + ttl: DEFAULT_TTL, + ipv4_addr, + } + .into() + } + + pub fn ns(zone: FQDN, nameserver: FQDN) -> Self { + NS { + zone, + ttl: DEFAULT_TTL, + nameserver, + } + .into() + } } impl FromStr for Record { @@ -91,6 +160,18 @@ impl FromStr for Record { } } +impl fmt::Display for Record { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Record::A(a) => write!(f, "{a}"), + Record::DS(ds) => write!(f, "{ds}"), + Record::NS(ns) => write!(f, "{ns}"), + Record::RRSIG(rrsig) => write!(f, "{rrsig}"), + Record::SOA(soa) => write!(f, "{soa}"), + } + } +} + #[derive(Debug)] pub struct A { pub fqdn: FQDN, @@ -129,6 +210,189 @@ impl FromStr for A { } } +impl fmt::Display for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + ipv4_addr, + } = self; + + write!(f, "{fqdn}\t{ttl}\tIN\tA\t{ipv4_addr}") + } +} + +// integer types chosen based on bit sizes in section 2.1 of RFC4034 +#[derive(Clone, Debug)] +pub struct DNSKEY { + pub zone: FQDN, + pub ttl: u32, + pub flags: u16, + pub protocol: u8, + pub algorithm: u8, + pub public_key: String, +} + +impl DNSKEY { + /// formats the `DNSKEY` in the format `delv` expects + pub(super) fn delv(&self) -> String { + let Self { + zone, + flags, + protocol, + algorithm, + public_key, + .. + } = self; + + format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n") + } +} + +impl FromStr for DNSKEY { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 7 columns".into()); + }; + + if record_type != "DNSKEY" { + return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut public_key = String::new(); + for column in columns { + public_key.push_str(column); + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + flags: flags.parse()?, + protocol: protocol.parse()?, + algorithm: algorithm.parse()?, + public_key, + }) + } +} + +impl fmt::Display for DNSKEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } = self; + + write!( + f, + "{zone}\t{ttl}\tIN\tDNSKEY\t{flags} {protocol} {algorithm}" + )?; + + write_split_long_string(f, public_key) + } +} + +#[derive(Clone, Debug)] +pub struct DS { + zone: FQDN, + ttl: u32, + key_tag: u16, + algorithm: u8, + digest_type: u8, + digest: String, +} + +impl FromStr for DS { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 7 columns".into()); + }; + + let expected = "DS"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` entry as a {expected} entry").into(), + ); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut digest = String::new(); + for column in columns { + digest.push_str(column); + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + key_tag: key_tag.parse()?, + algorithm: algorithm.parse()?, + digest_type: digest_type.parse()?, + digest, + }) + } +} + +impl fmt::Display for DS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + key_tag, + algorithm, + digest_type, + digest, + } = self; + + write!( + f, + "{zone}\t{ttl}\tIN\tDS\t{key_tag} {algorithm} {digest_type}" + )?; + + write_split_long_string(f, digest) + } +} + +#[derive(Debug)] +pub struct NS { + pub zone: FQDN, + pub ttl: u32, + pub nameserver: FQDN, +} + +impl fmt::Display for NS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + nameserver, + } = self; + + write!(f, "{zone}\t{ttl}\tIN\tNS {nameserver}") + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct RRSIG { @@ -190,6 +454,28 @@ impl FromStr for RRSIG { } } +impl fmt::Display for RRSIG { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + type_covered, + algorithm, + labels, + original_ttl, + signature_expiration, + signature_inception, + key_tag, + signer_name, + signature, + } = self; + + write!(f, "{fqdn}\t{ttl}\tIN\tRRSIG\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?; + + write_split_long_string(f, signature) + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { @@ -197,11 +483,7 @@ pub struct SOA { pub ttl: u32, pub nameserver: FQDN, pub admin: FQDN, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, + pub settings: SoaSettings, } impl FromStr for SOA { @@ -229,34 +511,198 @@ impl FromStr for SOA { ttl: ttl.parse()?, nameserver: nameserver.parse()?, admin: admin.parse()?, - serial: serial.parse()?, - refresh: refresh.parse()?, - retry: retry.parse()?, - expire: expire.parse()?, - minimum: minimum.parse()?, + settings: SoaSettings { + serial: serial.parse()?, + refresh: refresh.parse()?, + retry: retry.parse()?, + expire: expire.parse()?, + minimum: minimum.parse()?, + }, }) } } +impl fmt::Display for SOA { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + nameserver, + admin, + settings, + } = self; + + write!(f, "{zone}\t{ttl}\tIN\tSOA\t{nameserver} {admin} {settings}") + } +} + +#[derive(Debug)] +pub struct SoaSettings { + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl Default for SoaSettings { + fn default() -> Self { + Self { + serial: 2024010101, + refresh: 1800, // 30 minutes + retry: 900, // 15 minutes + expire: 604800, // 1 week + minimum: 86400, // 1 day + } + } +} + +impl fmt::Display for SoaSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + serial, + refresh, + retry, + expire, + minimum, + } = self; + + write!(f, "{serial} {refresh} {retry} {expire} {minimum}") + } +} + +fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Result { + for (index, c) in field.chars().enumerate() { + if index % 56 == 0 { + f.write_char(' ')?; + } + f.write_char(c)?; + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn can_parse_a_record() -> Result<()> { - let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; - let a: A = input.parse()?; + fn a() -> Result<()> { + // dig A a.root-servers.net + let input = "a.root-servers.net. 77859 IN A 198.41.0.4"; + let a @ A { + fqdn, + ttl, + ipv4_addr, + } = &input.parse()?; - assert_eq!("a.root-servers.net.", a.fqdn.as_str()); - assert_eq!(3600000, a.ttl); - assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); + assert_eq!("a.root-servers.net.", fqdn.as_str()); + assert_eq!(77859, *ttl); + assert_eq!(Ipv4Addr::new(198, 41, 0, 4), *ipv4_addr); + + let output = a.to_string(); + assert_eq!(output, input); Ok(()) } #[test] - fn can_parse_soa_record() -> Result<()> { - let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + fn dnskey() -> Result<()> { + // dig DNSKEY . + let input = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU="; + + let dnskey @ DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } = &input.parse()?; + + assert_eq!(FQDN::ROOT, *zone); + assert_eq!(1116, *ttl); + assert_eq!(257, *flags); + assert_eq!(3, *protocol); + assert_eq!(8, *algorithm); + let expected = "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU="; + assert_eq!(expected, public_key); + + let output = dnskey.to_string(); + assert_eq!(output, input); + + Ok(()) + } + + #[test] + fn ds() -> Result<()> { + // dig DS com. + let input = "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A"; + + let ds @ DS { + zone, + ttl, + key_tag, + algorithm, + digest_type, + digest, + } = &input.parse()?; + + assert_eq!(FQDN::COM, *zone); + assert_eq!(7612, *ttl); + assert_eq!(19718, *key_tag); + assert_eq!(13, *algorithm); + assert_eq!(2, *digest_type); + let expected = "8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D771D7805A"; + assert_eq!(expected, digest); + + let output = ds.to_string(); + assert_eq!(output, input); + + Ok(()) + } + + #[test] + fn rrsig() -> Result<()> { + // dig +dnssec SOA . + let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + + let rrsig @ RRSIG { + fqdn, + ttl, + type_covered, + algorithm, + labels, + original_ttl, + signature_expiration, + signature_inception, + key_tag, + signer_name, + signature, + } = &input.parse()?; + + assert_eq!(FQDN::ROOT, *fqdn); + assert_eq!(1800, *ttl); + assert_eq!(RecordType::SOA, *type_covered); + assert_eq!(7, *algorithm); + assert_eq!(0, *labels); + assert_eq!(1800, *original_ttl); + assert_eq!(20240306132701, *signature_expiration); + assert_eq!(20240207132701, *signature_inception); + assert_eq!(11264, *key_tag); + assert_eq!(FQDN::ROOT, *signer_name); + let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; + assert_eq!(expected, signature); + + let output = rrsig.to_string(); + assert_eq!(input, output); + + Ok(()) + } + + #[test] + fn soa() -> Result<()> { + // dig SOA . + let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; let soa: SOA = input.parse()?; @@ -264,32 +710,15 @@ mod tests { assert_eq!(15633, soa.ttl); assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); - assert_eq!(2024020501, soa.serial); - assert_eq!(1800, soa.refresh); - assert_eq!(900, soa.retry); - assert_eq!(604800, soa.expire); - assert_eq!(86400, soa.minimum); + let settings = &soa.settings; + assert_eq!(2024020501, settings.serial); + assert_eq!(1800, settings.refresh); + assert_eq!(900, settings.retry); + assert_eq!(604800, settings.expire); + assert_eq!(86400, settings.minimum); - Ok(()) - } - - #[test] - fn can_parse_rrsig_record() -> Result<()> { - let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; - - let rrsig: RRSIG = input.parse()?; - - assert_eq!(FQDN::ROOT, rrsig.fqdn); - assert_eq!(1800, rrsig.ttl); - assert_eq!(RecordType::SOA, rrsig.type_covered); - assert_eq!(7, rrsig.algorithm); - assert_eq!(0, rrsig.labels); - assert_eq!(20240306132701, rrsig.signature_expiration); - assert_eq!(20240207132701, rrsig.signature_inception); - assert_eq!(11264, rrsig.key_tag); - assert_eq!(FQDN::ROOT, rrsig.signer_name); - let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; - assert_eq!(expected, rrsig.signature); + let output = soa.to_string(); + assert_eq!(output, input); Ok(()) } diff --git a/packages/dns-test/src/trust_anchor.rs b/packages/dns-test/src/trust_anchor.rs index b14173e2..e6333bbe 100644 --- a/packages/dns-test/src/trust_anchor.rs +++ b/packages/dns-test/src/trust_anchor.rs @@ -1,6 +1,6 @@ use core::fmt; -use crate::zone_file::DNSKEY; +use crate::record::DNSKEY; pub struct TrustAnchor { keys: Vec, diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index f230d31d..ecc39740 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -246,7 +246,7 @@ struct Ip { mod tests { use crate::client::{Client, Dnssec, Recurse}; use crate::name_server::NameServer; - use crate::record::RecordType; + use crate::record::{Record, RecordType}; use crate::zone_file::Root; use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN}; @@ -297,8 +297,8 @@ mod tests { let mut nameservers_ns = NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.start()?; com_ns.referral( diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file.rs index 53c498df..e4bb46c5 100644 --- a/packages/dns-test/src/zone_file.rs +++ b/packages/dns-test/src/zone_file.rs @@ -1,72 +1,63 @@ -//! BIND-style zone file +//! BIND-style zone files //! //! Note that //! - the `@` syntax is not used to avoid relying on the order of the entries //! - relative domain names are not used; all domain names must be in fully-qualified form -use core::{array, fmt}; +use core::fmt; +use std::array; use std::net::Ipv4Addr; use std::str::FromStr; -use crate::{Error, FQDN}; +use crate::record::{self, Record, SOA}; +use crate::{Error, Result, DEFAULT_TTL, FQDN}; pub struct ZoneFile { - pub origin: FQDN, - pub ttl: u32, + origin: FQDN, pub soa: SOA, - pub entries: Vec, + pub records: Vec, } impl ZoneFile { /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN, soa: SOA) -> Self { + pub fn new(soa: SOA) -> Self { Self { - origin, - ttl: 1800, + origin: soa.zone.clone(), soa, - entries: Vec::new(), + records: Vec::new(), } } - /// Appends an entry - pub fn entry(&mut self, entry: impl Into) { - self.entries.push(entry.into()) + /// Adds the given `record` to the zone file + pub fn add(&mut self, record: impl Into) { + self.records.push(record.into()) } - /// Appends a NS + A entry pair + /// Shortcut method for adding a referral (NS + A record pair) pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) { - self.entry(NS { - zone: zone.clone(), - nameserver: nameserver.clone(), - }); - self.entry(A { - fqdn: nameserver, - ipv4_addr, - }); + self.add(Record::ns(zone, nameserver.clone())); + self.add(Record::a(nameserver, ipv4_addr)); + } + + pub(crate) fn origin(&self) -> &FQDN { + &self.origin } } impl fmt::Display for ZoneFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - origin, - ttl, - soa, - entries, - } = self; + let Self { soa, records, .. } = self; - writeln!(f, "$ORIGIN {origin}")?; - writeln!(f, "$TTL {ttl}")?; writeln!(f, "{soa}")?; - - for entry in entries { - writeln!(f, "{entry}")?; + for record in records { + writeln!(f, "{record}")?; } Ok(()) } } +/// A root (server) hint pub struct Root { pub ipv4_addr: Ipv4Addr, pub ns: FQDN, @@ -79,7 +70,7 @@ impl Root { Self { ipv4_addr, ns, - ttl: 3600000, // 1000 hours + ttl: DEFAULT_TTL, } } } @@ -93,100 +84,47 @@ impl fmt::Display for Root { } } -pub enum Entry { - A(A), - DNSKEY(DNSKEY), - DS(DS), - NS(NS), -} - -impl From for Entry { - fn from(v: DS) -> Self { - Self::DS(v) - } -} - -impl From for Entry { - fn from(v: A) -> Self { - Self::A(v) - } -} - -impl From for Entry { - fn from(v: NS) -> Self { - Self::NS(v) - } -} - -impl fmt::Display for Entry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Entry::A(a) => a.fmt(f), - Entry::DNSKEY(dnskey) => dnskey.fmt(f), - Entry::DS(ds) => ds.fmt(f), - Entry::NS(ns) => ns.fmt(f), - } - } -} - -#[derive(Clone)] -pub struct A { - pub fqdn: FQDN, - pub ipv4_addr: Ipv4Addr, -} - -impl fmt::Display for A { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { fqdn, ipv4_addr } = self; - - write!(f, "{fqdn}\tIN\tA\t{ipv4_addr}") - } -} - -// integer types chosen based on bit sizes in section 2.1 of RFC4034 -#[derive(Clone, Debug)] -pub struct DNSKEY { +// NOTE compared to `record::DNSKEY`, this zone file entry lacks the TTL field +#[allow(clippy::upper_case_acronyms)] +pub(crate) struct DNSKEY { zone: FQDN, flags: u16, protocol: u8, algorithm: u8, public_key: String, - - // extra information in `+multiline` format and `ldns-keygen`'s output - bits: u16, - key_tag: u16, } impl DNSKEY { - pub fn bits(&self) -> u16 { - self.bits - } - - pub fn key_tag(&self) -> u16 { - self.key_tag - } - - /// formats the `DNSKEY` in the format `delv` expects - pub(super) fn delv(&self) -> String { + pub fn with_ttl(self, ttl: u32) -> record::DNSKEY { let Self { zone, flags, protocol, algorithm, public_key, - .. } = self; - format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n") + record::DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } } } impl FromStr for DNSKEY { type Err = Error; - fn from_str(input: &str) -> Result { - let (before, after) = input.split_once(';').ok_or("comment was not found")?; - let mut columns = before.split_whitespace(); + fn from_str(mut input: &str) -> Result { + // discard trailing comment + if let Some((before, _after)) = input.split_once(';') { + input = before.trim(); + } + + let mut columns = input.split_whitespace(); let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] = array::from_fn(|_| columns.next()) @@ -202,306 +140,39 @@ impl FromStr for DNSKEY { return Err(format!("unknown class: {class}").into()); } - // {id = 24975 (zsk), size = 1024b} - let error = "invalid comment syntax"; - let (id_expr, size_expr) = after.split_once(',').ok_or(error)?; - - // {id = 24975 (zsk) - let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?; - if id_lhs.trim() != "{id" { - return Err(error.into()); - } - - // 24975 (zsk) - let (key_tag, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?; - - // size = 1024b} - let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?; - if size_lhs.trim() != "size" { - return Err(error.into()); - } - let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?; - Ok(Self { zone: zone.parse()?, flags: flags.parse()?, protocol: protocol.parse()?, algorithm: algorithm.parse()?, public_key: public_key.to_string(), - - key_tag: key_tag.parse()?, - bits, }) } } -impl fmt::Display for DNSKEY { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - flags, - protocol, - algorithm, - public_key, - bits: _, - key_tag: _, - } = self; - - write!( - f, - "{zone}\tIN\tDNSKEY\t{flags}\t{protocol}\t{algorithm}\t{public_key}" - ) - } -} - -#[derive(Clone)] -pub struct DS { - zone: FQDN, - _ttl: u32, - key_tag: u16, - algorithm: u8, - digest_type: u8, - digest: String, -} - -impl FromStr for DS { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type), Some(digest), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 8 columns".into()); - }; - - let expected = "DS"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` entry as a {expected} entry").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - zone: zone.parse()?, - _ttl: ttl.parse()?, - key_tag: key_tag.parse()?, - algorithm: algorithm.parse()?, - digest_type: digest_type.parse()?, - digest: digest.to_string(), - }) - } -} - -/// NOTE does NOT include the TTL field -impl fmt::Display for DS { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - _ttl, - key_tag, - algorithm, - digest_type, - digest, - } = self; - - write!( - f, - "{zone}\tIN\tDS\t{key_tag}\t{algorithm}\t{digest_type}\t{digest}" - ) - } -} - -pub struct NS { - pub zone: FQDN, - pub nameserver: FQDN, -} - -impl fmt::Display for NS { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - nameserver: ns, - } = self; - - write!(f, "{zone}\tIN\tNS\t{ns}") - } -} - -pub struct SOA { - pub zone: FQDN, - pub nameserver: FQDN, - pub admin: FQDN, - pub settings: SoaSettings, -} - -impl fmt::Display for SOA { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - nameserver: ns, - admin, - settings, - } = self; - - write!(f, "{zone}\tIN\tSOA\t{ns}\t{admin}\t{settings}") - } -} - -pub struct SoaSettings { - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, -} - -impl Default for SoaSettings { - fn default() -> Self { - Self { - serial: 2024010101, - refresh: 1800, // 30 minutes - retry: 900, // 15 minutes - expire: 604800, // 1 week - minimum: 86400, // 1 day - } - } -} - -impl fmt::Display for SoaSettings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - serial, - refresh, - retry, - expire, - minimum, - } = self; - - write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") - } -} - #[cfg(test)] mod tests { - use crate::Result; - use super::*; #[test] - fn a_to_string() -> Result<()> { - let expected = "e.gtld-servers.net. IN A 192.12.94.30"; - let a = example_a()?; - assert_eq!(expected, a.to_string()); + fn dnskey() -> Result<()> { + let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}"; + + let DNSKEY { + zone, + flags, + protocol, + algorithm, + public_key, + } = input.parse()?; + + assert_eq!(FQDN::ROOT, zone); + assert_eq!(256, flags); + assert_eq!(3, protocol); + assert_eq!(7, algorithm); + let expected = "AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB"; + assert_eq!(expected, public_key); Ok(()) } - - #[test] - fn ns_to_string() -> Result<()> { - let expected = "com. IN NS e.gtld-servers.net."; - let ns = example_ns()?; - assert_eq!(expected, ns.to_string()); - - Ok(()) - } - - #[test] - fn root_to_string() -> Result<()> { - let expected = ". 3600000 NS a.root-servers.net. -a.root-servers.net. 3600000 A 198.41.0.4"; - let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); - assert_eq!(expected, root.to_string()); - Ok(()) - } - - #[test] - fn soa_to_string() -> Result<()> { - let expected = - ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; - let soa = example_soa()?; - assert_eq!(expected, soa.to_string()); - - Ok(()) - } - - #[test] - fn zone_file_to_string() -> Result<()> { - let expected = "$ORIGIN . -$TTL 1800 -. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) -com. IN NS e.gtld-servers.net. -e.gtld-servers.net. IN A 192.12.94.30 -"; - let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); - zone.entry(example_ns()?); - zone.entry(example_a()?); - - assert_eq!(expected, zone.to_string()); - - Ok(()) - } - - // not quite roundtrip because we drop the TTL field when doing `to_string` - #[test] - fn ds_roundtrip() -> Result<()> { - let input = - ". 1800 IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; - let ds: DS = input.parse()?; - let output = ds.to_string(); - - let expected = - ". IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; - assert_eq!(expected, output); - - Ok(()) - } - - #[test] - fn dnskey_roundtrip() -> Result<()> { - let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}"; - - let dnskey: DNSKEY = input.parse()?; - - assert_eq!(256, dnskey.flags); - assert_eq!(3, dnskey.protocol); - assert_eq!(7, dnskey.algorithm); - let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; - assert_eq!(expected, dnskey.public_key); - assert_eq!(1024, dnskey.bits()); - assert_eq!(24975, dnskey.key_tag()); - - let output = dnskey.to_string(); - assert!(input.starts_with(&output)); - - Ok(()) - } - - fn example_a() -> Result { - Ok(A { - fqdn: FQDN("e.gtld-servers.net.")?, - ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), - }) - } - - fn example_ns() -> Result { - Ok(NS { - zone: FQDN::COM, - nameserver: FQDN("e.gtld-servers.net.")?, - }) - } - - fn example_soa() -> Result { - Ok(SOA { - zone: FQDN::ROOT, - nameserver: FQDN("a.root-servers.net.")?, - admin: FQDN("nstld.verisign-grs.com.")?, - settings: SoaSettings::default(), - }) - } }