diff --git a/Cargo.lock b/Cargo.lock index 2a3639b3..35f08284 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,7 @@ dependencies = [ name = "conformance-tests" version = "0.1.0" dependencies = [ + "base64", "dns-test", ] @@ -143,12 +144,19 @@ dependencies = [ "serde", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dns-test" version = "0.1.0" dependencies = [ "ctrlc", "minijinja", + "pretty_assertions", "serde", "serde_json", "serde_with", @@ -358,6 +366,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -701,3 +719,9 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/packages/conformance-tests/Cargo.toml b/packages/conformance-tests/Cargo.toml index 87fcffb9..03e22ff9 100644 --- a/packages/conformance-tests/Cargo.toml +++ b/packages/conformance-tests/Cargo.toml @@ -6,6 +6,7 @@ publish = false version = "0.1.0" [dependencies] +base64 = "0.21.7" dns-test.path = "../dns-test" [lib] diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index b3c4da7f..39470b83 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -1,8 +1,8 @@ use std::net::Ipv4Addr; -use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::client::{Client, DigSettings}; 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()); @@ -44,13 +44,9 @@ fn can_resolve() -> Result<()> { let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - resolver_ip_addr, - RecordType::A, - &needle_fqdn, - )?; + + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?; assert!(output.status.is_noerror()); @@ -75,8 +71,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( @@ -94,13 +90,8 @@ fn nxdomain() -> Result<()> { let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - resolver_ip_addr, - RecordType::A, - &needle_fqdn, - )?; + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?; assert!(dbg!(output).status.is_nxdomain()); diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 9772e309..233ea95c 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -1,4 +1,4 @@ -use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::RecordType; use dns_test::tshark::{Capture, Direction}; @@ -20,13 +20,8 @@ fn edns_support() -> Result<()> { let mut tshark = resolver.eavesdrop()?; let client = Client::new(network)?; - let ans = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver.ipv4_addr(), - RecordType::SOA, - &FQDN::ROOT, - )?; + let settings = *DigSettings::default().authentic_data().recurse(); + let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?; assert!(ans.status.is_servfail()); tshark.wait_for_capture()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 554e7417..416fff70 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -1,131 +1,2 @@ -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::zone_file::Root; -use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; - -// no DS records are involved; this is a single-link chain of trust -#[ignore] -#[test] -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()); - let ns = ns.sign()?; - - let root_ksk = ns.key_signing_key().clone(); - let root_zsk = ns.zone_signing_key().clone(); - - eprintln!("root.zone.signed:\n{}", ns.signed_zone_file()); - - let ns = ns.start()?; - - eprintln!("root.zone:\n{}", ns.zone_file()); - - let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())]; - - let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); - let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; - let resolver_addr = resolver.ipv4_addr(); - - let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver_addr, - RecordType::SOA, - &FQDN::ROOT, - )?; - - assert!(output.status.is_noerror()); - assert!(output.flags.authenticated_data); - - let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; - assert!(output.starts_with("; fully validated")); - - Ok(()) -} - -#[ignore] -#[test] -fn can_validate_with_delegation() -> Result<()> { - let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let needle_fqdn = FQDN("example.nameservers.com.")?; - - let network = Network::new()?; - let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; - - 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); - let nameservers_ns = nameservers_ns.sign()?; - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); - - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .ds(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - eprintln!("com.zone:\n{}", com_ns.zone_file()); - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .ds(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(); - - eprintln!("root.zone.signed:\n{}", root_ns.signed_zone_file()); - - let root_ns = root_ns.start()?; - - eprintln!("root.zone:\n{}", root_ns.zone_file()); - - let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; - - let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); - let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; - let resolver_addr = resolver.ipv4_addr(); - - let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver_addr, - RecordType::A, - &needle_fqdn, - )?; - - assert!(output.status.is_noerror()); - - assert!(output.flags.authenticated_data); - - let [a, _rrsig] = output.answer.try_into().unwrap(); - let a = a.try_into_a().unwrap(); - - assert_eq!(needle_fqdn, a.fqdn); - assert_eq!(expected_ipv4_addr, a.ipv4_addr); - - let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, &trust_anchor)?; - assert!(output.starts_with("; fully validated")); - - Ok(()) -} - -// TODO nxdomain with NSEC records -// TODO nxdomain with NSEC3 records +mod bogus; +mod secure; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs new file mode 100644 index 00000000..40011547 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -0,0 +1,93 @@ +use std::net::Ipv4Addr; + +use base64::prelude::*; +use dns_test::client::{Client, DigSettings}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; + +#[ignore] +#[test] +fn bad_signature_in_leaf_nameserver() -> Result<()> { + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .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 mut nameservers_ns = nameservers_ns.sign()?; + + // fault injection: change the signature field of the RRSIG that covers the A record we'll query + let mut modified = 0; + for record in &mut nameservers_ns.signed_zone_file_mut().records { + if let Record::RRSIG(rrsig) = record { + if rrsig.fqdn == needle_fqdn { + let mut signature = BASE64_STANDARD.decode(&rrsig.signature)?; + let last = signature.last_mut().expect("empty signature"); + *last = !*last; + + rrsig.signature = BASE64_STANDARD.encode(&signature); + modified += 1; + } + } + } + assert_eq!(modified, 1, "sanity check"); + + let nameservers_ds = nameservers_ns.ds().clone(); + let nameservers_ns = nameservers_ns.start()?; + + com_ns + .referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ) + .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()) + .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(); + + let root_ns = root_ns.start()?; + + let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; + + let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + + let mut settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + // the resolver will try to validate the chain of trust; the validation fails so it responds + // with SERVFAIL + assert!(output.status.is_servfail()); + + // avoids a SERVFAIL response + settings.checking_disabled(); + + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + // when the CD (Checking Disabled) bit is set the server won't respond with SERVFAIL on + // validation errors. the outcome of the validation process is reported in the AD bit + assert!(output.status.is_noerror()); + assert!(!output.flags.authenticated_data); + + Ok(()) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs new file mode 100644 index 00000000..b3571ce2 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -0,0 +1,121 @@ +use std::net::Ipv4Addr; + +use dns_test::client::{Client, DigSettings}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; + +// no DS records are involved; this is a single-link chain of trust +#[ignore] +#[test] +fn can_validate_without_delegation() -> Result<()> { + let network = Network::new()?; + let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); + let ns = ns.sign()?; + + let root_ksk = ns.key_signing_key().clone(); + let root_zsk = ns.zone_signing_key().clone(); + + eprintln!("root.zone.signed:\n{}", ns.signed_zone_file()); + + let ns = ns.start()?; + + eprintln!("root.zone:\n{}", ns.zone_file()); + + let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())]; + + let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?; + + assert!(output.status.is_noerror()); + assert!(output.flags.authenticated_data); + + let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; + assert!(output.starts_with("; fully validated")); + + Ok(()) +} + +#[ignore] +#[test] +fn can_validate_with_delegation() -> Result<()> { + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let peer = dns_test::peer(); + let network = Network::new()?; + let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .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()?; + + eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); + + com_ns + .referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ) + .add(nameservers_ds); + let com_ns = com_ns.sign()?; + let com_ds = com_ns.ds().clone(); + let com_ns = com_ns.start()?; + + eprintln!("com.zone:\n{}", com_ns.zone_file()); + + root_ns + .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) + .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(); + + eprintln!("root.zone.signed:\n{}", root_ns.signed_zone_file()); + + let root_ns = root_ns.start()?; + + eprintln!("root.zone:\n{}", root_ns.zone_file()); + + let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; + + let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + assert!(output.status.is_noerror()); + + assert!(output.flags.authenticated_data); + + let [a] = output.answer.try_into().unwrap(); + let a = a.try_into_a().unwrap(); + + assert_eq!(needle_fqdn, a.fqdn); + assert_eq!(expected_ipv4_addr, a.ipv4_addr); + + let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, &trust_anchor)?; + assert!(output.starts_with("; fully validated")); + + Ok(()) +} + +// TODO nxdomain with NSEC records +// TODO nxdomain with NSEC3 records diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index 7dc0924a..eacbbf9c 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -18,3 +18,4 @@ doctest = false [dev-dependencies] ctrlc = "3.4.2" +pretty_assertions = "1.4.0" 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/client.rs b/packages/dns-test/src/client.rs index 069f2562..85e99eb1 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -29,7 +29,7 @@ impl Client { &self, server: Ipv4Addr, record_type: RecordType, - fqdn: &FQDN<'_>, + fqdn: &FQDN, trust_anchor: &TrustAnchor, ) -> Result { const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys"; @@ -53,16 +53,17 @@ impl Client { pub fn dig( &self, - recurse: Recurse, - dnssec: Dnssec, + settings: DigSettings, server: Ipv4Addr, record_type: RecordType, - fqdn: &FQDN<'_>, + fqdn: &FQDN, ) -> Result { let output = self.inner.stdout(&[ "dig", - recurse.as_str(), - dnssec.as_str(), + settings.rdflag(), + settings.do_bit(), + settings.adflag(), + settings.cdflag(), &format!("@{server}"), record_type.as_str(), fqdn.as_str(), @@ -72,32 +73,68 @@ impl Client { } } -#[derive(Clone, Copy)] -pub enum Dnssec { - Yes, - No, +#[derive(Clone, Copy, Default)] +pub struct DigSettings { + adflag: bool, + cdflag: bool, + dnssec: bool, + recurse: bool, } -impl Dnssec { - fn as_str(&self) -> &'static str { - match self { - Self::Yes => "+dnssec", - Self::No => "+nodnssec", +impl DigSettings { + /// Sets the AD bit in the query + pub fn authentic_data(&mut self) -> &mut Self { + self.adflag = true; + self + } + + fn adflag(&self) -> &'static str { + if self.adflag { + "+adflag" + } else { + "+noadflag" } } -} -#[derive(Clone, Copy)] -pub enum Recurse { - Yes, - No, -} + /// Sets the CD bit in the query + pub fn checking_disabled(&mut self) -> &mut Self { + self.cdflag = true; + self + } -impl Recurse { - fn as_str(&self) -> &'static str { - match self { - Self::Yes => "+recurse", - Self::No => "+norecurse", + fn cdflag(&self) -> &'static str { + if self.cdflag { + "+cdflag" + } else { + "+nocdflag" + } + } + + /// Sets the DO bit in the query + pub fn dnssec(&mut self) -> &mut Self { + self.dnssec = true; + self + } + + fn do_bit(&self) -> &'static str { + if self.dnssec { + "+dnssec" + } else { + "+nodnssec" + } + } + + /// Sets the RD bit in the query + pub fn recurse(&mut self) -> &mut Self { + self.recurse = true; + self + } + + fn rdflag(&self) -> &'static str { + if self.recurse { + "+recurse" + } else { + "+norecurse" } } } @@ -184,11 +221,12 @@ impl FromStr for DigOutput { #[derive(Debug, Default, PartialEq)] pub struct DigFlags { - pub qr: bool, - pub recursion_desired: bool, - pub recursion_available: bool, - pub authoritative_answer: bool, pub authenticated_data: bool, + pub authoritative_answer: bool, + pub checking_disabled: bool, + pub qr: bool, + pub recursion_available: bool, + pub recursion_desired: bool, } impl FromStr for DigFlags { @@ -200,6 +238,7 @@ impl FromStr for DigFlags { let mut recursion_available = false; let mut authoritative_answer = false; let mut authenticated_data = false; + let mut checking_disabled = false; for flag in input.split_whitespace() { match flag { @@ -208,16 +247,18 @@ impl FromStr for DigFlags { "ra" => recursion_available = true, "aa" => authoritative_answer = true, "ad" => authenticated_data = true, + "cd" => checking_disabled = true, _ => return Err(format!("unknown flag: {flag}").into()), } } Ok(Self { - qr, - recursion_desired, - recursion_available, - authoritative_answer, authenticated_data, + authoritative_answer, + checking_disabled, + qr, + recursion_available, + recursion_desired, }) } } diff --git a/packages/dns-test/src/fqdn.rs b/packages/dns-test/src/fqdn.rs index 18e08a8b..5c3d8678 100644 --- a/packages/dns-test/src/fqdn.rs +++ b/packages/dns-test/src/fqdn.rs @@ -5,13 +5,13 @@ use std::borrow::Cow; use crate::{Error, Result}; #[derive(Clone, PartialEq)] -pub struct FQDN<'a> { - inner: Cow<'a, str>, +pub struct FQDN { + inner: Cow<'static, str>, } // TODO likely needs further validation #[allow(non_snake_case)] -pub fn FQDN<'a>(input: impl Into>) -> Result> { +pub fn FQDN(input: impl Into>) -> Result { let input = input.into(); if !input.ends_with('.') { return Err("FQDN must end with a `.`".into()); @@ -24,12 +24,12 @@ pub fn FQDN<'a>(input: impl Into>) -> Result> { Ok(FQDN { inner: input }) } -impl<'a> FQDN<'a> { - pub const ROOT: FQDN<'static> = FQDN { +impl FQDN { + pub const ROOT: FQDN = FQDN { inner: Cow::Borrowed("."), }; - pub const COM: FQDN<'static> = FQDN { + pub const COM: FQDN = FQDN { inner: Cow::Borrowed("com."), }; @@ -41,7 +41,7 @@ impl<'a> FQDN<'a> { &self.inner } - pub fn into_owned(self) -> FQDN<'static> { + pub fn into_owned(self) -> FQDN { let owned = match self.inner { Cow::Borrowed(borrowed) => borrowed.to_string(), Cow::Owned(owned) => owned, @@ -53,21 +53,21 @@ impl<'a> FQDN<'a> { } } -impl FromStr for FQDN<'static> { +impl FromStr for FQDN { type Err = Error; fn from_str(input: &str) -> Result { - Ok(FQDN(input)?.into_owned()) + FQDN(input.to_string()) } } -impl fmt::Debug for FQDN<'_> { +impl fmt::Debug for FQDN { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } -impl fmt::Display for FQDN<'_> { +impl fmt::Display for FQDN { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.inner) } 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 db9aab0a..5015ab74 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -2,17 +2,18 @@ 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<'a, State> { +pub struct NameServer { container: Container, - zone_file: ZoneFile<'a>, + zone_file: ZoneFile, state: State, } -impl<'a> NameServer<'a, Stopped> { +impl NameServer { /// Spins up a primary name server that has authority over the given `zone` /// /// The initial state of the server is the "Stopped" state where it won't answer any query. @@ -25,7 +26,7 @@ impl<'a> NameServer<'a, Stopped> { /// - one SOA record, with the primary name server field set to this name server's FQDN /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone - pub fn new(implementation: Implementation, zone: FQDN<'a>, network: &Network) -> Result { + pub fn new(implementation: Implementation, zone: FQDN, network: &Network) -> Result { assert!( matches!(implementation, Implementation::Unbound), "currently only `unbound` (`nsd`) can be used as a `NameServer`" @@ -34,18 +35,16 @@ impl<'a> NameServer<'a, Stopped> { 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 { @@ -56,30 +55,19 @@ impl<'a> NameServer<'a, Stopped> { } /// Adds a NS + A record pair to the zone file - pub fn referral( - &mut self, - zone: FQDN<'a>, - nameserver: FQDN<'a>, - ipv4_addr: Ipv4Addr, - ) -> &mut Self { + pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self { self.zone_file.referral(zone, nameserver, ipv4_addr); self } - /// Adds an A record pair to the zone file - pub fn a(&mut self, fqdn: FQDN<'a>, 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 } /// Freezes and signs the name server's zone file - pub fn sign(self) -> Result> { + pub fn sign(self) -> Result> { // TODO do we want to make these settings configurable? const ZSK_BITS: usize = 1024; const KSK_BITS: usize = 2048; @@ -94,19 +82,19 @@ impl<'a> NameServer<'a, Stopped> { 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,26 +108,28 @@ impl<'a> NameServer<'a, Stopped> { let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed"); let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?; - // we have an in-memory representation of the zone file so we just delete the on-disk version let zone_file_path = zone_file_path(); - container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; + let signed: ZoneFile = container + .stdout(&["cat", &format!("{zone_file_path}.signed")])? + .parse()?; - 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, + signed, + // inherit SOA's TTL value + ksk: ksk.with_ttl(ttl), + zsk: zsk.with_ttl(ttl), }, }) } /// Moves the server to the "Start" state where it can answer client queries - pub fn start(self) -> Result> { + pub fn start(self) -> Result> { let Self { container, zone_file, @@ -149,7 +139,7 @@ impl<'a> NameServer<'a, Stopped> { // 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())?; @@ -176,19 +166,21 @@ fn ns_count() -> usize { COUNT.fetch_add(1, atomic::Ordering::Relaxed) } -impl<'a> NameServer<'a, Signed> { +impl NameServer { /// Moves the server to the "Start" state where it can answer client queries - pub fn start(self) -> Result> { + pub fn start(self) -> Result> { let Self { container, zone_file, - state: _, + state, } = self; // 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.cp(&zone_file_path(), &state.signed.to_string())?; let child = container.spawn(&["nsd", "-d"])?; @@ -199,16 +191,20 @@ impl<'a> NameServer<'a, Signed> { }) } - 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 } - pub fn signed_zone_file(&self) -> &str { - &self.state.signed_zone_file + pub fn signed_zone_file(&self) -> &ZoneFile { + &self.state.signed + } + + pub fn signed_zone_file_mut(&mut self) -> &mut ZoneFile { + &mut self.state.signed } pub fn ds(&self) -> &DS { @@ -216,7 +212,7 @@ impl<'a> NameServer<'a, Signed> { } } -impl<'a> NameServer<'a, Running> { +impl NameServer { /// Starts a `tshark` instance that captures DNS messages flowing through this network node pub fn eavesdrop(&self) -> Result { self.container.eavesdrop() @@ -246,7 +242,7 @@ kill -TERM $(cat {pidfile})" } } -impl<'a, S> NameServer<'a, S> { +impl NameServer { pub fn container_id(&self) -> &str { self.container.id() } @@ -256,15 +252,15 @@ impl<'a, S> NameServer<'a, S> { } /// Zone file BEFORE signing - pub fn zone_file(&self) -> &ZoneFile<'a> { + pub fn zone_file(&self) -> &ZoneFile { &self.zone_file } - pub fn zone(&self) -> &FQDN<'a> { - &self.zone_file.origin + pub fn zone(&self) -> &FQDN { + self.zone_file.origin() } - pub fn fqdn(&self) -> &FQDN<'a> { + pub fn fqdn(&self) -> &FQDN { &self.zone_file.soa.nameserver } } @@ -273,20 +269,20 @@ pub struct Stopped; pub struct Signed { ds: DS, - zsk: DNSKEY, - ksk: DNSKEY, - signed_zone_file: String, + zsk: record::DNSKEY, + ksk: record::DNSKEY, + signed: ZoneFile, } pub struct Running { child: Child, } -fn primary_ns(ns_count: usize) -> FQDN<'static> { +fn primary_ns(ns_count: usize) -> FQDN { FQDN(format!("primary{ns_count}.nameservers.com.")).unwrap() } -fn admin_ns(ns_count: usize) -> FQDN<'static> { +fn admin_ns(ns_count: usize) -> FQDN { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } @@ -299,7 +295,7 @@ fn nsd_conf(fqdn: &FQDN) -> String { #[cfg(test)] mod tests { - use crate::client::{Client, Dnssec, Recurse}; + use crate::client::{Client, DigSettings}; use crate::record::RecordType; use super::*; @@ -311,13 +307,7 @@ mod tests { let ip_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::No, - Dnssec::No, - ip_addr, - RecordType::SOA, - &FQDN::COM, - )?; + let output = client.dig(DigSettings::default(), ip_addr, RecordType::SOA, &FQDN::COM)?; assert!(output.status.is_noerror()); @@ -342,8 +332,7 @@ mod tests { let client = Client::new(&network)?; let output = client.dig( - Recurse::No, - Dnssec::No, + DigSettings::default(), ipv4_addr, RecordType::NS, &FQDN::COM, @@ -368,13 +357,8 @@ mod tests { let ns_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::No, - Dnssec::Yes, - ns_addr, - RecordType::SOA, - &FQDN::ROOT, - )?; + let settings = *DigSettings::default().dnssec(); + let output = client.dig(settings, ns_addr, RecordType::SOA, &FQDN::ROOT)?; assert!(output.status.is_noerror()); diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index 0f6885ac..0abc7df8 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -1,53 +1,109 @@ //! Text representation of DNS records -use core::array; use core::result::Result as CoreResult; use core::str::FromStr; +use core::{array, fmt}; +use std::any; +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, - NS, - SOA, -} +const CLASS: &str = "IN"; // "internet" -impl RecordType { - pub fn as_str(&self) -> &'static str { - match self { - RecordType::A => "A", - RecordType::SOA => "SOA", - RecordType::NS => "NS", +macro_rules! record_types { + ($($variant:ident),*) => { + #[allow(clippy::upper_case_acronyms)] + #[derive(Debug, PartialEq)] + pub enum RecordType { + $($variant),* } - } + + impl RecordType { + pub fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant)),* + } + } + } + + impl FromStr for RecordType { + type Err = Error; + + fn from_str(input: &str) -> Result { + $(if input == stringify!($variant) { + return Ok(Self::$variant); + })* + + Err(format!("unknown record type: {input}").into()) + } + } + + impl fmt::Display for RecordType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } + } + }; } -impl FromStr for RecordType { - type Err = Error; - - fn from_str(input: &str) -> CoreResult { - let record_type = match input { - "A" => Self::A, - "SOA" => Self::SOA, - "NS" => Self::NS, - _ => return Err(format!("unknown record type: {input}").into()), - }; - - Ok(record_type) - } -} +record_types!(A, AAAA, DNSKEY, DS, MX, NS, NSEC3, NSEC3PARAM, RRSIG, SOA, TXT); #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + DNSKEY(DNSKEY), + DS(DS), + NS(NS), + NSEC3(NSEC3), + NSEC3PARAM(NSEC3PARAM), RRSIG(RRSIG), SOA(SOA), } +impl From for Record { + fn from(v: NSEC3) -> Self { + Self::NSEC3(v) + } +} + +impl From for Record { + fn from(v: DNSKEY) -> Self { + Self::DNSKEY(v) + } +} + +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 +124,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 { @@ -81,7 +155,11 @@ impl FromStr for Record { let record = match record_type { "A" => Record::A(input.parse()?), - "NS" => todo!(), + "DNSKEY" => Record::DNSKEY(input.parse()?), + "DS" => Record::DS(input.parse()?), + "NS" => Record::NS(input.parse()?), + "NSEC3" => Record::NSEC3(input.parse()?), + "NSEC3PARAM" => Record::NSEC3PARAM(input.parse()?), "RRSIG" => Record::RRSIG(input.parse()?), "SOA" => Record::SOA(input.parse()?), _ => return Err(format!("unknown record type: {record_type}").into()), @@ -91,9 +169,24 @@ 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::DNSKEY(dnskey) => write!(f, "{dnskey}"), + Record::NS(ns) => write!(f, "{ns}"), + Record::NSEC3(nsec3) => write!(f, "{nsec3}"), + Record::NSEC3PARAM(nsec3param) => write!(f, "{nsec3param}"), + Record::RRSIG(rrsig) => write!(f, "{rrsig}"), + Record::SOA(soa) => write!(f, "{soa}"), + } + } +} + #[derive(Debug)] pub struct A { - pub fqdn: FQDN<'static>, + pub fqdn: FQDN, pub ttl: u32, pub ipv4_addr: Ipv4Addr, } @@ -110,16 +203,8 @@ impl FromStr for A { return Err("expected 5 columns".into()); }; - let expected = "A"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` record as an {expected} record").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; Ok(Self { fqdn: fqdn.parse()?, @@ -129,19 +214,351 @@ 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; + + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\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(mut input: &str) -> Result { + if let Some((rr, _comment)) = input.rsplit_once(" ;") { + input = rr.trim_end(); + } + + 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()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + 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; + + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\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) -> 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)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 7 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + 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; + + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\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; + + let record_type = unqualified_type_name::(); + write!(f, "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{nameserver}") + } +} + +impl FromStr for NS { + 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(nameserver), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 5 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + nameserver: nameserver.parse()?, + }) + } +} + +// integer types chosen based on bit sizes in section 3.2 of RFC5155 +#[derive(Debug)] +pub struct NSEC3 { + pub fqdn: FQDN, + pub ttl: u32, + pub hash_alg: u8, + pub flags: u8, + pub iterations: u16, + pub salt: String, + pub next_hashed_owner_name: String, + pub record_types: Vec, +} + +impl FromStr for NSEC3 { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(hash_alg), Some(flags), Some(iterations), Some(salt), Some(next_hashed_owner_name)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 9 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + let mut record_types = vec![]; + for column in columns { + record_types.push(column.parse()?); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + hash_alg: hash_alg.parse()?, + flags: flags.parse()?, + iterations: iterations.parse()?, + salt: salt.to_string(), + next_hashed_owner_name: next_hashed_owner_name.to_string(), + record_types, + }) + } +} + +impl fmt::Display for NSEC3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + hash_alg, + flags, + iterations, + salt, + next_hashed_owner_name, + record_types, + } = self; + + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} {salt} {next_hashed_owner_name}")?; + + for record_type in record_types { + write!(f, " {record_type}")?; + } + + Ok(()) + } +} + +// integer types chosen based on bit sizes in section 4.2 of RFC5155 +#[derive(Debug)] +pub struct NSEC3PARAM { + pub zone: FQDN, + pub ttl: u32, + pub hash_alg: u8, + pub flags: u8, + pub iterations: u16, +} + +impl FromStr for NSEC3PARAM { + 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(hash_alg), Some(flags), Some(iterations), Some(dash), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 8 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + if dash != "-" { + todo!("salt is not implemented") + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + hash_alg: hash_alg.parse()?, + flags: flags.parse()?, + iterations: iterations.parse()?, + }) + } +} + +impl fmt::Display for NSEC3PARAM { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + hash_alg, + flags, + iterations, + } = self; + + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} -" + ) + } +} + +// integer types chosen based on bit sizes in section 3.1 of RFC4034 #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct RRSIG { - pub fqdn: FQDN<'static>, + pub fqdn: FQDN, pub ttl: u32, pub type_covered: RecordType, - pub algorithm: u32, - pub labels: u32, + pub algorithm: u8, + pub labels: u8, pub original_ttl: u32, + // NOTE on the wire these are 32-bit UNIX timestamps but in text representation they are + // `strftime` formatted + // TODO switch these to `chrono::DateTime`? pub signature_expiration: u64, pub signature_inception: u64, - pub key_tag: u32, - pub signer_name: FQDN<'static>, + pub key_tag: u16, + pub signer_name: FQDN, /// base64 encoded pub signature: String, } @@ -158,16 +575,8 @@ impl FromStr for RRSIG { return Err("expected at least 12 columns".into()); }; - let expected = "RRSIG"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` record as a {expected} record").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; let mut signature = String::new(); for column in columns { @@ -190,18 +599,37 @@ 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; + + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\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 { - pub zone: FQDN<'static>, + pub zone: FQDN, pub ttl: u32, - pub nameserver: FQDN<'static>, - pub admin: FQDN<'static>, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, + pub nameserver: FQDN, + pub admin: FQDN, + pub settings: SoaSettings, } impl FromStr for SOA { @@ -216,80 +644,377 @@ impl FromStr for SOA { return Err("expected 11 columns".into()); }; - if record_type != "SOA" { - return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; Ok(Self { zone: zone.parse()?, 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; + + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\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 check_class(class: &str) -> Result<()> { + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(()) +} + +fn check_record_type(record_type: &str) -> Result<()> { + let expected = unqualified_type_name::(); + if record_type == expected { + Ok(()) + } else { + Err(format!("tried to parse `{record_type}` record as an {expected} record").into()) + } +} + +fn unqualified_type_name() -> &'static str { + let name = any::type_name::(); + if let Some((_rest, component)) = name.rsplit_once(':') { + component + } else { + name + } +} + +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()?; + use pretty_assertions::assert_eq; - 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); + // dig A a.root-servers.net + const A_INPUT: &str = "a.root-servers.net. 77859 IN A 198.41.0.4"; + + #[test] + fn a() -> Result<()> { + let a @ A { + fqdn, + ttl, + ipv4_addr, + } = &A_INPUT.parse()?; + + 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!(A_INPUT, output); + + Ok(()) + } + + // dig DNSKEY . + const DNSKEY_INPUT: &str = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU="; + + #[test] + fn dnskey() -> Result<()> { + let dnskey @ DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } = &DNSKEY_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!(DNSKEY_INPUT, output); 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 parsing_dnskey_ignores_trailing_comment() -> Result<()> { + // `ldns-signzone`'s output + const DNSKEY_INPUT2: &str = ". 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15 ;{id = 11387 (zsk), size = 1024b}"; - let soa: SOA = input.parse()?; + let DNSKEY { public_key, .. } = DNSKEY_INPUT2.parse()?; + + let expected = "AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15"; + assert_eq!(expected, public_key); + + Ok(()) + } + + // dig DS com. + const DS_INPUT: &str = + "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A"; + + #[test] + fn ds() -> Result<()> { + let ds @ DS { + zone, + ttl, + key_tag, + algorithm, + digest_type, + digest, + } = &DS_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!(DS_INPUT, output); + + Ok(()) + } + + // dig NS . + const NS_INPUT: &str = ". 86400 IN NS f.root-servers.net."; + + #[test] + fn ns() -> Result<()> { + let ns @ NS { + zone, + ttl, + nameserver, + } = &NS_INPUT.parse()?; + + assert_eq!(FQDN::ROOT, *zone); + assert_eq!(86400, *ttl); + assert_eq!("f.root-servers.net.", nameserver.as_str()); + + let output = ns.to_string(); + assert_eq!(NS_INPUT, output); + + Ok(()) + } + + // dig +dnssec A unicorn.example.com. + const NSEC3_INPUT: &str = "abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com. 3571 IN NSEC3 1 0 5 53BCBC5805D2B761 GVPMD82B8ER38VUEGP72I721LIH19RGR A NS SOA MX TXT AAAA RRSIG DNSKEY NSEC3PARAM"; + + #[test] + fn nsec3() -> Result<()> { + let nsec3 @ NSEC3 { + fqdn, + ttl, + hash_alg, + flags, + iterations, + salt, + next_hashed_owner_name, + record_types, + } = &NSEC3_INPUT.parse()?; + + assert_eq!( + "abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com.", + fqdn.as_str() + ); + assert_eq!(3571, *ttl); + assert_eq!(1, *hash_alg); + assert_eq!(0, *flags); + assert_eq!(5, *iterations); + assert_eq!("53BCBC5805D2B761", salt); + assert_eq!("GVPMD82B8ER38VUEGP72I721LIH19RGR", next_hashed_owner_name); + assert_eq!( + [ + RecordType::A, + RecordType::NS, + RecordType::SOA, + RecordType::MX, + RecordType::TXT, + RecordType::AAAA, + RecordType::RRSIG, + RecordType::DNSKEY, + RecordType::NSEC3PARAM + ], + record_types.as_slice() + ); + + let output = nsec3.to_string(); + assert_eq!(NSEC3_INPUT, output); + + Ok(()) + } + + // dig NSEC3PARAM com. + const NSEC3PARAM_INPUT: &str = "com. 86238 IN NSEC3PARAM 1 0 0 -"; + + #[test] + fn nsec3param() -> Result<()> { + let nsec3param @ NSEC3PARAM { + zone, + ttl, + hash_alg, + flags, + iterations, + } = &NSEC3PARAM_INPUT.parse()?; + + assert_eq!(FQDN::COM, *zone); + assert_eq!(86238, *ttl); + assert_eq!(1, *hash_alg); + assert_eq!(0, *flags); + assert_eq!(0, *iterations); + + let output = nsec3param.to_string(); + assert_eq!(NSEC3PARAM_INPUT, output); + + Ok(()) + } + + // dig +dnssec SOA . + const RRSIG_INPUT: &str = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + + #[test] + fn rrsig() -> Result<()> { + let rrsig @ RRSIG { + fqdn, + ttl, + type_covered, + algorithm, + labels, + original_ttl, + signature_expiration, + signature_inception, + key_tag, + signer_name, + signature, + } = &RRSIG_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!(RRSIG_INPUT, output); + + Ok(()) + } + + // dig SOA . + const SOA_INPUT: &str = + ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + + #[test] + fn soa() -> Result<()> { + let soa: SOA = SOA_INPUT.parse()?; assert_eq!(".", soa.zone.as_str()); 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); + + let output = soa.to_string(); + assert_eq!(SOA_INPUT, output); 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); + fn any() -> Result<()> { + assert!(matches!(A_INPUT.parse()?, Record::A(..))); + assert!(matches!(DNSKEY_INPUT.parse()?, Record::DNSKEY(..))); + assert!(matches!(DS_INPUT.parse()?, Record::DS(..))); + assert!(matches!(NS_INPUT.parse()?, Record::NS(..))); + assert!(matches!(NSEC3_INPUT.parse()?, Record::NSEC3(..))); + assert!(matches!(NSEC3PARAM_INPUT.parse()?, Record::NSEC3PARAM(..))); + assert!(matches!(RRSIG_INPUT.parse()?, Record::RRSIG(..))); + assert!(matches!(SOA_INPUT.parse()?, Record::SOA(..))); 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..0aef4397 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -244,9 +244,9 @@ struct Ip { #[cfg(test)] mod tests { - use crate::client::{Client, Dnssec, Recurse}; + use crate::client::{Client, DigSettings}; 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}; @@ -260,8 +260,7 @@ mod tests { let client = Client::new(network)?; let resp = client.dig( - Recurse::No, - Dnssec::No, + DigSettings::default(), ns.ipv4_addr(), RecordType::SOA, &FQDN::ROOT, @@ -297,8 +296,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( @@ -322,13 +321,8 @@ mod tests { let resolver_addr = resolver.ipv4_addr(); let client = Client::new(network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - dbg!(resolver_addr), - RecordType::A, - root_ns.fqdn(), - )?; + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, dbg!(resolver_addr), RecordType::A, root_ns.fqdn())?; assert!(output.status.is_noerror()); diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file.rs deleted file mode 100644 index 8dc153c9..00000000 --- a/packages/dns-test/src/zone_file.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! BIND-style zone file -//! -//! 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 std::net::Ipv4Addr; -use std::str::FromStr; - -use crate::{Error, FQDN}; - -pub struct ZoneFile<'a> { - pub origin: FQDN<'a>, - pub ttl: u32, - pub soa: SOA<'a>, - pub entries: Vec>, -} - -impl<'a> ZoneFile<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN<'a>, soa: SOA<'a>) -> Self { - Self { - origin, - ttl: 1800, - soa, - entries: Vec::new(), - } - } - - /// Appends an entry - pub fn entry(&mut self, entry: impl Into>) { - self.entries.push(entry.into()) - } - - /// Appends a NS + A entry pair - pub fn referral(&mut self, zone: FQDN<'a>, nameserver: FQDN<'a>, ipv4_addr: Ipv4Addr) { - self.entry(NS { - zone: zone.clone(), - nameserver: nameserver.clone(), - }); - self.entry(A { - fqdn: nameserver, - ipv4_addr, - }); - } -} - -impl fmt::Display for ZoneFile<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - origin, - ttl, - soa, - entries, - } = self; - - writeln!(f, "$ORIGIN {origin}")?; - writeln!(f, "$TTL {ttl}")?; - writeln!(f, "{soa}")?; - - for entry in entries { - writeln!(f, "{entry}")?; - } - - Ok(()) - } -} - -pub struct Root<'a> { - pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, - pub ttl: u32, -} - -impl<'a> Root<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { - Self { - ipv4_addr, - ns, - ttl: 3600000, // 1000 hours - } - } -} - -impl fmt::Display for Root<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { ipv4_addr, ns, ttl } = self; - - writeln!(f, ".\t{ttl}\tNS\t{ns}")?; - write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") - } -} - -pub enum Entry<'a> { - A(A<'a>), - DNSKEY(DNSKEY), - DS(DS), - NS(NS<'a>), -} - -impl<'a> From for Entry<'a> { - fn from(v: DS) -> Self { - Self::DS(v) - } -} - -impl<'a> From> for Entry<'a> { - fn from(v: A<'a>) -> Self { - Self::A(v) - } -} - -impl<'a> From> for Entry<'a> { - fn from(v: NS<'a>) -> 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<'a> { - pub fqdn: FQDN<'a>, - 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 { - zone: FQDN<'static>, - 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 { - 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) -> Result { - let (before, after) = input.split_once(';').ok_or("comment was not found")?; - let mut columns = before.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()) - else { - return Err("expected 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()); - } - - // {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<'static>, - _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<'a> { - pub zone: FQDN<'a>, - pub nameserver: FQDN<'a>, -} - -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<'a> { - pub zone: FQDN<'a>, - pub nameserver: FQDN<'a>, - pub admin: FQDN<'a>, - 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()); - - 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(), - }) - } -} diff --git a/packages/dns-test/src/zone_file/mod.rs b/packages/dns-test/src/zone_file/mod.rs new file mode 100644 index 00000000..57a6c29a --- /dev/null +++ b/packages/dns-test/src/zone_file/mod.rs @@ -0,0 +1,225 @@ +//! 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::fmt; +use std::array; +use std::net::Ipv4Addr; +use std::str::FromStr; + +use crate::record::{self, Record, SOA}; +use crate::{Error, Result, DEFAULT_TTL, FQDN}; + +pub struct ZoneFile { + origin: FQDN, + pub soa: SOA, + pub records: Vec, +} + +impl ZoneFile { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(soa: SOA) -> Self { + Self { + origin: soa.zone.clone(), + soa, + records: Vec::new(), + } + } + + /// Adds the given `record` to the zone file + pub fn add(&mut self, record: impl Into) { + self.records.push(record.into()) + } + + /// Shortcut method for adding a referral (NS + A record pair) + pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) { + 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 { soa, records, .. } = self; + + writeln!(f, "{soa}")?; + for record in records { + writeln!(f, "{record}")?; + } + + Ok(()) + } +} + +impl FromStr for ZoneFile { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut records = vec![]; + let mut maybe_soa = None; + for line in input.lines() { + let line = line.trim(); + + if line.is_empty() { + continue; + } + + let record: Record = line.parse()?; + if let Record::SOA(soa) = record { + if maybe_soa.is_some() { + return Err("found more than one SOA record".into()); + } + + maybe_soa = Some(soa); + } else { + records.push(record) + } + } + + let soa = maybe_soa.ok_or("no SOA record found in zone file")?; + Ok(Self { + origin: soa.zone.clone(), + soa, + records, + }) + } +} + +/// A root (server) hint +pub struct Root { + pub ipv4_addr: Ipv4Addr, + pub ns: FQDN, + pub ttl: u32, +} + +impl Root { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(ns: FQDN, ipv4_addr: Ipv4Addr) -> Self { + Self { + ipv4_addr, + ns, + ttl: DEFAULT_TTL, + } + } +} + +impl fmt::Display for Root { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { ipv4_addr, ns, ttl } = self; + + writeln!(f, ".\t{ttl}\tNS\t{ns}")?; + write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") + } +} + +// 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, +} + +impl DNSKEY { + pub fn with_ttl(self, ttl: u32) -> record::DNSKEY { + let Self { + zone, + flags, + protocol, + algorithm, + public_key, + } = self; + + record::DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } + } +} + +impl FromStr for DNSKEY { + type Err = Error; + + 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()) + else { + return Err("expected 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()); + } + + Ok(Self { + zone: zone.parse()?, + flags: flags.parse()?, + protocol: protocol.parse()?, + algorithm: algorithm.parse()?, + public_key: public_key.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pretty_assertions::assert_eq; + + #[test] + 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 roundtrip() -> Result<()> { + // `ldns-signzone`'s output minus trailing comments; long trailing fields have been split as well + let input = include_str!("muster.zone"); + let zone: ZoneFile = input.parse()?; + let output = zone.to_string(); + assert_eq!(input, output); + + Ok(()) + } +} diff --git a/packages/dns-test/src/zone_file/muster.zone b/packages/dns-test/src/zone_file/muster.zone new file mode 100644 index 00000000..7fab9f0e --- /dev/null +++ b/packages/dns-test/src/zone_file/muster.zone @@ -0,0 +1,11 @@ +. 86400 IN SOA primary0.nameservers.com. admin0.nameservers.com. 2024022028 1800 900 604800 86400 +. 86400 IN RRSIG SOA 7 0 86400 20240319104519 20240220104519 11387 . Ks9b5tMyNxxrvw3JkgGkR2H5NPqTDwAwmwh3B7iNC0UHAYGU4B01ZJHj DIsJqDoJ2hsKG5oq0hQuwBSKv2nSBA1oSQcNrBDzOk105gu6tsXg2O8V ZCpAtEColco5ziOX8AWRqRMM5adSfA4xyj5H3NToMjRVDLpVpZsU4BAa 4dU= +. 86400 IN NS primary0.nameservers.com. +. 86400 IN RRSIG NS 7 0 86400 20240319104519 20240220104519 11387 . rZpACeVX3m2CwI/gY/rVYNOAs6ge4h+M74yV+CoAZYJaJLjeHd+jY0YV ixU3hap9bbFCZqhXKU5WSpJSsc/9PrgxEt2XycpbvAJwvIwdqWLUW741 /AOwnyrgv+7PLp4vkDdeLI9tcsY5V/ABpQrYW2i8Gtz90OEpvEEd5+4C LyU= +. 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQ iakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJP QtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nu hfWE4w15 +. 86400 IN DNSKEY 257 3 7 AwEAAco2Ck4XM5M4RO+QiwZhMFW9Hf8s0cOWH6QZ8OUQisjP6n+gYsbE pOOHhRiABN+QuVhRK9BN+Mt0LqMSBjSy53t5P3NerckqUTQ4HlZkn2QK bhc+TOgvHN5iDj0RBMkTaJ09y5vYmeNv5npFk6hV+VsbBoFRLXTuPSms 8LsH72W6y1HEHNzvAd5H3ro1d2awp66CXRTOcbXbFAIELpTgAU6ZJjEo RBMASZ3Ug4oZ96yvegy2OZnAyFsxBGdOvecs+zoYKeezqaq21YMpnZkf eE7RYexGPm1p8/7smQjBph/uoVDp5k5DuPkTmzpafVOn2YHGB395vT37 uLi9B5Oef9c= +. 86400 IN RRSIG DNSKEY 7 0 86400 20240319104519 20240220104519 11245 . yH/aEcWQhgfmf8RjByMYDDuglaquWsHECA+nRmedIA4Kz7Vc74f77JLi QrhvFFSIFkQNyixNsTugLmTZunphbLrbQNKTWw8gpgd/8u6Oc9OdTYJu T+ADL+Rrgge7mDkPjDRKNhQ6VkIiRzwLBFhoYTA1LZF98CAnJGQcpw4W 1YCkqPbXIzsa3hq2OajC8NzZMEgeI95N1CJ/o5AmhwLtWVuv04q+seGX roSiTlWIQRKGsbCR2v97UjMG4l9XIbijzZbY4dK2/4WrCIw9mjp/cSE8 r/AdfegTi1oqOM9i4QebKvyU9c3rnJRVbFMhEXL1e0M/5bZNytXp43ex VTTHcA== +. 3600 IN NSEC3PARAM 1 0 1 - +. 3600 IN RRSIG NSEC3PARAM 7 0 3600 20240319104519 20240220104519 11387 . IhM+g5s6DwlFKbj6+zd+f/CqN1I4/QtF0aTOMvf0c+s5l+emx/yZEVCT 8LdX4cmz72eYeC4w/dM2btrhhHohhb/hdK1v7ukxtBVgvk6pOmuye2/E cuGkll7B59l+wlRaSmeXAQjiCUX6gyg9tlvmtcnomWVgtjIgMKJpggy8 B6k= +fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN NSEC3 1 1 1 - fasdp12mo9fh69ahu5bseugoh3np33tc NS SOA RRSIG DNSKEY NSEC3PARAM +fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN RRSIG NSEC3 7 1 86400 20240319104519 20240220104519 11387 . dsdwsTOGL5BvrC1v/5bmDy5Bz8wnN/IG3XRAg6RqKVMK0fLPMsd5uhXm U2gPJ5xUg1RkBQ5+etlBRm2p7vSDjbMa/hjRbvUJgP+c4dL68g+FcHv4 v9fb1Jaao9Goy/ZxZ1dbwXAdxhi+pyvikCdNcKsdiCtFD9pX7V5Nh2Cc GQQ=