refactor/ns: build pattern + type state

This commit is contained in:
Jorge Aparicio 2024-02-06 18:11:31 +01:00
parent 7ad5bacbdc
commit 3e5ef300ce
3 changed files with 126 additions and 154 deletions

View File

@ -5,48 +5,115 @@ use crate::container::Container;
use crate::record::{self, Referral, SoaSettings, ZoneFile}; use crate::record::{self, Referral, SoaSettings, ZoneFile};
use crate::{Domain, Result, CHMOD_RW_EVERYONE}; use crate::{Domain, Result, CHMOD_RW_EVERYONE};
pub struct AuthoritativeNameServer<'a> { pub struct AuthoritativeNameServer<'a, State> {
child: Child,
container: Container, container: Container,
zone_file: ZoneFile<'a>, zone_file: ZoneFile<'a>,
_state: State,
} }
impl<'a> AuthoritativeNameServer<'a> { impl<'a> AuthoritativeNameServer<'a, Stopped> {
/// Spins up a container in a parked state where the name server is not running yet /// Spins up a primary name server that has authority over the given `zone`
pub fn reserve() -> Result<StoppedAuthoritativeNameServer> { ///
/// 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<Self> {
let ns_count = crate::nameserver_count(); let ns_count = crate::nameserver_count();
let nameserver = primary_ns(ns_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()?, container: Container::run()?,
nameserver, zone_file,
ns_count, _state: Stopped,
}) })
} }
/// This is short-hand for `Self::reserve().start(/* .. */)` /// Adds a NS + A record pair to the zone file
pub fn start( pub fn referral(&mut self, referral: &Referral<'a>) -> &mut Self {
domain: Domain<'a>, self.zone_file.referral(referral);
referrals: &[Referral<'a>], self
a_records: &[record::A<'a>],
) -> Result<Self> {
Self::reserve()?.start(domain, referrals, a_records)
} }
/// 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<AuthoritativeNameServer<'a, Running>> {
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 { pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr() self.container.ipv4_addr()
} }
pub fn nameserver(&self) -> &Domain<'a> {
&self.zone_file.soa.ns
}
pub fn zone_file(&self) -> &ZoneFile<'a> { pub fn zone_file(&self) -> &ZoneFile<'a> {
&self.zone_file &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) { fn drop(&mut self) {
let _ = self.child.kill(); let _ = self.child.kill();
} }
@ -60,85 +127,6 @@ fn admin_ns(ns_count: usize) -> Domain<'static> {
Domain(format!("admin{ns_count}.nameservers.com.")).unwrap() 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<AuthoritativeNameServer<'a>> {
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 { fn nsd_conf(domain: &Domain) -> String {
minijinja::render!( minijinja::render!(
include_str!("templates/nsd.conf.jinja"), include_str!("templates/nsd.conf.jinja"),
@ -157,12 +145,11 @@ mod tests {
#[test] #[test]
fn simplest() -> Result<()> { fn simplest() -> Result<()> {
let com_domain = Domain("com.")?; let tld_ns = AuthoritativeNameServer::new(Domain::COM)?.start()?;
let tld_ns = AuthoritativeNameServer::start(com_domain.clone(), &[], &[])?;
let ip_addr = tld_ns.ipv4_addr(); let ip_addr = tld_ns.ipv4_addr();
let client = Client::new()?; 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()); assert!(output.status.is_noerror());
@ -172,20 +159,20 @@ mod tests {
#[test] #[test]
fn with_referral() -> Result<()> { fn with_referral() -> Result<()> {
let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1);
let com_domain = Domain("com.")?; let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?;
let root_ns = AuthoritativeNameServer::start( root_ns.referral(&Referral {
Domain::ROOT, domain: Domain::COM,
&[Referral { ipv4_addr: expected_ip_addr,
domain: com_domain.clone(), ns: Domain("primary.tld-server.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 ip_addr = root_ns.ipv4_addr(); let ipv4_addr = root_ns.ipv4_addr();
let client = Client::new()?; 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()); assert!(output.status.is_noerror());

View File

@ -29,6 +29,10 @@ impl<'a> Domain<'a> {
inner: Cow::Borrowed("."), inner: Cow::Borrowed("."),
}; };
pub const COM: Domain<'static> = Domain {
inner: Cow::Borrowed("com."),
};
pub fn is_root(&self) -> bool { pub fn is_root(&self) -> bool {
self.inner == "." self.inner == "."
} }

View File

@ -42,7 +42,7 @@ impl Drop for RecursiveResolver {
mod tests { mod tests {
use crate::{ use crate::{
client::{RecordType, Recurse}, client::{RecordType, Recurse},
record::{self, Referral}, record::Referral,
AuthoritativeNameServer, Client, Domain, AuthoritativeNameServer, Client, Domain,
}; };
@ -53,53 +53,34 @@ mod tests {
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
let needle = Domain("example.nameservers.com.")?; let needle = Domain("example.nameservers.com.")?;
let root_ns = AuthoritativeNameServer::reserve()?; let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?;
let com_ns = AuthoritativeNameServer::reserve()?; let mut com_ns = AuthoritativeNameServer::new(Domain::COM)?;
let nameservers_domain = Domain("nameservers.com.")?; let nameservers_domain = Domain("nameservers.com.")?;
let nameservers_ns = AuthoritativeNameServer::start( let mut nameservers_ns = AuthoritativeNameServer::new(nameservers_domain.clone())?;
nameservers_domain.clone(), nameservers_ns
&[], .a(root_ns.nameserver().clone(), root_ns.ipv4_addr())
&[ .a(com_ns.nameserver().clone(), com_ns.ipv4_addr())
record::A { .a(needle.clone(), expected_ipv4_addr);
domain: root_ns.nameserver().clone(), let nameservers_ns = nameservers_ns.start()?;
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_file()); eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
let com_domain = Domain("com.")?; com_ns.referral(&Referral {
let com_ns = com_ns.start( domain: nameservers_domain,
com_domain.clone(), ipv4_addr: nameservers_ns.ipv4_addr(),
&[Referral { ns: nameservers_ns.nameserver().clone(),
domain: nameservers_domain, });
ipv4_addr: nameservers_ns.ipv4_addr(), let com_ns = com_ns.start()?;
ns: nameservers_ns.nameserver().clone(),
}],
&[],
)?;
eprintln!("com.zone:\n{}", com_ns.zone_file()); eprintln!("com.zone:\n{}", com_ns.zone_file());
let root_ns = root_ns.start( root_ns.referral(&Referral {
Domain::ROOT, domain: Domain::COM,
&[Referral { ipv4_addr: com_ns.ipv4_addr(),
domain: com_domain, ns: com_ns.nameserver().clone(),
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()); eprintln!("root.zone:\n{}", root_ns.zone_file());