diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index fb44c4ad..e19bd50a 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -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 diff --git a/packages/dns-test/src/fqdn.rs b/packages/dns-test/src/fqdn.rs index 5c3d8678..b0ae757c 100644 --- a/packages/dns-test/src/fqdn.rs +++ b/packages/dns-test/src/fqdn.rs @@ -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 { + 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(()) + } +} diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index eea06b21..3bef3fc3 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -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>, + pub root: Root, + pub trust_anchor: Option, +} + +/// 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)), +} + +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, sign: Sign) -> Result { + // 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::>()?, + 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 { container: Container,