From 5858309bfa530baa0c0be2be8b690a10b2e4fd33 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:52:30 +0100 Subject: [PATCH] revise names and module organization --- src/client.rs | 177 +--------------- src/fqdn.rs | 4 +- src/lib.rs | 16 +- src/name_server.rs | 68 ++++--- src/record.rs | 385 +++++++++++++---------------------- src/recursive_resolver.rs | 36 ++-- src/templates/nsd.conf.jinja | 2 +- src/zone_file.rs | 287 ++++++++++++++++++++++++++ 8 files changed, 491 insertions(+), 484 deletions(-) create mode 100644 src/zone_file.rs diff --git a/src/client.rs b/src/client.rs index 38a01ebf..b66522a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,8 @@ -use core::array; -use core::result::Result as CoreResult; use core::str::FromStr; use std::net::Ipv4Addr; use crate::container::Container; +use crate::record::{Record, RecordType}; use crate::{Error, Result, FQDN}; pub struct Client { @@ -22,37 +21,20 @@ impl Client { recurse: Recurse, server: Ipv4Addr, record_type: RecordType, - domain: &FQDN<'_>, + fqdn: &FQDN<'_>, ) -> Result { let output = self.inner.stdout(&[ "dig", recurse.as_str(), &format!("@{server}"), record_type.as_str(), - domain.as_str(), + fqdn.as_str(), ])?; output.parse() } } -#[allow(clippy::upper_case_acronyms)] -pub enum RecordType { - A, - NS, - SOA, -} - -impl RecordType { - fn as_str(&self) -> &'static str { - match self { - RecordType::A => "A", - RecordType::SOA => "SOA", - RecordType::NS => "NS", - } - } -} - #[derive(Clone, Copy)] pub enum Recurse { Yes, @@ -213,132 +195,12 @@ impl FromStr for DigStatus { } } -#[derive(Debug)] -#[allow(clippy::upper_case_acronyms)] -pub enum Record { - A(A), - SOA(SOA), -} - -impl Record { - pub fn try_into_a(self) -> CoreResult { - if let Self::A(v) = self { - Ok(v) - } else { - Err(self) - } - } -} - -impl FromStr for Record { - type Err = Error; - - fn from_str(input: &str) -> Result { - let record_type = input - .split_whitespace() - .nth(3) - .ok_or("record is missing the type column")?; - - let record = match record_type { - "A" => Record::A(input.parse()?), - "NS" => todo!(), - "SOA" => Record::SOA(input.parse()?), - _ => return Err(format!("unknown record type: {record_type}").into()), - }; - - Ok(record) - } -} - -#[derive(Debug)] -pub struct A { - pub domain: FQDN<'static>, - pub ttl: u32, - pub ipv4_addr: Ipv4Addr, -} - -impl FromStr for A { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 5 columns".into()); - }; - - if record_type != "A" { - return Err(format!("tried to parse `{record_type}` record as an A record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - domain: domain.parse()?, - ttl: ttl.parse()?, - ipv4_addr: ipv4_addr.parse()?, - }) - } -} - -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug)] -pub struct SOA { - pub domain: FQDN<'static>, - pub ttl: u32, - pub nameserver: FQDN<'static>, - pub admin: FQDN<'static>, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, -} - -impl FromStr for SOA { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 11 columns".into()); - }; - - if record_type != "SOA" { - return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - domain: domain.parse()?, - ttl: ttl.parse()?, - nameserver: nameserver.parse()?, - admin: admin.parse()?, - serial: serial.parse()?, - refresh: refresh.parse()?, - retry: retry.parse()?, - expire: expire.parse()?, - minimum: minimum.parse()?, - }) - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn nxdomain() -> Result<()> { + fn dig_nxdomain() -> Result<()> { // $ dig nonexistent.domain. let input = " ; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain. @@ -374,35 +236,4 @@ mod tests { Ok(()) } - - #[test] - fn can_parse_a_record() -> Result<()> { - let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; - let a: A = input.parse()?; - - assert_eq!("a.root-servers.net.", a.domain.as_str()); - assert_eq!(3600000, a.ttl); - assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); - - Ok(()) - } - - #[test] - fn can_parse_soa_record() -> Result<()> { - let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; - - let soa: SOA = input.parse()?; - - assert_eq!(".", soa.domain.as_str()); - assert_eq!(15633, soa.ttl); - assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); - assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); - assert_eq!(2024020501, soa.serial); - assert_eq!(1800, soa.refresh); - assert_eq!(900, soa.retry); - assert_eq!(604800, soa.expire); - assert_eq!(86400, soa.minimum); - - Ok(()) - } } diff --git a/src/fqdn.rs b/src/fqdn.rs index fb76e80d..18e08a8b 100644 --- a/src/fqdn.rs +++ b/src/fqdn.rs @@ -14,11 +14,11 @@ pub struct FQDN<'a> { pub fn FQDN<'a>(input: impl Into>) -> Result> { let input = input.into(); if !input.ends_with('.') { - return Err("domain must end with a `.`".into()); + return Err("FQDN must end with a `.`".into()); } if input != "." && input.starts_with('.') { - return Err("non-root domain cannot start with a `.`".into()); + return Err("non-root FQDN cannot start with a `.`".into()); } Ok(FQDN { inner: input }) diff --git a/src/lib.rs b/src/lib.rs index d312149a..20d12ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,4 @@ -use std::sync::atomic::{self, AtomicUsize}; - -pub use crate::client::Client; pub use crate::fqdn::FQDN; -pub use crate::name_server::NameServer; pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; @@ -10,14 +6,10 @@ pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; -mod client; -pub mod container; +pub mod client; +mod container; mod fqdn; -mod name_server; +pub mod name_server; pub mod record; mod recursive_resolver; - -fn nameserver_count() -> usize { - static COUNT: AtomicUsize = AtomicUsize::new(0); - COUNT.fetch_add(1, atomic::Ordering::Relaxed) -} +pub mod zone_file; diff --git a/src/name_server.rs b/src/name_server.rs index c6466bbf..8c4c3aa2 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -1,8 +1,9 @@ +use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::record::{self, Referral, SoaSettings, ZoneFile}; +use crate::zone_file::{self, SoaSettings, ZoneFile}; use crate::{Result, CHMOD_RW_EVERYONE, FQDN}; pub struct NameServer<'a, State> { @@ -21,23 +22,24 @@ impl<'a> NameServer<'a, Stopped> { /// /// 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 + /// - one SOA record, with the primary name server field set to this name server's FQDN + /// - one NS record, with this name server's FQDN set as the only available name server for + /// the zone pub fn new(zone: FQDN<'a>) -> Result { - let ns_count = crate::nameserver_count(); + let ns_count = ns_count(); let nameserver = primary_ns(ns_count); - let soa = record::Soa { - domain: zone.clone(), - ns: nameserver.clone(), + let soa = zone_file::SOA { + zone: zone.clone(), + nameserver: 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(), + zone_file.entry(zone_file::NS { + zone, + nameserver: nameserver.clone(), }); Ok(Self { @@ -48,14 +50,19 @@ impl<'a> NameServer<'a, Stopped> { } /// 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); + pub fn referral( + &mut self, + zone: FQDN<'a>, + nameserver: FQDN<'a>, + ipv4_addr: Ipv4Addr, + ) -> &mut Self { + self.zone_file.referral(zone, nameserver, ipv4_addr); self } /// Adds an A record pair to the zone file - pub fn a(&mut self, domain: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { - self.zone_file.record(record::A { domain, ipv4_addr }); + pub fn a(&mut self, fqdn: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + self.zone_file.entry(zone_file::A { fqdn, ipv4_addr }); self } @@ -93,6 +100,11 @@ impl<'a> NameServer<'a, Stopped> { } } +fn ns_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(0); + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} + impl<'a, S> NameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() @@ -102,8 +114,12 @@ impl<'a, S> NameServer<'a, S> { &self.zone_file } - pub fn nameserver(&self) -> &FQDN<'a> { - &self.zone_file.soa.ns + pub fn zone(&self) -> &FQDN<'a> { + &self.zone_file.origin + } + + pub fn fqdn(&self) -> &FQDN<'a> { + &self.zone_file.soa.nameserver } } @@ -127,19 +143,17 @@ fn admin_ns(ns_count: usize) -> FQDN<'static> { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } -fn nsd_conf(domain: &FQDN) -> String { +fn nsd_conf(fqdn: &FQDN) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), - domain => domain.as_str() + fqdn => fqdn.as_str() ) } #[cfg(test)] mod tests { - use crate::{ - client::{RecordType, Recurse}, - Client, - }; + use crate::client::{Client, Recurse}; + use crate::record::RecordType; use super::*; @@ -160,11 +174,11 @@ mod tests { fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); let mut root_ns = NameServer::new(FQDN::ROOT)?; - root_ns.referral(&Referral { - domain: FQDN::COM, - ipv4_addr: expected_ip_addr, - ns: FQDN("primary.tld-server.com.")?, - }); + root_ns.referral( + FQDN::COM, + FQDN("primary.tld-server.com.")?, + expected_ip_addr, + ); let root_ns = root_ns.start()?; eprintln!("root.zone:\n{}", root_ns.zone_file()); diff --git a/src/record.rs b/src/record.rs index 04c42cc5..cc4edbfe 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,181 +1,108 @@ -//! DNS records in BIND syntax -//! -//! Note that the `@` syntax is not used to avoid relying on the order of the records +//! Text representation of DNS records -use core::fmt; +use core::array; +use core::result::Result as CoreResult; +use core::str::FromStr; use std::net::Ipv4Addr; -use crate::FQDN; +use crate::{Error, Result, FQDN}; -pub struct ZoneFile<'a> { - pub origin: FQDN<'a>, - pub ttl: u32, - pub soa: Soa<'a>, - pub records: Vec>, +#[allow(clippy::upper_case_acronyms)] +pub enum RecordType { + A, + NS, + SOA, } -impl<'a> ZoneFile<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN<'a>, soa: Soa<'a>) -> Self { - Self { - origin, - ttl: 1800, - soa, - records: Vec::new(), - } - } - - /// Appends a record - pub fn record(&mut self, record: impl Into>) { - self.records.push(record.into()) - } - - /// Appends a NS + A record pair - pub fn referral(&mut self, referral: &Referral<'a>) { - let Referral { - domain, - ipv4_addr, - ns, - } = referral; - - self.record(Ns { - domain: domain.clone(), - ns: ns.clone(), - }); - self.record(A { - domain: ns.clone(), - ipv4_addr: *ipv4_addr, - }); - } -} - -impl fmt::Display for ZoneFile<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - origin, - ttl, - soa, - records, - } = self; - - writeln!(f, "$ORIGIN {origin}")?; - writeln!(f, "$TTL {ttl}")?; - writeln!(f, "{soa}")?; - - for record in records { - writeln!(f, "{record}")?; - } - - Ok(()) - } -} - -pub struct Referral<'a> { - pub domain: FQDN<'a>, - pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, -} - -pub struct Root<'a> { - pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, - pub ttl: u32, -} - -impl<'a> Root<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { - Self { - ipv4_addr, - ns, - ttl: 3600000, // 1000 hours - } - } -} - -impl fmt::Display for Root<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { ipv4_addr, ns, ttl } = self; - - writeln!(f, ".\t{ttl}\tNS\t{ns}")?; - write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") - } -} - -pub enum Record<'a> { - A(A<'a>), - Ns(Ns<'a>), -} - -impl<'a> From> for Record<'a> { - fn from(v: A<'a>) -> Self { - Self::A(v) - } -} - -impl<'a> From> for Record<'a> { - fn from(v: Ns<'a>) -> Self { - Self::Ns(v) - } -} - -impl fmt::Display for Record<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl RecordType { + pub fn as_str(&self) -> &'static str { match self { - Record::A(a) => a.fmt(f), - Record::Ns(ns) => ns.fmt(f), + RecordType::A => "A", + RecordType::SOA => "SOA", + RecordType::NS => "NS", } } } -#[derive(Clone)] -pub struct A<'a> { - pub domain: FQDN<'a>, +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +pub enum Record { + A(A), + SOA(SOA), +} + +impl Record { + pub fn try_into_a(self) -> CoreResult { + if let Self::A(v) = self { + Ok(v) + } else { + Err(self) + } + } +} + +impl FromStr for Record { + type Err = Error; + + fn from_str(input: &str) -> Result { + let record_type = input + .split_whitespace() + .nth(3) + .ok_or("record is missing the type column")?; + + let record = match record_type { + "A" => Record::A(input.parse()?), + "NS" => todo!(), + "SOA" => Record::SOA(input.parse()?), + _ => return Err(format!("unknown record type: {record_type}").into()), + }; + + Ok(record) + } +} + +#[derive(Debug)] +pub struct A { + pub fqdn: FQDN<'static>, + pub ttl: u32, pub ipv4_addr: Ipv4Addr, } -impl fmt::Display for A<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { domain, ipv4_addr } = self; +impl FromStr for A { + type Err = Error; - write!(f, "{domain}\tIN\tA\t{ipv4_addr}") + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 5 columns".into()); + }; + + if record_type != "A" { + return Err(format!("tried to parse `{record_type}` record as an A record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + ipv4_addr: ipv4_addr.parse()?, + }) } } -pub struct Ns<'a> { - pub domain: FQDN<'a>, - pub ns: FQDN<'a>, -} - -impl fmt::Display for Ns<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { domain, ns } = self; - - write!(f, "{domain}\tIN\tNS\t{ns}") - } -} - -pub struct Soa<'a> { - pub domain: FQDN<'a>, - pub ns: FQDN<'a>, - pub admin: FQDN<'a>, - pub settings: SoaSettings, -} - -impl fmt::Display for Soa<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - domain, - ns, - admin, - settings, - } = self; - - write!(f, "{domain}\tIN\tSOA\t{ns}\t{admin}\t{settings}") - } -} - -pub struct SoaSettings { +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +pub struct SOA { + pub zone: FQDN<'static>, + pub ttl: u32, + pub nameserver: FQDN<'static>, + pub admin: FQDN<'static>, pub serial: u32, pub refresh: u32, pub retry: u32, @@ -183,112 +110,72 @@ pub struct SoaSettings { pub minimum: u32, } -impl Default for SoaSettings { - fn default() -> Self { - Self { - serial: 2024010101, - refresh: 1800, // 30 minutes - retry: 900, // 15 minutes - expire: 604800, // 1 week - minimum: 86400, // 1 day +impl FromStr for SOA { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 11 columns".into()); + }; + + if record_type != "SOA" { + return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); } - } -} -impl fmt::Display for SoaSettings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - serial, - refresh, - retry, - expire, - minimum, - } = self; + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } - write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + nameserver: nameserver.parse()?, + admin: admin.parse()?, + serial: serial.parse()?, + refresh: refresh.parse()?, + retry: retry.parse()?, + expire: expire.parse()?, + minimum: minimum.parse()?, + }) } } #[cfg(test)] mod tests { - use crate::Result; - use super::*; #[test] - fn a_to_string() -> Result<()> { - let expected = "e.gtld-servers.net. IN A 192.12.94.30"; - let a = example_a()?; - assert_eq!(expected, a.to_string()); + fn can_parse_a_record() -> Result<()> { + let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; + let a: A = input.parse()?; + + assert_eq!("a.root-servers.net.", a.fqdn.as_str()); + assert_eq!(3600000, a.ttl); + assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); Ok(()) } #[test] - fn ns_to_string() -> Result<()> { - let expected = "com. IN NS e.gtld-servers.net."; - let ns = example_ns()?; - assert_eq!(expected, ns.to_string()); + fn can_parse_soa_record() -> Result<()> { + let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + + let soa: SOA = input.parse()?; + + assert_eq!(".", soa.zone.as_str()); + assert_eq!(15633, soa.ttl); + assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); + assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); + assert_eq!(2024020501, soa.serial); + assert_eq!(1800, soa.refresh); + assert_eq!(900, soa.retry); + assert_eq!(604800, soa.expire); + assert_eq!(86400, soa.minimum); Ok(()) } - - #[test] - fn root_to_string() -> Result<()> { - let expected = ". 3600000 NS a.root-servers.net. -a.root-servers.net. 3600000 A 198.41.0.4"; - let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); - assert_eq!(expected, root.to_string()); - Ok(()) - } - - #[test] - fn soa_to_string() -> Result<()> { - let expected = - ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; - let soa = example_soa()?; - assert_eq!(expected, soa.to_string()); - - Ok(()) - } - - #[test] - fn zone_file_to_string() -> Result<()> { - let expected = "$ORIGIN . -$TTL 1800 -. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) -com. IN NS e.gtld-servers.net. -e.gtld-servers.net. IN A 192.12.94.30 -"; - let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); - zone.record(example_ns()?); - zone.record(example_a()?); - - assert_eq!(expected, zone.to_string()); - - Ok(()) - } - - fn example_a() -> Result> { - Ok(A { - domain: FQDN("e.gtld-servers.net.")?, - ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), - }) - } - - fn example_ns() -> Result> { - Ok(Ns { - domain: FQDN::COM, - ns: FQDN("e.gtld-servers.net.")?, - }) - } - - fn example_soa() -> Result> { - Ok(Soa { - domain: FQDN::ROOT, - ns: FQDN("a.root-servers.net.")?, - admin: FQDN("nstld.verisign-grs.com.")?, - settings: SoaSettings::default(), - }) - } } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index eb1b7f0b..3d7b6431 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::record::Root; +use crate::zone_file::Root; use crate::{Result, CHMOD_RW_EVERYONE}; pub struct RecursiveResolver { @@ -41,9 +41,10 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { use crate::{ - client::{RecordType, Recurse}, - record::Referral, - Client, NameServer, FQDN, + client::{Client, Recurse}, + name_server::NameServer, + record::RecordType, + FQDN, }; use super::*; @@ -56,35 +57,30 @@ mod tests { let mut root_ns = NameServer::new(FQDN::ROOT)?; let mut com_ns = NameServer::new(FQDN::COM)?; - let nameservers_domain = FQDN("nameservers.com.")?; - let mut nameservers_ns = NameServer::new(nameservers_domain.clone())?; + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?; nameservers_ns - .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) - .a(com_ns.nameserver().clone(), com_ns.ipv4_addr()) + .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) + .a(com_ns.fqdn().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()); - com_ns.referral(&Referral { - domain: nameservers_domain, - ipv4_addr: nameservers_ns.ipv4_addr(), - ns: nameservers_ns.nameserver().clone(), - }); + 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(&Referral { - domain: FQDN::COM, - ipv4_addr: com_ns.ipv4_addr(), - ns: com_ns.nameserver().clone(), - }); + 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 roots = &[Root::new(root_ns.nameserver().clone(), root_ns.ipv4_addr())]; + let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); @@ -96,7 +92,7 @@ mod tests { let [answer] = output.answer.try_into().unwrap(); let a = answer.try_into_a().unwrap(); - assert_eq!(needle, a.domain); + assert_eq!(needle, a.fqdn); assert_eq!(expected_ipv4_addr, a.ipv4_addr); Ok(()) diff --git a/src/templates/nsd.conf.jinja b/src/templates/nsd.conf.jinja index d3af5808..2a7541af 100644 --- a/src/templates/nsd.conf.jinja +++ b/src/templates/nsd.conf.jinja @@ -2,5 +2,5 @@ remote-control: control-enable: no zone: - name: {{ domain }} + name: {{ fqdn }} zonefile: /etc/nsd/zones/main.zone diff --git a/src/zone_file.rs b/src/zone_file.rs new file mode 100644 index 00000000..524f26db --- /dev/null +++ b/src/zone_file.rs @@ -0,0 +1,287 @@ +//! BIND-style zone file +//! +//! Note that +//! - the `@` syntax is not used to avoid relying on the order of the entries +//! - relative domain names are not used; all domain names must be in fully-qualified form + +use core::fmt; +use std::net::Ipv4Addr; + +use crate::FQDN; + +pub struct ZoneFile<'a> { + pub origin: FQDN<'a>, + pub ttl: u32, + pub soa: SOA<'a>, + pub entries: Vec>, +} + +impl<'a> ZoneFile<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(origin: FQDN<'a>, soa: SOA<'a>) -> Self { + Self { + origin, + ttl: 1800, + soa, + entries: Vec::new(), + } + } + + /// Appends an entry + pub fn entry(&mut self, entry: impl Into>) { + self.entries.push(entry.into()) + } + + /// Appends a NS + A entry pair + pub fn referral(&mut self, zone: FQDN<'a>, nameserver: FQDN<'a>, ipv4_addr: Ipv4Addr) { + self.entry(NS { + zone: zone.clone(), + nameserver: nameserver.clone(), + }); + self.entry(A { + fqdn: nameserver, + ipv4_addr, + }); + } +} + +impl fmt::Display for ZoneFile<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + origin, + ttl, + soa, + entries, + } = self; + + writeln!(f, "$ORIGIN {origin}")?; + writeln!(f, "$TTL {ttl}")?; + writeln!(f, "{soa}")?; + + for entry in entries { + writeln!(f, "{entry}")?; + } + + Ok(()) + } +} + +pub struct Root<'a> { + pub ipv4_addr: Ipv4Addr, + pub ns: FQDN<'a>, + pub ttl: u32, +} + +impl<'a> Root<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { + Self { + ipv4_addr, + ns, + ttl: 3600000, // 1000 hours + } + } +} + +impl fmt::Display for Root<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { ipv4_addr, ns, ttl } = self; + + writeln!(f, ".\t{ttl}\tNS\t{ns}")?; + write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") + } +} + +pub enum Entry<'a> { + A(A<'a>), + NS(NS<'a>), +} + +impl<'a> From> for Entry<'a> { + fn from(v: A<'a>) -> Self { + Self::A(v) + } +} + +impl<'a> From> for Entry<'a> { + fn from(v: NS<'a>) -> Self { + Self::NS(v) + } +} + +impl fmt::Display for Entry<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Entry::A(a) => a.fmt(f), + Entry::NS(ns) => ns.fmt(f), + } + } +} + +#[derive(Clone)] +pub struct A<'a> { + pub fqdn: FQDN<'a>, + pub ipv4_addr: Ipv4Addr, +} + +impl fmt::Display for A<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { fqdn, ipv4_addr } = self; + + write!(f, "{fqdn}\tIN\tA\t{ipv4_addr}") + } +} + +pub struct NS<'a> { + pub zone: FQDN<'a>, + pub nameserver: FQDN<'a>, +} + +impl fmt::Display for NS<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + nameserver: ns, + } = self; + + write!(f, "{zone}\tIN\tNS\t{ns}") + } +} + +pub struct SOA<'a> { + pub zone: FQDN<'a>, + pub nameserver: FQDN<'a>, + pub admin: FQDN<'a>, + pub settings: SoaSettings, +} + +impl fmt::Display for SOA<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + nameserver: ns, + admin, + settings, + } = self; + + write!(f, "{zone}\tIN\tSOA\t{ns}\t{admin}\t{settings}") + } +} + +pub struct SoaSettings { + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl Default for SoaSettings { + fn default() -> Self { + Self { + serial: 2024010101, + refresh: 1800, // 30 minutes + retry: 900, // 15 minutes + expire: 604800, // 1 week + minimum: 86400, // 1 day + } + } +} + +impl fmt::Display for SoaSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + serial, + refresh, + retry, + expire, + minimum, + } = self; + + write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") + } +} + +#[cfg(test)] +mod tests { + use crate::Result; + + use super::*; + + #[test] + fn a_to_string() -> Result<()> { + let expected = "e.gtld-servers.net. IN A 192.12.94.30"; + let a = example_a()?; + assert_eq!(expected, a.to_string()); + + Ok(()) + } + + #[test] + fn ns_to_string() -> Result<()> { + let expected = "com. IN NS e.gtld-servers.net."; + let ns = example_ns()?; + assert_eq!(expected, ns.to_string()); + + Ok(()) + } + + #[test] + fn root_to_string() -> Result<()> { + let expected = ". 3600000 NS a.root-servers.net. +a.root-servers.net. 3600000 A 198.41.0.4"; + let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); + assert_eq!(expected, root.to_string()); + Ok(()) + } + + #[test] + fn soa_to_string() -> Result<()> { + let expected = + ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; + let soa = example_soa()?; + assert_eq!(expected, soa.to_string()); + + Ok(()) + } + + #[test] + fn zone_file_to_string() -> Result<()> { + let expected = "$ORIGIN . +$TTL 1800 +. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) +com. IN NS e.gtld-servers.net. +e.gtld-servers.net. IN A 192.12.94.30 +"; + let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); + zone.entry(example_ns()?); + zone.entry(example_a()?); + + assert_eq!(expected, zone.to_string()); + + Ok(()) + } + + fn example_a() -> Result> { + Ok(A { + fqdn: FQDN("e.gtld-servers.net.")?, + ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), + }) + } + + fn example_ns() -> Result> { + Ok(NS { + zone: FQDN::COM, + nameserver: FQDN("e.gtld-servers.net.")?, + }) + } + + fn example_soa() -> Result> { + Ok(SOA { + zone: FQDN::ROOT, + nameserver: FQDN("a.root-servers.net.")?, + admin: FQDN("nstld.verisign-grs.com.")?, + settings: SoaSettings::default(), + }) + } +}