Merge pull request #50 from ferrous-systems/ja-ede-support-take-2

add support for Extended DNS Error (EDE)
This commit is contained in:
Jorge Aparicio 2024-04-22 16:40:38 +02:00 committed by GitHub
commit 4ce9ec9937
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 545 additions and 157 deletions

View File

@ -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)?;

View File

@ -1,2 +1,3 @@
mod bogus;
mod ede;
mod secure;

View File

@ -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)?;

View 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(())
}

View File

@ -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)?;

View File

@ -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(())
}
}

View File

@ -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

View File

@ -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(())
}
}

View File

@ -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,
)
}
},

View File

@ -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,

View File

@ -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 {

View File

@ -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);

View File

@ -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 %}