refactor/ns: build pattern + type state
This commit is contained in:
parent
7ad5bacbdc
commit
3e5ef300ce
|
@ -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());
|
||||
|
||||
|
|
|
@ -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 == "."
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user