From 3e5ef300ce2d03265a8e6c82f7c597ad29285d97 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:11:31 +0100 Subject: [PATCH] refactor/ns: build pattern + type state --- src/authoritative_name_server.rs | 215 +++++++++++++++---------------- src/domain.rs | 4 + src/recursive_resolver.rs | 61 +++------ 3 files changed, 126 insertions(+), 154 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 7aafdb7a..c3b85162 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -5,48 +5,115 @@ use crate::container::Container; use crate::record::{self, Referral, SoaSettings, ZoneFile}; use crate::{Domain, Result, CHMOD_RW_EVERYONE}; -pub struct AuthoritativeNameServer<'a> { - child: Child, +pub struct AuthoritativeNameServer<'a, State> { container: Container, zone_file: ZoneFile<'a>, + _state: State, } -impl<'a> AuthoritativeNameServer<'a> { - /// Spins up a container in a parked state where the name server is not running yet - pub fn reserve() -> Result { +impl<'a> AuthoritativeNameServer<'a, Stopped> { + /// 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 FQDN of the name server will have the form `primary{count}.nameservers.com.` where + /// `{count}` is a (process-wide) unique, monotonically increasing integer + /// + /// The zone file will contain these records + /// + /// - one SOA record, with the primary name server set to the name server domain + /// - one NS record, with the name server domain set as the only available name server for + pub fn new(zone: Domain<'a>) -> Result { let ns_count = crate::nameserver_count(); let nameserver = primary_ns(ns_count); - Ok(StoppedAuthoritativeNameServer { + let soa = record::Soa { + domain: zone.clone(), + ns: nameserver.clone(), + admin: admin_ns(ns_count), + settings: SoaSettings::default(), + }; + let mut zone_file = ZoneFile::new(zone.clone(), soa); + + zone_file.record(record::Ns { + domain: zone, + ns: nameserver.clone(), + }); + + Ok(Self { container: Container::run()?, - nameserver, - ns_count, + zone_file, + _state: Stopped, }) } - /// This is short-hand for `Self::reserve().start(/* .. */)` - pub fn start( - domain: Domain<'a>, - referrals: &[Referral<'a>], - a_records: &[record::A<'a>], - ) -> Result { - Self::reserve()?.start(domain, referrals, a_records) + /// Adds a NS + A record pair to the zone file + pub fn referral(&mut self, referral: &Referral<'a>) -> &mut Self { + self.zone_file.referral(referral); + self } + /// Adds an A record pair to the zone file + pub fn a(&mut self, domain: Domain<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + self.zone_file.record(record::A { domain, ipv4_addr }); + self + } + + /// Moves the server to the "Start" state where it can answer client queries + pub fn start(self) -> Result> { + let Self { + container, + zone_file, + _state: _, + } = self; + + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + container.cp( + "/etc/nsd/nsd.conf", + &nsd_conf(&zone_file.origin), + CHMOD_RW_EVERYONE, + )?; + + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + container.cp( + "/etc/nsd/zones/main.zone", + &zone_file.to_string(), + CHMOD_RW_EVERYONE, + )?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(AuthoritativeNameServer { + container, + zone_file, + _state: Running { child }, + }) + } +} + +impl<'a, S> AuthoritativeNameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } - pub fn nameserver(&self) -> &Domain<'a> { - &self.zone_file.soa.ns - } - pub fn zone_file(&self) -> &ZoneFile<'a> { &self.zone_file } + + pub fn nameserver(&self) -> &Domain<'a> { + &self.zone_file.soa.ns + } } -impl Drop for AuthoritativeNameServer<'_> { +pub struct Stopped; + +pub struct Running { + child: Child, +} + +impl Drop for Running { fn drop(&mut self) { let _ = self.child.kill(); } @@ -60,85 +127,6 @@ 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 - } - - /// Starts a primary name server that has authority over the given `zone` - /// - /// The domain of the name server will have the form `primary{count}.nameservers.com.` where - /// `{count}` is a unique, monotonically increasing integer - /// - /// The zone will contain these records - /// - /// - one SOA record, with the primary name server set to the name server domain - /// - one NS record, with the name server domain set as the only available name server for - /// `zone` - /// - one NS + A record pair, for each referral in the `referrals` list - /// - the A records in the `a_records` list - pub fn start<'a>( - self, - zone: 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_file_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone), CHMOD_RW_EVERYONE)?; - - let soa = record::Soa { - domain: zone.clone(), - ns: nameserver.clone(), - admin: admin_ns(ns_count), - settings: SoaSettings::default(), - }; - let mut zone_file = ZoneFile::new(zone.clone(), soa); - - zone_file.record(record::Ns { - domain: zone.clone(), - ns: nameserver.clone(), - }); - - for referral in referrals { - zone_file.referral(referral) - } - - for a in a_records { - zone_file.record(a.clone()) - } - - container.cp(zone_file_path, &zone_file.to_string(), CHMOD_RW_EVERYONE)?; - - let child = container.spawn(&["nsd", "-d"])?; - - Ok(AuthoritativeNameServer { - child, - container, - zone_file, - }) - } -} - fn nsd_conf(domain: &Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), @@ -157,12 +145,11 @@ mod tests { #[test] fn simplest() -> Result<()> { - let com_domain = Domain("com.")?; - let tld_ns = AuthoritativeNameServer::start(com_domain.clone(), &[], &[])?; + let tld_ns = AuthoritativeNameServer::new(Domain::COM)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &com_domain)?; + let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &Domain::COM)?; assert!(output.status.is_noerror()); @@ -172,20 +159,20 @@ mod tests { #[test] fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let com_domain = Domain("com.")?; - let root_ns = AuthoritativeNameServer::start( - Domain::ROOT, - &[Referral { - domain: com_domain.clone(), - ipv4_addr: expected_ip_addr, - ns: Domain("primary.tld-server.com.")?, - }], - &[], - )?; - let ip_addr = root_ns.ipv4_addr(); + let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; + root_ns.referral(&Referral { + domain: Domain::COM, + ipv4_addr: expected_ip_addr, + ns: Domain("primary.tld-server.com.")?, + }); + let root_ns = root_ns.start()?; + + eprintln!("root.zone:\n{}", root_ns.zone_file()); + + let ipv4_addr = root_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::NS, &com_domain)?; + let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &Domain::COM)?; assert!(output.status.is_noerror()); diff --git a/src/domain.rs b/src/domain.rs index 159d4623..bb74ce58 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -29,6 +29,10 @@ impl<'a> Domain<'a> { inner: Cow::Borrowed("."), }; + pub const COM: Domain<'static> = Domain { + inner: Cow::Borrowed("com."), + }; + pub fn is_root(&self) -> bool { self.inner == "." } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 22fcea0e..7e725714 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -42,7 +42,7 @@ impl Drop for RecursiveResolver { mod tests { use crate::{ client::{RecordType, Recurse}, - record::{self, Referral}, + record::Referral, AuthoritativeNameServer, Client, Domain, }; @@ -53,53 +53,34 @@ mod tests { 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 mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; + let mut com_ns = AuthoritativeNameServer::new(Domain::COM)?; 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, - }, - ], - )?; + let mut nameservers_ns = AuthoritativeNameServer::new(nameservers_domain.clone())?; + nameservers_ns + .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) + .a(com_ns.nameserver().clone(), com_ns.ipv4_addr()) + .a(needle.clone(), expected_ipv4_addr); + let nameservers_ns = nameservers_ns.start()?; eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); - 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(), - }], - &[], - )?; + com_ns.referral(&Referral { + domain: nameservers_domain, + ipv4_addr: nameservers_ns.ipv4_addr(), + ns: nameservers_ns.nameserver().clone(), + }); + let com_ns = com_ns.start()?; eprintln!("com.zone:\n{}", com_ns.zone_file()); - let root_ns = root_ns.start( - Domain::ROOT, - &[Referral { - domain: com_domain, - ipv4_addr: com_ns.ipv4_addr(), - ns: com_ns.nameserver().clone(), - }], - &[], - )?; + root_ns.referral(&Referral { + domain: Domain::COM, + ipv4_addr: com_ns.ipv4_addr(), + ns: com_ns.nameserver().clone(), + }); + let root_ns = root_ns.start()?; eprintln!("root.zone:\n{}", root_ns.zone_file());