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::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());
|
||||||
|
|
||||||
|
@ -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 == "."
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user