Merge pull request #50 from ferrous-systems/ja-ede-support-take-2
add support for Extended DNS Error (EDE)
This commit is contained in:
commit
4ce9ec9937
|
@ -1,9 +1,8 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, DigSettings};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::name_server::{Graph, NameServer, Sign};
|
||||
use dns_test::record::{Record, RecordType};
|
||||
use dns_test::zone_file::Root;
|
||||
use dns_test::{Network, Resolver, Result, FQDN};
|
||||
|
||||
#[test]
|
||||
|
@ -13,37 +12,17 @@ fn can_resolve() -> Result<()> {
|
|||
|
||||
let network = Network::new()?;
|
||||
let peer = dns_test::peer();
|
||||
let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?;
|
||||
let mut com_ns = NameServer::new(&peer, 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.start()?;
|
||||
let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?;
|
||||
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
let Graph {
|
||||
nameservers: _nameservers,
|
||||
root,
|
||||
..
|
||||
} = Graph::build(leaf_ns, Sign::No)?;
|
||||
|
||||
com_ns.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
);
|
||||
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());
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let resolver = Resolver::new(
|
||||
&network,
|
||||
Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()),
|
||||
)
|
||||
.start(&dns_test::subject())?;
|
||||
let resolver = Resolver::new(&network, root).start(&dns_test::subject())?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
|
@ -69,30 +48,16 @@ fn nxdomain() -> Result<()> {
|
|||
|
||||
let network = Network::new()?;
|
||||
let peer = dns_test::peer();
|
||||
let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?;
|
||||
let mut com_ns = NameServer::new(&peer, 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()));
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
let leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?;
|
||||
|
||||
com_ns.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
);
|
||||
let com_ns = com_ns.start()?;
|
||||
let Graph {
|
||||
nameservers: _nameservers,
|
||||
root,
|
||||
..
|
||||
} = Graph::build(leaf_ns, Sign::No)?;
|
||||
|
||||
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
let resolver = Resolver::new(
|
||||
&network,
|
||||
Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()),
|
||||
)
|
||||
.start(&dns_test::subject())?;
|
||||
let resolver = Resolver::new(&network, root).start(&dns_test::subject())?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
mod bogus;
|
||||
mod ede;
|
||||
mod secure;
|
||||
|
|
|
@ -2,9 +2,8 @@ use std::net::Ipv4Addr;
|
|||
|
||||
use base64::prelude::*;
|
||||
use dns_test::client::{Client, DigSettings};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::name_server::{Graph, NameServer, Sign};
|
||||
use dns_test::record::{Record, RecordType};
|
||||
use dns_test::zone_file::Root;
|
||||
use dns_test::{Network, Resolver, Result, FQDN};
|
||||
|
||||
#[ignore]
|
||||
|
@ -15,62 +14,41 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> {
|
|||
|
||||
let network = Network::new()?;
|
||||
let peer = dns_test::peer();
|
||||
let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?;
|
||||
let mut com_ns = NameServer::new(&peer, 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()?;
|
||||
let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?;
|
||||
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
|
||||
|
||||
// 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;
|
||||
let Graph {
|
||||
nameservers: _nameservers,
|
||||
root,
|
||||
trust_anchor,
|
||||
} = Graph::build(
|
||||
leaf_ns,
|
||||
Sign::AndAmend(&|zone, records| {
|
||||
if zone == &FQDN::NAMESERVERS {
|
||||
let mut modified = 0;
|
||||
for record in records {
|
||||
if let Record::RRSIG(rrsig) = record {
|
||||
if rrsig.fqdn == needle_fqdn {
|
||||
let mut signature = BASE64_STANDARD.decode(&rrsig.signature).unwrap();
|
||||
let last = signature.last_mut().expect("empty signature");
|
||||
*last = !*last;
|
||||
|
||||
rrsig.signature = BASE64_STANDARD.encode(&signature);
|
||||
modified += 1;
|
||||
rrsig.signature = BASE64_STANDARD.encode(&signature);
|
||||
modified += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(modified, 1, "sanity check");
|
||||
}
|
||||
}
|
||||
}
|
||||
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 resolver = Resolver::new(
|
||||
&network,
|
||||
Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()),
|
||||
)
|
||||
.trust_anchor_key(root_ksk)
|
||||
.trust_anchor_key(root_zsk)
|
||||
.start(&dns_test::subject())?;
|
||||
let trust_anchor = &trust_anchor.unwrap();
|
||||
let resolver = Resolver::new(&network, root)
|
||||
.trust_anchor(trust_anchor)
|
||||
.start(&dns_test::subject())?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
|
|
171
packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs
Normal file
171
packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, DigSettings, ExtendedDnsError};
|
||||
use dns_test::name_server::{Graph, NameServer, Sign};
|
||||
use dns_test::record::{Record, RecordType};
|
||||
use dns_test::{Network, Resolver, Result, FQDN};
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn dnskey_missing() -> Result<()> {
|
||||
fixture(
|
||||
ExtendedDnsError::DnskeyMissing,
|
||||
|_needle_fqdn, zone, records| {
|
||||
if zone == &FQDN::NAMESERVERS {
|
||||
// remove the DNSKEY record that contains the ZSK
|
||||
let mut remove_count = 0;
|
||||
*records = records
|
||||
.drain(..)
|
||||
.filter(|record| {
|
||||
let remove = if let Record::DNSKEY(dnskey) = record {
|
||||
dnskey.is_zone_signing_key()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if remove {
|
||||
remove_count += 1;
|
||||
}
|
||||
|
||||
!remove
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(1, remove_count, "sanity check");
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn rrsigs_missing() -> Result<()> {
|
||||
fixture(
|
||||
ExtendedDnsError::RrsigsMissing,
|
||||
|needle_fqdn, zone, records| {
|
||||
if zone == &FQDN::NAMESERVERS {
|
||||
// remove the RRSIG records that covers the needle record
|
||||
let mut remove_count = 0;
|
||||
*records = records
|
||||
.drain(..)
|
||||
.filter(|record| {
|
||||
let remove = if let Record::RRSIG(rrsig) = record {
|
||||
rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if remove {
|
||||
remove_count += 1;
|
||||
}
|
||||
|
||||
!remove
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(1, remove_count, "sanity check");
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn unsupported_dnskey_algorithm() -> Result<()> {
|
||||
fixture(
|
||||
ExtendedDnsError::UnsupportedDnskeyAlgorithm,
|
||||
|needle_fqdn, zone, records| {
|
||||
if zone == &FQDN::NAMESERVERS {
|
||||
// lie about the algorithm that was used to sign the needle record
|
||||
let mut modified_count = 0;
|
||||
for record in records {
|
||||
if let Record::RRSIG(rrsig) = record {
|
||||
if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn {
|
||||
assert_ne!(1, rrsig.algorithm, "modify the value below");
|
||||
rrsig.algorithm = 1;
|
||||
modified_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(1, modified_count, "sanity check");
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn dnssec_bogus() -> Result<()> {
|
||||
fixture(
|
||||
ExtendedDnsError::DnssecBogus,
|
||||
|needle_fqdn, zone, records| {
|
||||
if zone == &FQDN::NAMESERVERS {
|
||||
// corrupt the RRSIG record that covers the needle record
|
||||
let mut modified_count = 0;
|
||||
for record in records {
|
||||
if let Record::RRSIG(rrsig) = record {
|
||||
if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn {
|
||||
rrsig.signature_expiration = rrsig.signature_inception - 1;
|
||||
modified_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(1, modified_count, "sanity check");
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Sets up a minimal, DNSSEC-enabled DNS graph where the leaf zone contains a "needle" A record
|
||||
// that we'll search for
|
||||
//
|
||||
// `amend` can be used to modify zone files *after* they have been signed. it's used to introduce
|
||||
// errors in the signed zone files
|
||||
//
|
||||
// the query for the needle record is expected to fail with the `expected` Extended DNS Error
|
||||
fn fixture(
|
||||
expected: ExtendedDnsError,
|
||||
amend: fn(needle_fqdn: &FQDN, zone: &FQDN, records: &mut Vec<Record>),
|
||||
) -> Result<()> {
|
||||
let subject = dns_test::subject();
|
||||
let supports_ede = subject.supports_ede();
|
||||
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle_fqdn = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let network = Network::new()?;
|
||||
let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?;
|
||||
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
|
||||
|
||||
let Graph {
|
||||
nameservers: _nameservers,
|
||||
root,
|
||||
trust_anchor,
|
||||
} = Graph::build(
|
||||
leaf_ns,
|
||||
Sign::AndAmend(&|zone, records| {
|
||||
amend(&needle_fqdn, zone, records);
|
||||
}),
|
||||
)?;
|
||||
|
||||
let mut resolver = Resolver::new(&network, root);
|
||||
|
||||
if supports_ede {
|
||||
resolver.extended_dns_errors();
|
||||
}
|
||||
|
||||
let trust_anchor = &trust_anchor.unwrap();
|
||||
let resolver = resolver.trust_anchor(trust_anchor).start(&subject)?;
|
||||
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_servfail());
|
||||
|
||||
if supports_ede {
|
||||
assert_eq!(Some(expected), output.ede);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, DigSettings};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::name_server::{Graph, NameServer, Sign};
|
||||
use dns_test::record::{Record, RecordType};
|
||||
use dns_test::zone_file::Root;
|
||||
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
|
||||
|
@ -51,53 +51,20 @@ fn can_validate_with_delegation() -> Result<()> {
|
|||
|
||||
let peer = dns_test::peer();
|
||||
let network = Network::new()?;
|
||||
let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?;
|
||||
let mut com_ns = NameServer::new(&peer, 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()?;
|
||||
let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?;
|
||||
leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
let Graph {
|
||||
nameservers: _nameservers,
|
||||
root,
|
||||
trust_anchor,
|
||||
} = Graph::build(leaf_ns, Sign::Yes)?;
|
||||
|
||||
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 trust_anchor = &TrustAnchor::from_iter([root_ksk, root_zsk]);
|
||||
let resolver = Resolver::new(
|
||||
&network,
|
||||
Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()),
|
||||
)
|
||||
.trust_anchor(trust_anchor)
|
||||
.start(&dns_test::subject())?;
|
||||
let trust_anchor = &trust_anchor.unwrap();
|
||||
let resolver = Resolver::new(&network, root)
|
||||
.trust_anchor(trust_anchor)
|
||||
.start(&dns_test::subject())?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new(&network)?;
|
||||
|
|
|
@ -141,6 +141,7 @@ impl DigSettings {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct DigOutput {
|
||||
pub ede: Option<ExtendedDnsError>,
|
||||
pub flags: DigFlags,
|
||||
pub status: DigStatus,
|
||||
pub answer: Vec<Record>,
|
||||
|
@ -154,6 +155,7 @@ impl FromStr for DigOutput {
|
|||
fn from_str(input: &str) -> Result<Self> {
|
||||
const FLAGS_PREFIX: &str = ";; flags: ";
|
||||
const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: ";
|
||||
const EDE_PREFIX: &str = "; EDE: ";
|
||||
const ANSWER_HEADER: &str = ";; ANSWER SECTION:";
|
||||
const AUTHORITY_HEADER: &str = ";; AUTHORITY SECTION:";
|
||||
|
||||
|
@ -173,6 +175,7 @@ impl FromStr for DigOutput {
|
|||
let mut status = None;
|
||||
let mut answer = None;
|
||||
let mut authority = None;
|
||||
let mut ede = None;
|
||||
|
||||
let mut lines = input.lines();
|
||||
while let Some(line) = lines.next() {
|
||||
|
@ -196,6 +199,17 @@ impl FromStr for DigOutput {
|
|||
}
|
||||
|
||||
status = Some(status_text.parse()?);
|
||||
} else if let Some(unprefixed) = line.strip_prefix(EDE_PREFIX) {
|
||||
let code = unprefixed
|
||||
.split_once(' ')
|
||||
.map(|(code, _rest)| code)
|
||||
.unwrap_or(unprefixed);
|
||||
|
||||
if ede.is_some() {
|
||||
return Err(more_than_once(EDE_PREFIX).into());
|
||||
}
|
||||
|
||||
ede = Some(code.parse()?);
|
||||
} else if line.starts_with(ANSWER_HEADER) {
|
||||
if answer.is_some() {
|
||||
return Err(more_than_once(ANSWER_HEADER).into());
|
||||
|
@ -230,14 +244,41 @@ impl FromStr for DigOutput {
|
|||
}
|
||||
|
||||
Ok(Self {
|
||||
flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?,
|
||||
status: status.ok_or_else(|| not_found(STATUS_PREFIX))?,
|
||||
answer: answer.unwrap_or_default(),
|
||||
authority: authority.unwrap_or_default(),
|
||||
ede,
|
||||
flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?,
|
||||
status: status.ok_or_else(|| not_found(STATUS_PREFIX))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ExtendedDnsError {
|
||||
DnskeyMissing,
|
||||
DnssecBogus,
|
||||
RrsigsMissing,
|
||||
UnsupportedDnskeyAlgorithm,
|
||||
}
|
||||
|
||||
impl FromStr for ExtendedDnsError {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> std::prelude::v1::Result<Self, Self::Err> {
|
||||
let code: u16 = input.parse()?;
|
||||
|
||||
let code = match code {
|
||||
1 => Self::UnsupportedDnskeyAlgorithm,
|
||||
6 => Self::DnssecBogus,
|
||||
9 => Self::DnskeyMissing,
|
||||
10 => Self::RrsigsMissing,
|
||||
_ => todo!("EDE {code} has not yet been implemented"),
|
||||
};
|
||||
|
||||
Ok(code)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct DigFlags {
|
||||
pub authenticated_data: bool,
|
||||
|
@ -398,4 +439,32 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ede() -> Result<()> {
|
||||
let input = "; <<>> DiG 9.18.24-1-Debian <<>> +recurse +nodnssec +adflag +nocdflag @192.168.176.5 A example.nameservers.com.
|
||||
; (1 server found)
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 49801
|
||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
; EDE: 9 (DNSKEY Missing)
|
||||
;; QUESTION SECTION:
|
||||
;example.nameservers.com. IN A
|
||||
|
||||
;; Query time: 26 msec
|
||||
;; SERVER: 192.168.176.5#53(192.168.176.5) (UDP)
|
||||
;; WHEN: Tue Mar 05 17:45:29 UTC 2024
|
||||
;; MSG SIZE rcvd: 58
|
||||
";
|
||||
|
||||
let output: DigOutput = input.parse()?;
|
||||
|
||||
assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ impl Container {
|
|||
id,
|
||||
name,
|
||||
ipv4_addr,
|
||||
_network: network.clone(),
|
||||
network: network.clone(),
|
||||
};
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
|
@ -250,6 +250,10 @@ impl Container {
|
|||
pub fn id(&self) -> &str {
|
||||
&self.inner.id
|
||||
}
|
||||
|
||||
pub(crate) fn network(&self) -> &Network {
|
||||
&self.inner.network
|
||||
}
|
||||
}
|
||||
|
||||
fn verbose_docker_build() -> bool {
|
||||
|
@ -282,7 +286,7 @@ struct Inner {
|
|||
id: String,
|
||||
// TODO probably also want the IPv6 address
|
||||
ipv4_addr: Ipv4Addr,
|
||||
_network: Network,
|
||||
network: Network,
|
||||
}
|
||||
|
||||
/// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the
|
||||
|
|
|
@ -33,6 +33,10 @@ impl FQDN {
|
|||
inner: Cow::Borrowed("com."),
|
||||
};
|
||||
|
||||
pub const NAMESERVERS: FQDN = FQDN {
|
||||
inner: Cow::Borrowed("nameservers.com."),
|
||||
};
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.inner == "."
|
||||
}
|
||||
|
@ -51,6 +55,28 @@ impl FQDN {
|
|||
inner: Cow::Owned(owned),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<FQDN> {
|
||||
let (fragment, parent) = self.inner.split_once('.').unwrap();
|
||||
|
||||
if fragment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let parent = if parent.is_empty() {
|
||||
FQDN::ROOT
|
||||
} else {
|
||||
FQDN(parent.to_string()).unwrap()
|
||||
};
|
||||
Some(parent)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_labels(&self) -> usize {
|
||||
self.inner
|
||||
.split('.')
|
||||
.filter(|label| !label.is_empty())
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FQDN {
|
||||
|
@ -72,3 +98,37 @@ impl fmt::Display for FQDN {
|
|||
f.write_str(&self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parent() -> Result<()> {
|
||||
let mut fqdn = FQDN("example.nameservers.com.")?;
|
||||
assert_eq!(3, fqdn.num_labels());
|
||||
|
||||
let parent = fqdn.parent();
|
||||
assert_eq!(
|
||||
Some("nameservers.com."),
|
||||
parent.as_ref().map(|fqdn| fqdn.as_str())
|
||||
);
|
||||
fqdn = parent.unwrap();
|
||||
assert_eq!(2, fqdn.num_labels());
|
||||
|
||||
let parent = fqdn.parent();
|
||||
assert_eq!(Some(FQDN::COM), parent);
|
||||
fqdn = parent.unwrap();
|
||||
assert_eq!(1, fqdn.num_labels());
|
||||
|
||||
let parent = fqdn.parent();
|
||||
assert_eq!(Some(FQDN::ROOT), parent);
|
||||
fqdn = parent.unwrap();
|
||||
assert_eq!(0, fqdn.num_labels());
|
||||
|
||||
let parent = fqdn.parent();
|
||||
assert!(parent.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,15 @@ use crate::FQDN;
|
|||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Config<'a> {
|
||||
NameServer { origin: &'a FQDN },
|
||||
Resolver { use_dnssec: bool, netmask: &'a str },
|
||||
NameServer {
|
||||
origin: &'a FQDN,
|
||||
},
|
||||
Resolver {
|
||||
use_dnssec: bool,
|
||||
netmask: &'a str,
|
||||
/// Extended DNS error (RFC8914)
|
||||
ede: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Config<'_> {
|
||||
|
@ -42,6 +49,14 @@ pub enum Implementation {
|
|||
}
|
||||
|
||||
impl Implementation {
|
||||
pub fn supports_ede(&self) -> bool {
|
||||
match self {
|
||||
Implementation::Bind => false,
|
||||
Implementation::Hickory(_) => true,
|
||||
Implementation::Unbound => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_bind(&self) -> bool {
|
||||
matches!(self, Self::Bind)
|
||||
|
@ -52,8 +67,11 @@ impl Implementation {
|
|||
Config::Resolver {
|
||||
use_dnssec,
|
||||
netmask,
|
||||
ede,
|
||||
} => match self {
|
||||
Self::Bind => {
|
||||
assert!(!ede, "the BIND resolver does not support EDE (RFC8914)");
|
||||
|
||||
minijinja::render!(
|
||||
include_str!("templates/named.resolver.conf.jinja"),
|
||||
use_dnssec => use_dnssec,
|
||||
|
@ -62,6 +80,7 @@ impl Implementation {
|
|||
}
|
||||
|
||||
Self::Hickory(_) => {
|
||||
// TODO enable EDE in Hickory when supported
|
||||
minijinja::render!(
|
||||
include_str!("templates/hickory.resolver.toml.jinja"),
|
||||
use_dnssec => use_dnssec,
|
||||
|
@ -73,6 +92,7 @@ impl Implementation {
|
|||
include_str!("templates/unbound.conf.jinja"),
|
||||
use_dnssec => use_dnssec,
|
||||
netmask => netmask,
|
||||
ede => ede,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,8 +5,135 @@ use crate::container::{Child, Container, Network};
|
|||
use crate::implementation::{Config, Role};
|
||||
use crate::record::{self, Record, SoaSettings, DS, SOA};
|
||||
use crate::tshark::Tshark;
|
||||
use crate::zone_file::{self, ZoneFile};
|
||||
use crate::{Implementation, Result, DEFAULT_TTL, FQDN};
|
||||
use crate::zone_file::{self, Root, ZoneFile};
|
||||
use crate::{Implementation, Result, TrustAnchor, DEFAULT_TTL, FQDN};
|
||||
|
||||
pub struct Graph {
|
||||
pub nameservers: Vec<NameServer<Running>>,
|
||||
pub root: Root,
|
||||
pub trust_anchor: Option<TrustAnchor>,
|
||||
}
|
||||
|
||||
/// Whether to sign the zone files
|
||||
pub enum Sign<'a> {
|
||||
No,
|
||||
Yes,
|
||||
/// Signs the zone files and then modifies the records produced by the signing process
|
||||
// XXX if captures are needed use `&dyn Fn(..)` instead of a function pointer
|
||||
AndAmend(&'a dyn Fn(&FQDN, &mut Vec<Record>)),
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
/// Builds up a minimal DNS graph from `leaf` up to a root name server and returns all the
|
||||
/// name servers in the graph
|
||||
///
|
||||
/// All new name servers will share the `Implementation` of `leaf`.
|
||||
///
|
||||
/// The returned name servers are sorted from leaf zone to root zone.
|
||||
///
|
||||
/// both `Sign::Yes` and `Sign::AndAmend` will add a DS record with the hash of the child's
|
||||
/// key to the parent's zone file
|
||||
///
|
||||
/// a non-empty `TrustAnchor` is returned only when `Sign::Yes` or `Sign::AndAmend` is used
|
||||
pub fn build(leaf: NameServer<Stopped>, sign: Sign) -> Result<Self> {
|
||||
// TODO if `leaf` is not authoritative over `nameservers.com.`, we would need two "lines" to
|
||||
// root. for example, if `leaf` is authoritative over `example.net.` we would need these two
|
||||
// lines:
|
||||
// - `nameservers.com.`, `com.`, `.` to cover the `primaryNNN.nameservers.com.` domains that
|
||||
// `NameServer` implicitly uses
|
||||
// - `example.net.`, `net.`, `.` to cover the requested `leaf` name server
|
||||
assert_eq!(&FQDN::NAMESERVERS, leaf.zone(), "not yet implemented");
|
||||
|
||||
// first pass: create nameservers for parent zones
|
||||
let mut zone = leaf.zone().clone();
|
||||
let mut nameservers = vec![leaf];
|
||||
while let Some(parent) = zone.parent() {
|
||||
let leaf = &mut nameservers[0];
|
||||
let nameserver = NameServer::new(
|
||||
&leaf.implementation,
|
||||
parent.clone(),
|
||||
leaf.container.network(),
|
||||
)?;
|
||||
|
||||
leaf.add(Record::a(nameserver.fqdn().clone(), nameserver.ipv4_addr()));
|
||||
nameservers.push(nameserver);
|
||||
|
||||
zone = parent;
|
||||
}
|
||||
|
||||
// XXX will not hold when `leaf` is not authoritative over `nameservers.com.`
|
||||
assert_eq!(3, nameservers.len());
|
||||
|
||||
// second pass: add referrals from parent to child
|
||||
// `windows_mut` is not a thing in `core::iter` so use indexing as a workaround
|
||||
for index in 0..nameservers.len() - 1 {
|
||||
let [child, parent] = &mut nameservers[index..][..2] else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
parent.referral(
|
||||
child.zone().clone(),
|
||||
child.fqdn().clone(),
|
||||
child.ipv4_addr(),
|
||||
);
|
||||
}
|
||||
|
||||
let root = nameservers.last().unwrap();
|
||||
let root = Root::new(root.fqdn().clone(), root.ipv4_addr());
|
||||
|
||||
// start name servers
|
||||
let (nameservers, trust_anchor) = match sign {
|
||||
Sign::No => (
|
||||
nameservers
|
||||
.into_iter()
|
||||
.map(|nameserver| nameserver.start())
|
||||
.collect::<Result<_>>()?,
|
||||
None,
|
||||
),
|
||||
|
||||
_ => {
|
||||
let mut trust_anchor = TrustAnchor::empty();
|
||||
let maybe_mutate = match sign {
|
||||
Sign::No => unreachable!(),
|
||||
Sign::Yes => None,
|
||||
Sign::AndAmend(f) => Some(f),
|
||||
};
|
||||
|
||||
let mut running = vec![];
|
||||
let mut child_ds = None;
|
||||
let len = nameservers.len();
|
||||
for (index, mut nameserver) in nameservers.into_iter().enumerate() {
|
||||
if let Some(ds) = child_ds.take() {
|
||||
nameserver.add(ds);
|
||||
}
|
||||
|
||||
let mut nameserver = nameserver.sign()?;
|
||||
child_ds = Some(nameserver.ds().clone());
|
||||
if let Some(mutate) = maybe_mutate {
|
||||
let zone = nameserver.zone().clone();
|
||||
mutate(&zone, &mut nameserver.signed_zone_file_mut().records);
|
||||
}
|
||||
|
||||
if index == len - 1 {
|
||||
// the last nameserver covers `.`
|
||||
trust_anchor.add(nameserver.key_signing_key().clone());
|
||||
trust_anchor.add(nameserver.zone_signing_key().clone());
|
||||
}
|
||||
|
||||
running.push(nameserver.start()?);
|
||||
}
|
||||
|
||||
(running, Some(trust_anchor))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Graph {
|
||||
nameservers,
|
||||
root,
|
||||
trust_anchor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NameServer<State> {
|
||||
container: Container,
|
||||
|
|
|
@ -239,6 +239,8 @@ pub struct DNSKEY {
|
|||
}
|
||||
|
||||
impl DNSKEY {
|
||||
const KSK_BIT: u16 = 1;
|
||||
|
||||
/// formats the `DNSKEY` in the format `delv` expects
|
||||
pub(super) fn delv(&self) -> String {
|
||||
let Self {
|
||||
|
@ -252,6 +254,19 @@ impl DNSKEY {
|
|||
|
||||
format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n")
|
||||
}
|
||||
|
||||
pub fn clear_key_signing_key_bit(&mut self) {
|
||||
self.flags &= !Self::KSK_BIT;
|
||||
}
|
||||
|
||||
pub fn is_key_signing_key(&self) -> bool {
|
||||
let mask = Self::KSK_BIT;
|
||||
self.flags & mask == mask
|
||||
}
|
||||
|
||||
pub fn is_zone_signing_key(&self) -> bool {
|
||||
!self.is_key_signing_key()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DNSKEY {
|
||||
|
|
|
@ -19,6 +19,7 @@ impl Resolver {
|
|||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(network: &Network, root: Root) -> ResolverSettings {
|
||||
ResolverSettings {
|
||||
ede: false,
|
||||
network: network.clone(),
|
||||
roots: vec![root],
|
||||
trust_anchor: TrustAnchor::empty(),
|
||||
|
@ -60,6 +61,8 @@ kill -TERM $(cat {pidfile})"
|
|||
}
|
||||
|
||||
pub struct ResolverSettings {
|
||||
/// Extended DNS Errors (RFC8914)
|
||||
ede: bool,
|
||||
network: Network,
|
||||
roots: Vec<Root>,
|
||||
trust_anchor: TrustAnchor,
|
||||
|
@ -84,6 +87,7 @@ impl ResolverSettings {
|
|||
let config = Config::Resolver {
|
||||
use_dnssec,
|
||||
netmask: self.network.netmask(),
|
||||
ede: self.ede,
|
||||
};
|
||||
container.cp(
|
||||
implementation.conf_file_path(config.role()),
|
||||
|
@ -115,6 +119,12 @@ impl ResolverSettings {
|
|||
})
|
||||
}
|
||||
|
||||
/// Enables the Extended DNS Errors (RFC8914) feature
|
||||
pub fn extended_dns_errors(&mut self) -> &mut Self {
|
||||
self.ede = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a root hint
|
||||
pub fn root(&mut self, root: Root) -> &mut Self {
|
||||
self.roots.push(root);
|
||||
|
|
|
@ -5,6 +5,7 @@ server:
|
|||
access-control: {{ netmask }} allow
|
||||
root-hints: /etc/root.hints
|
||||
pidfile: /tmp/unbound.pid
|
||||
ede: {% if ede %} yes {% else %} no {% endif %}
|
||||
{% if use_dnssec %}
|
||||
trust-anchor-file: /etc/trusted-key.key
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in New Issue
Block a user