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::{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<StoppedAuthoritativeNameServer> {
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<Self> {
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> {
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<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 {
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<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 {
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());

View File

@ -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 == "."
}

View File

@ -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());