Merge branch 'main' into ja-explicit-license
This commit is contained in:
commit
eda8fd6ec3
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -79,6 +79,7 @@ dependencies = [
|
|||||||
name = "conformance-tests"
|
name = "conformance-tests"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"dns-test",
|
"dns-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -143,12 +144,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dns-test"
|
name = "dns-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
@ -358,6 +366,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.78"
|
version = "1.0.78"
|
||||||
@ -701,3 +719,9 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
@ -6,6 +6,7 @@ publish = false
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.21.7"
|
||||||
dns-test.path = "../dns-test"
|
dns-test.path = "../dns-test"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::net::Ipv4Addr;
|
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::name_server::NameServer;
|
||||||
use dns_test::record::RecordType;
|
use dns_test::record::{Record, RecordType};
|
||||||
use dns_test::zone_file::Root;
|
use dns_test::zone_file::Root;
|
||||||
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
|
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ fn can_resolve() -> Result<()> {
|
|||||||
let mut nameservers_ns =
|
let mut nameservers_ns =
|
||||||
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
|
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
|
||||||
nameservers_ns
|
nameservers_ns
|
||||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
|
||||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()))
|
||||||
.a(needle_fqdn.clone(), expected_ipv4_addr);
|
.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
|
||||||
let nameservers_ns = nameservers_ns.start()?;
|
let nameservers_ns = nameservers_ns.start()?;
|
||||||
|
|
||||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
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 resolver_ip_addr = resolver.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new(&network)?;
|
let client = Client::new(&network)?;
|
||||||
let output = client.dig(
|
|
||||||
Recurse::Yes,
|
let settings = *DigSettings::default().recurse();
|
||||||
Dnssec::No,
|
let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?;
|
||||||
resolver_ip_addr,
|
|
||||||
RecordType::A,
|
|
||||||
&needle_fqdn,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
@ -75,8 +71,8 @@ fn nxdomain() -> Result<()> {
|
|||||||
let mut nameservers_ns =
|
let mut nameservers_ns =
|
||||||
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
|
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
|
||||||
nameservers_ns
|
nameservers_ns
|
||||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
|
||||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
|
||||||
let nameservers_ns = nameservers_ns.start()?;
|
let nameservers_ns = nameservers_ns.start()?;
|
||||||
|
|
||||||
com_ns.referral(
|
com_ns.referral(
|
||||||
@ -94,13 +90,8 @@ fn nxdomain() -> Result<()> {
|
|||||||
let resolver_ip_addr = resolver.ipv4_addr();
|
let resolver_ip_addr = resolver.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new(&network)?;
|
let client = Client::new(&network)?;
|
||||||
let output = client.dig(
|
let settings = *DigSettings::default().recurse();
|
||||||
Recurse::Yes,
|
let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?;
|
||||||
Dnssec::No,
|
|
||||||
resolver_ip_addr,
|
|
||||||
RecordType::A,
|
|
||||||
&needle_fqdn,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
assert!(dbg!(output).status.is_nxdomain());
|
assert!(dbg!(output).status.is_nxdomain());
|
||||||
|
|
||||||
|
@ -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::name_server::NameServer;
|
||||||
use dns_test::record::RecordType;
|
use dns_test::record::RecordType;
|
||||||
use dns_test::tshark::{Capture, Direction};
|
use dns_test::tshark::{Capture, Direction};
|
||||||
@ -20,13 +20,8 @@ fn edns_support() -> Result<()> {
|
|||||||
let mut tshark = resolver.eavesdrop()?;
|
let mut tshark = resolver.eavesdrop()?;
|
||||||
|
|
||||||
let client = Client::new(network)?;
|
let client = Client::new(network)?;
|
||||||
let ans = client.dig(
|
let settings = *DigSettings::default().authentic_data().recurse();
|
||||||
Recurse::Yes,
|
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
|
||||||
Dnssec::Yes,
|
|
||||||
resolver.ipv4_addr(),
|
|
||||||
RecordType::SOA,
|
|
||||||
&FQDN::ROOT,
|
|
||||||
)?;
|
|
||||||
assert!(ans.status.is_servfail());
|
assert!(ans.status.is_servfail());
|
||||||
|
|
||||||
tshark.wait_for_capture()?;
|
tshark.wait_for_capture()?;
|
||||||
|
@ -1,131 +1,2 @@
|
|||||||
use std::net::Ipv4Addr;
|
mod bogus;
|
||||||
|
mod secure;
|
||||||
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
|
|
||||||
|
@ -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(())
|
||||||
|
}
|
@ -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
|
@ -18,3 +18,4 @@ doctest = false
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ctrlc = "3.4.2"
|
ctrlc = "3.4.2"
|
||||||
|
pretty_assertions = "1.4.0"
|
||||||
|
@ -2,7 +2,7 @@ use std::sync::mpsc;
|
|||||||
|
|
||||||
use dns_test::client::Client;
|
use dns_test::client::Client;
|
||||||
use dns_test::name_server::NameServer;
|
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::zone_file::Root;
|
||||||
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
|
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)?;
|
let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?;
|
||||||
nameservers_ns
|
nameservers_ns
|
||||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
|
||||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
|
||||||
let nameservers_ns = nameservers_ns.sign()?;
|
let nameservers_ns = nameservers_ns.sign()?;
|
||||||
let nameservers_ds = nameservers_ns.ds().clone();
|
let nameservers_ds = nameservers_ns.ds().clone();
|
||||||
let nameservers_ns = nameservers_ns.start()?;
|
let nameservers_ns = nameservers_ns.start()?;
|
||||||
@ -31,14 +31,14 @@ fn main() -> Result<()> {
|
|||||||
nameservers_ns.fqdn().clone(),
|
nameservers_ns.fqdn().clone(),
|
||||||
nameservers_ns.ipv4_addr(),
|
nameservers_ns.ipv4_addr(),
|
||||||
)
|
)
|
||||||
.ds(nameservers_ds);
|
.add(nameservers_ds);
|
||||||
let com_ns = com_ns.sign()?;
|
let com_ns = com_ns.sign()?;
|
||||||
let com_ds = com_ns.ds().clone();
|
let com_ds = com_ns.ds().clone();
|
||||||
let com_ns = com_ns.start()?;
|
let com_ns = com_ns.start()?;
|
||||||
|
|
||||||
root_ns
|
root_ns
|
||||||
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
.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_ns = root_ns.sign()?;
|
||||||
let root_ksk = root_ns.key_signing_key().clone();
|
let root_ksk = root_ns.key_signing_key().clone();
|
||||||
let root_zsk = root_ns.zone_signing_key().clone();
|
let root_zsk = root_ns.zone_signing_key().clone();
|
||||||
|
@ -29,7 +29,7 @@ impl Client {
|
|||||||
&self,
|
&self,
|
||||||
server: Ipv4Addr,
|
server: Ipv4Addr,
|
||||||
record_type: RecordType,
|
record_type: RecordType,
|
||||||
fqdn: &FQDN<'_>,
|
fqdn: &FQDN,
|
||||||
trust_anchor: &TrustAnchor,
|
trust_anchor: &TrustAnchor,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys";
|
const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys";
|
||||||
@ -53,16 +53,17 @@ impl Client {
|
|||||||
|
|
||||||
pub fn dig(
|
pub fn dig(
|
||||||
&self,
|
&self,
|
||||||
recurse: Recurse,
|
settings: DigSettings,
|
||||||
dnssec: Dnssec,
|
|
||||||
server: Ipv4Addr,
|
server: Ipv4Addr,
|
||||||
record_type: RecordType,
|
record_type: RecordType,
|
||||||
fqdn: &FQDN<'_>,
|
fqdn: &FQDN,
|
||||||
) -> Result<DigOutput> {
|
) -> Result<DigOutput> {
|
||||||
let output = self.inner.stdout(&[
|
let output = self.inner.stdout(&[
|
||||||
"dig",
|
"dig",
|
||||||
recurse.as_str(),
|
settings.rdflag(),
|
||||||
dnssec.as_str(),
|
settings.do_bit(),
|
||||||
|
settings.adflag(),
|
||||||
|
settings.cdflag(),
|
||||||
&format!("@{server}"),
|
&format!("@{server}"),
|
||||||
record_type.as_str(),
|
record_type.as_str(),
|
||||||
fqdn.as_str(),
|
fqdn.as_str(),
|
||||||
@ -72,32 +73,68 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub enum Dnssec {
|
pub struct DigSettings {
|
||||||
Yes,
|
adflag: bool,
|
||||||
No,
|
cdflag: bool,
|
||||||
|
dnssec: bool,
|
||||||
|
recurse: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dnssec {
|
impl DigSettings {
|
||||||
fn as_str(&self) -> &'static str {
|
/// Sets the AD bit in the query
|
||||||
match self {
|
pub fn authentic_data(&mut self) -> &mut Self {
|
||||||
Self::Yes => "+dnssec",
|
self.adflag = true;
|
||||||
Self::No => "+nodnssec",
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adflag(&self) -> &'static str {
|
||||||
|
if self.adflag {
|
||||||
|
"+adflag"
|
||||||
|
} else {
|
||||||
|
"+noadflag"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
/// Sets the CD bit in the query
|
||||||
pub enum Recurse {
|
pub fn checking_disabled(&mut self) -> &mut Self {
|
||||||
Yes,
|
self.cdflag = true;
|
||||||
No,
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Recurse {
|
fn cdflag(&self) -> &'static str {
|
||||||
fn as_str(&self) -> &'static str {
|
if self.cdflag {
|
||||||
match self {
|
"+cdflag"
|
||||||
Self::Yes => "+recurse",
|
} else {
|
||||||
Self::No => "+norecurse",
|
"+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)]
|
#[derive(Debug, Default, PartialEq)]
|
||||||
pub struct DigFlags {
|
pub struct DigFlags {
|
||||||
pub qr: bool,
|
|
||||||
pub recursion_desired: bool,
|
|
||||||
pub recursion_available: bool,
|
|
||||||
pub authoritative_answer: bool,
|
|
||||||
pub authenticated_data: 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 {
|
impl FromStr for DigFlags {
|
||||||
@ -200,6 +238,7 @@ impl FromStr for DigFlags {
|
|||||||
let mut recursion_available = false;
|
let mut recursion_available = false;
|
||||||
let mut authoritative_answer = false;
|
let mut authoritative_answer = false;
|
||||||
let mut authenticated_data = false;
|
let mut authenticated_data = false;
|
||||||
|
let mut checking_disabled = false;
|
||||||
|
|
||||||
for flag in input.split_whitespace() {
|
for flag in input.split_whitespace() {
|
||||||
match flag {
|
match flag {
|
||||||
@ -208,16 +247,18 @@ impl FromStr for DigFlags {
|
|||||||
"ra" => recursion_available = true,
|
"ra" => recursion_available = true,
|
||||||
"aa" => authoritative_answer = true,
|
"aa" => authoritative_answer = true,
|
||||||
"ad" => authenticated_data = true,
|
"ad" => authenticated_data = true,
|
||||||
|
"cd" => checking_disabled = true,
|
||||||
_ => return Err(format!("unknown flag: {flag}").into()),
|
_ => return Err(format!("unknown flag: {flag}").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
qr,
|
|
||||||
recursion_desired,
|
|
||||||
recursion_available,
|
|
||||||
authoritative_answer,
|
|
||||||
authenticated_data,
|
authenticated_data,
|
||||||
|
authoritative_answer,
|
||||||
|
checking_disabled,
|
||||||
|
qr,
|
||||||
|
recursion_available,
|
||||||
|
recursion_desired,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,13 @@ use std::borrow::Cow;
|
|||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct FQDN<'a> {
|
pub struct FQDN {
|
||||||
inner: Cow<'a, str>,
|
inner: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO likely needs further validation
|
// TODO likely needs further validation
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn FQDN<'a>(input: impl Into<Cow<'a, str>>) -> Result<FQDN<'a>> {
|
pub fn FQDN(input: impl Into<Cow<'static, str>>) -> Result<FQDN> {
|
||||||
let input = input.into();
|
let input = input.into();
|
||||||
if !input.ends_with('.') {
|
if !input.ends_with('.') {
|
||||||
return Err("FQDN must end with a `.`".into());
|
return Err("FQDN must end with a `.`".into());
|
||||||
@ -24,12 +24,12 @@ pub fn FQDN<'a>(input: impl Into<Cow<'a, str>>) -> Result<FQDN<'a>> {
|
|||||||
Ok(FQDN { inner: input })
|
Ok(FQDN { inner: input })
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FQDN<'a> {
|
impl FQDN {
|
||||||
pub const ROOT: FQDN<'static> = FQDN {
|
pub const ROOT: FQDN = FQDN {
|
||||||
inner: Cow::Borrowed("."),
|
inner: Cow::Borrowed("."),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const COM: FQDN<'static> = FQDN {
|
pub const COM: FQDN = FQDN {
|
||||||
inner: Cow::Borrowed("com."),
|
inner: Cow::Borrowed("com."),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ impl<'a> FQDN<'a> {
|
|||||||
&self.inner
|
&self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_owned(self) -> FQDN<'static> {
|
pub fn into_owned(self) -> FQDN {
|
||||||
let owned = match self.inner {
|
let owned = match self.inner {
|
||||||
Cow::Borrowed(borrowed) => borrowed.to_string(),
|
Cow::Borrowed(borrowed) => borrowed.to_string(),
|
||||||
Cow::Owned(owned) => owned,
|
Cow::Owned(owned) => owned,
|
||||||
@ -53,21 +53,21 @@ impl<'a> FQDN<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for FQDN<'static> {
|
impl FromStr for FQDN {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt::Display::fmt(self, f)
|
fmt::Display::fmt(self, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FQDN<'_> {
|
impl fmt::Display for FQDN {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(&self.inner)
|
f.write_str(&self.inner)
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,6 @@ pub use crate::fqdn::FQDN;
|
|||||||
pub use crate::resolver::Resolver;
|
pub use crate::resolver::Resolver;
|
||||||
pub use crate::trust_anchor::TrustAnchor;
|
pub use crate::trust_anchor::TrustAnchor;
|
||||||
|
|
||||||
pub type Error = Box<dyn std::error::Error>;
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
mod container;
|
mod container;
|
||||||
mod fqdn;
|
mod fqdn;
|
||||||
@ -23,6 +20,12 @@ mod trust_anchor;
|
|||||||
pub mod tshark;
|
pub mod tshark;
|
||||||
pub mod zone_file;
|
pub mod zone_file;
|
||||||
|
|
||||||
|
pub type Error = Box<dyn std::error::Error>;
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
// 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)]
|
#[derive(Clone)]
|
||||||
pub enum Implementation {
|
pub enum Implementation {
|
||||||
Unbound,
|
Unbound,
|
||||||
|
@ -2,17 +2,18 @@ use core::sync::atomic::{self, AtomicUsize};
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use crate::container::{Child, Container, Network};
|
use crate::container::{Child, Container, Network};
|
||||||
|
use crate::record::{self, Record, SoaSettings, DS, SOA};
|
||||||
use crate::tshark::Tshark;
|
use crate::tshark::Tshark;
|
||||||
use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS};
|
use crate::zone_file::{self, ZoneFile};
|
||||||
use crate::{Implementation, Result, FQDN};
|
use crate::{Implementation, Result, DEFAULT_TTL, FQDN};
|
||||||
|
|
||||||
pub struct NameServer<'a, State> {
|
pub struct NameServer<State> {
|
||||||
container: Container,
|
container: Container,
|
||||||
zone_file: ZoneFile<'a>,
|
zone_file: ZoneFile,
|
||||||
state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NameServer<'a, Stopped> {
|
impl NameServer<Stopped> {
|
||||||
/// Spins up a primary name server that has authority over the given `zone`
|
/// 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.
|
/// 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 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
|
/// - one NS record, with this name server's FQDN set as the only available name server for
|
||||||
/// the zone
|
/// the zone
|
||||||
pub fn new(implementation: Implementation, zone: FQDN<'a>, network: &Network) -> Result<Self> {
|
pub fn new(implementation: Implementation, zone: FQDN, network: &Network) -> Result<Self> {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(implementation, Implementation::Unbound),
|
matches!(implementation, Implementation::Unbound),
|
||||||
"currently only `unbound` (`nsd`) can be used as a `NameServer`"
|
"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 ns_count = ns_count();
|
||||||
let nameserver = primary_ns(ns_count);
|
let nameserver = primary_ns(ns_count);
|
||||||
|
|
||||||
let soa = zone_file::SOA {
|
let soa = SOA {
|
||||||
zone: zone.clone(),
|
zone: zone.clone(),
|
||||||
|
ttl: DEFAULT_TTL,
|
||||||
nameserver: nameserver.clone(),
|
nameserver: nameserver.clone(),
|
||||||
admin: admin_ns(ns_count),
|
admin: admin_ns(ns_count),
|
||||||
settings: SoaSettings::default(),
|
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_file.add(Record::ns(zone, nameserver.clone()));
|
||||||
zone,
|
|
||||||
nameserver: nameserver.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let image = implementation.into();
|
let image = implementation.into();
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@ -56,30 +55,19 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a NS + A record pair to the zone file
|
/// Adds a NS + A record pair to the zone file
|
||||||
pub fn referral(
|
pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self {
|
||||||
&mut self,
|
|
||||||
zone: FQDN<'a>,
|
|
||||||
nameserver: FQDN<'a>,
|
|
||||||
ipv4_addr: Ipv4Addr,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.zone_file.referral(zone, nameserver, ipv4_addr);
|
self.zone_file.referral(zone, nameserver, ipv4_addr);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an A record pair to the zone file
|
/// Adds a record to the name server's zone file
|
||||||
pub fn a(&mut self, fqdn: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self {
|
pub fn add(&mut self, record: impl Into<Record>) -> &mut Self {
|
||||||
self.zone_file.entry(zone_file::A { fqdn, ipv4_addr });
|
self.zone_file.add(record);
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a DS record to the zone file
|
|
||||||
pub fn ds(&mut self, ds: DS) -> &mut Self {
|
|
||||||
self.zone_file.entry(ds);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Freezes and signs the name server's zone file
|
/// Freezes and signs the name server's zone file
|
||||||
pub fn sign(self) -> Result<NameServer<'a, Signed>> {
|
pub fn sign(self) -> Result<NameServer<Signed>> {
|
||||||
// TODO do we want to make these settings configurable?
|
// TODO do we want to make these settings configurable?
|
||||||
const ZSK_BITS: usize = 1024;
|
const ZSK_BITS: usize = 1024;
|
||||||
const KSK_BITS: usize = 2048;
|
const KSK_BITS: usize = 2048;
|
||||||
@ -94,19 +82,19 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
|
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
|
||||||
container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?;
|
container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?;
|
||||||
|
|
||||||
let zone = &zone_file.origin;
|
let zone = zone_file.origin();
|
||||||
|
|
||||||
let zsk_keygen =
|
let zsk_keygen =
|
||||||
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
|
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
|
||||||
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
|
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
|
||||||
let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key");
|
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 =
|
let ksk_keygen =
|
||||||
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
|
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
|
||||||
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
|
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
|
||||||
let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key");
|
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
|
// -n = use NSEC3 instead of NSEC
|
||||||
// -p = set the opt-out flag on all nsec3 rrs
|
// -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 key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed");
|
||||||
let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?;
|
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();
|
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 {
|
Ok(NameServer {
|
||||||
container,
|
container,
|
||||||
zone_file,
|
zone_file,
|
||||||
state: Signed {
|
state: Signed {
|
||||||
ds,
|
ds,
|
||||||
ksk,
|
signed,
|
||||||
signed_zone_file,
|
// inherit SOA's TTL value
|
||||||
zsk,
|
ksk: ksk.with_ttl(ttl),
|
||||||
|
zsk: zsk.with_ttl(ttl),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves the server to the "Start" state where it can answer client queries
|
/// Moves the server to the "Start" state where it can answer client queries
|
||||||
pub fn start(self) -> Result<NameServer<'a, Running>> {
|
pub fn start(self) -> Result<NameServer<Running>> {
|
||||||
let Self {
|
let Self {
|
||||||
container,
|
container,
|
||||||
zone_file,
|
zone_file,
|
||||||
@ -149,7 +139,7 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
// for PID file
|
// for PID file
|
||||||
container.status_ok(&["mkdir", "-p", "/run/nsd/"])?;
|
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.status_ok(&["mkdir", "-p", ZONES_DIR])?;
|
||||||
container.cp(&zone_file_path(), &zone_file.to_string())?;
|
container.cp(&zone_file_path(), &zone_file.to_string())?;
|
||||||
@ -176,19 +166,21 @@ fn ns_count() -> usize {
|
|||||||
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NameServer<'a, Signed> {
|
impl NameServer<Signed> {
|
||||||
/// Moves the server to the "Start" state where it can answer client queries
|
/// Moves the server to the "Start" state where it can answer client queries
|
||||||
pub fn start(self) -> Result<NameServer<'a, Running>> {
|
pub fn start(self) -> Result<NameServer<Running>> {
|
||||||
let Self {
|
let Self {
|
||||||
container,
|
container,
|
||||||
zone_file,
|
zone_file,
|
||||||
state: _,
|
state,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// for PID file
|
// for PID file
|
||||||
container.status_ok(&["mkdir", "-p", "/run/nsd/"])?;
|
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"])?;
|
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
|
&self.state.ksk
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zone_signing_key(&self) -> &DNSKEY {
|
pub fn zone_signing_key(&self) -> &record::DNSKEY {
|
||||||
&self.state.zsk
|
&self.state.zsk
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn signed_zone_file(&self) -> &str {
|
pub fn signed_zone_file(&self) -> &ZoneFile {
|
||||||
&self.state.signed_zone_file
|
&self.state.signed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signed_zone_file_mut(&mut self) -> &mut ZoneFile {
|
||||||
|
&mut self.state.signed
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ds(&self) -> &DS {
|
pub fn ds(&self) -> &DS {
|
||||||
@ -216,7 +212,7 @@ impl<'a> NameServer<'a, Signed> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NameServer<'a, Running> {
|
impl NameServer<Running> {
|
||||||
/// Starts a `tshark` instance that captures DNS messages flowing through this network node
|
/// Starts a `tshark` instance that captures DNS messages flowing through this network node
|
||||||
pub fn eavesdrop(&self) -> Result<Tshark> {
|
pub fn eavesdrop(&self) -> Result<Tshark> {
|
||||||
self.container.eavesdrop()
|
self.container.eavesdrop()
|
||||||
@ -246,7 +242,7 @@ kill -TERM $(cat {pidfile})"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S> NameServer<'a, S> {
|
impl<S> NameServer<S> {
|
||||||
pub fn container_id(&self) -> &str {
|
pub fn container_id(&self) -> &str {
|
||||||
self.container.id()
|
self.container.id()
|
||||||
}
|
}
|
||||||
@ -256,15 +252,15 @@ impl<'a, S> NameServer<'a, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Zone file BEFORE signing
|
/// Zone file BEFORE signing
|
||||||
pub fn zone_file(&self) -> &ZoneFile<'a> {
|
pub fn zone_file(&self) -> &ZoneFile {
|
||||||
&self.zone_file
|
&self.zone_file
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zone(&self) -> &FQDN<'a> {
|
pub fn zone(&self) -> &FQDN {
|
||||||
&self.zone_file.origin
|
self.zone_file.origin()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fqdn(&self) -> &FQDN<'a> {
|
pub fn fqdn(&self) -> &FQDN {
|
||||||
&self.zone_file.soa.nameserver
|
&self.zone_file.soa.nameserver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,20 +269,20 @@ pub struct Stopped;
|
|||||||
|
|
||||||
pub struct Signed {
|
pub struct Signed {
|
||||||
ds: DS,
|
ds: DS,
|
||||||
zsk: DNSKEY,
|
zsk: record::DNSKEY,
|
||||||
ksk: DNSKEY,
|
ksk: record::DNSKEY,
|
||||||
signed_zone_file: String,
|
signed: ZoneFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Running {
|
pub struct Running {
|
||||||
child: Child,
|
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()
|
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()
|
FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +295,7 @@ fn nsd_conf(fqdn: &FQDN) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::client::{Client, Dnssec, Recurse};
|
use crate::client::{Client, DigSettings};
|
||||||
use crate::record::RecordType;
|
use crate::record::RecordType;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -311,13 +307,7 @@ mod tests {
|
|||||||
let ip_addr = tld_ns.ipv4_addr();
|
let ip_addr = tld_ns.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new(&network)?;
|
let client = Client::new(&network)?;
|
||||||
let output = client.dig(
|
let output = client.dig(DigSettings::default(), ip_addr, RecordType::SOA, &FQDN::COM)?;
|
||||||
Recurse::No,
|
|
||||||
Dnssec::No,
|
|
||||||
ip_addr,
|
|
||||||
RecordType::SOA,
|
|
||||||
&FQDN::COM,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
@ -342,8 +332,7 @@ mod tests {
|
|||||||
|
|
||||||
let client = Client::new(&network)?;
|
let client = Client::new(&network)?;
|
||||||
let output = client.dig(
|
let output = client.dig(
|
||||||
Recurse::No,
|
DigSettings::default(),
|
||||||
Dnssec::No,
|
|
||||||
ipv4_addr,
|
ipv4_addr,
|
||||||
RecordType::NS,
|
RecordType::NS,
|
||||||
&FQDN::COM,
|
&FQDN::COM,
|
||||||
@ -368,13 +357,8 @@ mod tests {
|
|||||||
let ns_addr = tld_ns.ipv4_addr();
|
let ns_addr = tld_ns.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new(&network)?;
|
let client = Client::new(&network)?;
|
||||||
let output = client.dig(
|
let settings = *DigSettings::default().dnssec();
|
||||||
Recurse::No,
|
let output = client.dig(settings, ns_addr, RecordType::SOA, &FQDN::ROOT)?;
|
||||||
Dnssec::Yes,
|
|
||||||
ns_addr,
|
|
||||||
RecordType::SOA,
|
|
||||||
&FQDN::ROOT,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use crate::zone_file::DNSKEY;
|
use crate::record::DNSKEY;
|
||||||
|
|
||||||
pub struct TrustAnchor {
|
pub struct TrustAnchor {
|
||||||
keys: Vec<DNSKEY>,
|
keys: Vec<DNSKEY>,
|
||||||
|
@ -244,9 +244,9 @@ struct Ip {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::client::{Client, Dnssec, Recurse};
|
use crate::client::{Client, DigSettings};
|
||||||
use crate::name_server::NameServer;
|
use crate::name_server::NameServer;
|
||||||
use crate::record::RecordType;
|
use crate::record::{Record, RecordType};
|
||||||
use crate::zone_file::Root;
|
use crate::zone_file::Root;
|
||||||
use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN};
|
use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN};
|
||||||
|
|
||||||
@ -260,8 +260,7 @@ mod tests {
|
|||||||
|
|
||||||
let client = Client::new(network)?;
|
let client = Client::new(network)?;
|
||||||
let resp = client.dig(
|
let resp = client.dig(
|
||||||
Recurse::No,
|
DigSettings::default(),
|
||||||
Dnssec::No,
|
|
||||||
ns.ipv4_addr(),
|
ns.ipv4_addr(),
|
||||||
RecordType::SOA,
|
RecordType::SOA,
|
||||||
&FQDN::ROOT,
|
&FQDN::ROOT,
|
||||||
@ -297,8 +296,8 @@ mod tests {
|
|||||||
let mut nameservers_ns =
|
let mut nameservers_ns =
|
||||||
NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?;
|
NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?;
|
||||||
nameservers_ns
|
nameservers_ns
|
||||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
|
||||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
|
||||||
let nameservers_ns = nameservers_ns.start()?;
|
let nameservers_ns = nameservers_ns.start()?;
|
||||||
|
|
||||||
com_ns.referral(
|
com_ns.referral(
|
||||||
@ -322,13 +321,8 @@ mod tests {
|
|||||||
let resolver_addr = resolver.ipv4_addr();
|
let resolver_addr = resolver.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new(network)?;
|
let client = Client::new(network)?;
|
||||||
let output = client.dig(
|
let settings = *DigSettings::default().recurse();
|
||||||
Recurse::Yes,
|
let output = client.dig(settings, dbg!(resolver_addr), RecordType::A, root_ns.fqdn())?;
|
||||||
Dnssec::No,
|
|
||||||
dbg!(resolver_addr),
|
|
||||||
RecordType::A,
|
|
||||||
root_ns.fqdn(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
|
@ -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<Entry<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Entry<'a>>) {
|
|
||||||
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<DS> for Entry<'a> {
|
|
||||||
fn from(v: DS) -> Self {
|
|
||||||
Self::DS(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<A<'a>> for Entry<'a> {
|
|
||||||
fn from(v: A<'a>) -> Self {
|
|
||||||
Self::A(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<NS<'a>> 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<Self, Self::Err> {
|
|
||||||
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<Self, Self::Err> {
|
|
||||||
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<A<'static>> {
|
|
||||||
Ok(A {
|
|
||||||
fqdn: FQDN("e.gtld-servers.net.")?,
|
|
||||||
ipv4_addr: Ipv4Addr::new(192, 12, 94, 30),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn example_ns() -> Result<NS<'static>> {
|
|
||||||
Ok(NS {
|
|
||||||
zone: FQDN::COM,
|
|
||||||
nameserver: FQDN("e.gtld-servers.net.")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn example_soa() -> Result<SOA<'static>> {
|
|
||||||
Ok(SOA {
|
|
||||||
zone: FQDN::ROOT,
|
|
||||||
nameserver: FQDN("a.root-servers.net.")?,
|
|
||||||
admin: FQDN("nstld.verisign-grs.com.")?,
|
|
||||||
settings: SoaSettings::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
225
packages/dns-test/src/zone_file/mod.rs
Normal file
225
packages/dns-test/src/zone_file/mod.rs
Normal file
@ -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<Record>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Record>) {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
// 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(())
|
||||||
|
}
|
||||||
|
}
|
11
packages/dns-test/src/zone_file/muster.zone
Normal file
11
packages/dns-test/src/zone_file/muster.zone
Normal file
@ -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=
|
Loading…
Reference in New Issue
Block a user