diff --git a/Cargo.toml b/Cargo.toml index 50ccdc0a..72d5349d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ license = "MIT or Apache 2.0" [dependencies] minijinja = "1.0.12" tempfile = "3.9.0" + +[lib] +doctest = false diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index bde2b3b5..1e2a9563 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -12,40 +12,26 @@ pub struct AuthoritativeNameServer<'a> { } impl<'a> AuthoritativeNameServer<'a> { - pub fn start(domain: Domain<'a>, referrals: &[Referral<'a>]) -> Result { - let container = Container::run()?; - - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - - container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; - + /// Spins up a container in a parked state where the name server is not running yet + pub fn reserve() -> Result { let ns_count = crate::nameserver_count(); - let ns = Domain(format!("primary.ns{ns_count}.com."))?; - let soa = record::Soa { - domain: domain.clone(), - ns, - admin: Domain(format!("admin.ns{ns_count}.com."))?, - settings: SoaSettings::default(), - }; - let mut zone = Zone::new(domain, soa); - for referral in referrals { - zone.referral(referral) - } + let nameserver = primary_ns(ns_count); - container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; - - let child = container.spawn(&["nsd", "-d"])?; - - Ok(Self { - child, - container, - zone, + Ok(StoppedAuthoritativeNameServer { + container: Container::run()?, + nameserver, + ns_count, }) } + pub fn start( + domain: Domain<'a>, + referrals: &[Referral<'a>], + a_records: &[record::A<'a>], + ) -> Result { + Self::reserve()?.start(domain, referrals, a_records) + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } @@ -53,6 +39,10 @@ impl<'a> AuthoritativeNameServer<'a> { pub fn nameserver(&self) -> &Domain<'a> { &self.zone.soa.ns } + + pub fn zone(&self) -> &Zone<'a> { + &self.zone + } } impl Drop for AuthoritativeNameServer<'_> { @@ -61,6 +51,85 @@ impl Drop for AuthoritativeNameServer<'_> { } } +fn primary_ns(ns_count: usize) -> Domain<'static> { + Domain(format!("primary{ns_count}.nameservers.com.")).unwrap() +} + +fn admin_ns(ns_count: usize) -> Domain<'static> { + Domain(format!("admin{ns_count}.nameservers.com.")).unwrap() +} + +pub struct StoppedAuthoritativeNameServer { + container: Container, + nameserver: Domain<'static>, + ns_count: usize, +} + +impl StoppedAuthoritativeNameServer { + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.container.ipv4_addr() + } + + pub fn nameserver(&self) -> &Domain<'static> { + &self.nameserver + } + + pub fn start<'a>( + self, + domain: Domain<'a>, + referrals: &[Referral<'a>], + a_records: &[record::A<'a>], + ) -> Result> { + let Self { + container, + nameserver, + ns_count, + } = self; + + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; + + let soa = record::Soa { + domain: domain.clone(), + ns: nameserver.clone(), + admin: admin_ns(ns_count), + settings: SoaSettings::default(), + }; + let mut zone = Zone::new(domain.clone(), soa); + + zone.record(record::Ns { + domain: domain.clone(), + ns: nameserver, + }); + zone.record(record::A { + domain, + ipv4_addr: container.ipv4_addr(), + }); + + for referral in referrals { + zone.referral(referral) + } + + for a in a_records { + zone.record(a.clone()) + } + + container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(AuthoritativeNameServer { + child, + container, + zone, + }) + } +} + fn nsd_conf(domain: &Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), @@ -74,7 +143,7 @@ mod tests { #[test] fn tld_ns() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; + let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[], &[])?; let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; @@ -89,7 +158,7 @@ mod tests { #[test] fn root_ns() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[])?; + let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[], &[])?; let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; @@ -112,6 +181,7 @@ mod tests { ipv4_addr: expected_ip_addr, ns: Domain("primary.tld-server.com.")?, }], + &[], )?; let ip_addr = root_ns.ipv4_addr(); diff --git a/src/record.rs b/src/record.rs index aaafbb9d..c659e93a 100644 --- a/src/record.rs +++ b/src/record.rs @@ -43,7 +43,7 @@ impl<'a> Zone<'a> { ns: ns.clone(), }); self.record(A { - domain: domain.clone(), + domain: ns.clone(), ipv4_addr: *ipv4_addr, }); } @@ -128,6 +128,7 @@ impl fmt::Display for Record<'_> { } } +#[derive(Clone)] pub struct A<'a> { pub domain: Domain<'a>, pub ipv4_addr: Ipv4Addr, diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index ed823bc5..c246fadc 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -40,36 +40,94 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { - use crate::{record::Referral, AuthoritativeNameServer, Domain}; + use crate::{ + record::{self, Referral}, + AuthoritativeNameServer, Domain, + }; use super::*; #[test] fn can_resolve() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle = Domain("example.nameservers.com.")?; + let root_ns = AuthoritativeNameServer::reserve()?; + let com_ns = AuthoritativeNameServer::reserve()?; - let root_ns = AuthoritativeNameServer::start( + let nameservers_domain = Domain("nameservers.com.")?; + let nameservers_ns = AuthoritativeNameServer::start( + nameservers_domain.clone(), + &[], + &[ + record::A { + domain: root_ns.nameserver().clone(), + ipv4_addr: root_ns.ipv4_addr(), + }, + record::A { + domain: com_ns.nameserver().clone(), + ipv4_addr: com_ns.ipv4_addr(), + }, + record::A { + domain: needle.clone(), + ipv4_addr: expected_ipv4_addr, + }, + ], + )?; + + eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone()); + + let com_domain = Domain("com.")?; + let com_ns = com_ns.start( + com_domain.clone(), + &[Referral { + domain: nameservers_domain, + ipv4_addr: nameservers_ns.ipv4_addr(), + ns: nameservers_ns.nameserver().clone(), + }], + &[], + )?; + + eprintln!("com.zone:\n{}", com_ns.zone()); + + let root_ns = root_ns.start( Domain::ROOT, &[Referral { - domain: Domain("com.")?, - ipv4_addr: tld_ns.ipv4_addr(), - ns: tld_ns.nameserver().clone(), + domain: com_domain, + ipv4_addr: com_ns.ipv4_addr(), + ns: com_ns.nameserver().clone(), }], + &[], )?; + eprintln!("root.zone:\n{}", root_ns.zone()); + let roots = &[Root::new(root_ns.nameserver().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); let container = Container::run()?; - let output = - container.output(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; + let output = container.output(&[ + "dig", + &format!("@{}", resolver_ip_addr), + &needle.to_string(), + ])?; eprintln!("{}", output.stdout); assert!(output.status.success()); assert!(output.stdout.contains("status: NOERROR")); + let mut found = false; + let needle = needle.to_string(); + for line in output.stdout.lines() { + if line.starts_with(&needle) { + found = true; + assert!(line.ends_with(&expected_ipv4_addr.to_string())); + } + } + + assert!(found); + Ok(()) } }