revise names and module organization
This commit is contained in:
parent
7f7d9f7ccf
commit
5858309bfa
177
src/client.rs
177
src/client.rs
@ -1,9 +1,8 @@
|
|||||||
use core::array;
|
|
||||||
use core::result::Result as CoreResult;
|
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
|
use crate::record::{Record, RecordType};
|
||||||
use crate::{Error, Result, FQDN};
|
use crate::{Error, Result, FQDN};
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
@ -22,37 +21,20 @@ impl Client {
|
|||||||
recurse: Recurse,
|
recurse: Recurse,
|
||||||
server: Ipv4Addr,
|
server: Ipv4Addr,
|
||||||
record_type: RecordType,
|
record_type: RecordType,
|
||||||
domain: &FQDN<'_>,
|
fqdn: &FQDN<'_>,
|
||||||
) -> Result<DigOutput> {
|
) -> Result<DigOutput> {
|
||||||
let output = self.inner.stdout(&[
|
let output = self.inner.stdout(&[
|
||||||
"dig",
|
"dig",
|
||||||
recurse.as_str(),
|
recurse.as_str(),
|
||||||
&format!("@{server}"),
|
&format!("@{server}"),
|
||||||
record_type.as_str(),
|
record_type.as_str(),
|
||||||
domain.as_str(),
|
fqdn.as_str(),
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
output.parse()
|
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)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Recurse {
|
pub enum Recurse {
|
||||||
Yes,
|
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<A, Self> {
|
|
||||||
if let Self::A(v) = self {
|
|
||||||
Ok(v)
|
|
||||||
} else {
|
|
||||||
Err(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Record {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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<Self> {
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nxdomain() -> Result<()> {
|
fn dig_nxdomain() -> Result<()> {
|
||||||
// $ dig nonexistent.domain.
|
// $ dig nonexistent.domain.
|
||||||
let input = "
|
let input = "
|
||||||
; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain.
|
; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain.
|
||||||
@ -374,35 +236,4 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,11 @@ pub struct FQDN<'a> {
|
|||||||
pub fn FQDN<'a>(input: impl Into<Cow<'a, str>>) -> Result<FQDN<'a>> {
|
pub fn FQDN<'a>(input: impl Into<Cow<'a, str>>) -> Result<FQDN<'a>> {
|
||||||
let input = input.into();
|
let input = input.into();
|
||||||
if !input.ends_with('.') {
|
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('.') {
|
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 })
|
Ok(FQDN { inner: input })
|
||||||
|
16
src/lib.rs
16
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::fqdn::FQDN;
|
||||||
pub use crate::name_server::NameServer;
|
|
||||||
pub use crate::recursive_resolver::RecursiveResolver;
|
pub use crate::recursive_resolver::RecursiveResolver;
|
||||||
|
|
||||||
pub type Error = Box<dyn std::error::Error>;
|
pub type Error = Box<dyn std::error::Error>;
|
||||||
@ -10,14 +6,10 @@ pub type Result<T> = core::result::Result<T, Error>;
|
|||||||
|
|
||||||
const CHMOD_RW_EVERYONE: &str = "666";
|
const CHMOD_RW_EVERYONE: &str = "666";
|
||||||
|
|
||||||
mod client;
|
pub mod client;
|
||||||
pub mod container;
|
mod container;
|
||||||
mod fqdn;
|
mod fqdn;
|
||||||
mod name_server;
|
pub mod name_server;
|
||||||
pub mod record;
|
pub mod record;
|
||||||
mod recursive_resolver;
|
mod recursive_resolver;
|
||||||
|
pub mod zone_file;
|
||||||
fn nameserver_count() -> usize {
|
|
||||||
static COUNT: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
use core::sync::atomic::{self, AtomicUsize};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
|
|
||||||
use crate::container::Container;
|
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};
|
use crate::{Result, CHMOD_RW_EVERYONE, FQDN};
|
||||||
|
|
||||||
pub struct NameServer<'a, State> {
|
pub struct NameServer<'a, State> {
|
||||||
@ -21,23 +22,24 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
///
|
///
|
||||||
/// The zone file will contain these records
|
/// The zone file will contain these records
|
||||||
///
|
///
|
||||||
/// - one SOA record, with the primary name server set to the name server domain
|
/// - one SOA record, with the primary name server field set to this name server's FQDN
|
||||||
/// - one NS record, with the name server domain set as the only available name server for
|
/// - 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<Self> {
|
pub fn new(zone: FQDN<'a>) -> Result<Self> {
|
||||||
let ns_count = crate::nameserver_count();
|
let ns_count = ns_count();
|
||||||
let nameserver = primary_ns(ns_count);
|
let nameserver = primary_ns(ns_count);
|
||||||
|
|
||||||
let soa = record::Soa {
|
let soa = zone_file::SOA {
|
||||||
domain: zone.clone(),
|
zone: zone.clone(),
|
||||||
ns: nameserver.clone(),
|
nameserver: nameserver.clone(),
|
||||||
admin: admin_ns(ns_count),
|
admin: admin_ns(ns_count),
|
||||||
settings: SoaSettings::default(),
|
settings: SoaSettings::default(),
|
||||||
};
|
};
|
||||||
let mut zone_file = ZoneFile::new(zone.clone(), soa);
|
let mut zone_file = ZoneFile::new(zone.clone(), soa);
|
||||||
|
|
||||||
zone_file.record(record::Ns {
|
zone_file.entry(zone_file::NS {
|
||||||
domain: zone,
|
zone,
|
||||||
ns: nameserver.clone(),
|
nameserver: nameserver.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@ -48,14 +50,19 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a NS + A record pair to the zone file
|
/// Adds a NS + A record pair to the zone file
|
||||||
pub fn referral(&mut self, referral: &Referral<'a>) -> &mut Self {
|
pub fn referral(
|
||||||
self.zone_file.referral(referral);
|
&mut self,
|
||||||
|
zone: FQDN<'a>,
|
||||||
|
nameserver: FQDN<'a>,
|
||||||
|
ipv4_addr: Ipv4Addr,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.zone_file.referral(zone, nameserver, ipv4_addr);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an A record pair to the zone file
|
/// Adds an A record pair to the zone file
|
||||||
pub fn a(&mut self, domain: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self {
|
pub fn a(&mut self, fqdn: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self {
|
||||||
self.zone_file.record(record::A { domain, ipv4_addr });
|
self.zone_file.entry(zone_file::A { fqdn, ipv4_addr });
|
||||||
self
|
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> {
|
impl<'a, S> NameServer<'a, S> {
|
||||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||||
self.container.ipv4_addr()
|
self.container.ipv4_addr()
|
||||||
@ -102,8 +114,12 @@ impl<'a, S> NameServer<'a, S> {
|
|||||||
&self.zone_file
|
&self.zone_file
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nameserver(&self) -> &FQDN<'a> {
|
pub fn zone(&self) -> &FQDN<'a> {
|
||||||
&self.zone_file.soa.ns
|
&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()
|
FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nsd_conf(domain: &FQDN) -> String {
|
fn nsd_conf(fqdn: &FQDN) -> String {
|
||||||
minijinja::render!(
|
minijinja::render!(
|
||||||
include_str!("templates/nsd.conf.jinja"),
|
include_str!("templates/nsd.conf.jinja"),
|
||||||
domain => domain.as_str()
|
fqdn => fqdn.as_str()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::client::{Client, Recurse};
|
||||||
client::{RecordType, Recurse},
|
use crate::record::RecordType;
|
||||||
Client,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -160,11 +174,11 @@ mod tests {
|
|||||||
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 mut root_ns = NameServer::new(FQDN::ROOT)?;
|
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||||
root_ns.referral(&Referral {
|
root_ns.referral(
|
||||||
domain: FQDN::COM,
|
FQDN::COM,
|
||||||
ipv4_addr: expected_ip_addr,
|
FQDN("primary.tld-server.com.")?,
|
||||||
ns: FQDN("primary.tld-server.com.")?,
|
expected_ip_addr,
|
||||||
});
|
);
|
||||||
let root_ns = root_ns.start()?;
|
let root_ns = root_ns.start()?;
|
||||||
|
|
||||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||||
|
385
src/record.rs
385
src/record.rs
@ -1,181 +1,108 @@
|
|||||||
//! DNS records in BIND syntax
|
//! Text representation of DNS records
|
||||||
//!
|
|
||||||
//! Note that the `@` syntax is not used to avoid relying on the order of the records
|
|
||||||
|
|
||||||
use core::fmt;
|
use core::array;
|
||||||
|
use core::result::Result as CoreResult;
|
||||||
|
use core::str::FromStr;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use crate::FQDN;
|
use crate::{Error, Result, FQDN};
|
||||||
|
|
||||||
pub struct ZoneFile<'a> {
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub origin: FQDN<'a>,
|
pub enum RecordType {
|
||||||
pub ttl: u32,
|
A,
|
||||||
pub soa: Soa<'a>,
|
NS,
|
||||||
pub records: Vec<Record<'a>>,
|
SOA,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ZoneFile<'a> {
|
impl RecordType {
|
||||||
/// Convenience constructor that uses "reasonable" defaults
|
pub fn as_str(&self) -> &'static str {
|
||||||
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<Record<'a>>) {
|
|
||||||
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<A<'a>> for Record<'a> {
|
|
||||||
fn from(v: A<'a>) -> Self {
|
|
||||||
Self::A(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Ns<'a>> 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 {
|
|
||||||
match self {
|
match self {
|
||||||
Record::A(a) => a.fmt(f),
|
RecordType::A => "A",
|
||||||
Record::Ns(ns) => ns.fmt(f),
|
RecordType::SOA => "SOA",
|
||||||
|
RecordType::NS => "NS",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Debug)]
|
||||||
pub struct A<'a> {
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub domain: FQDN<'a>,
|
pub enum Record {
|
||||||
|
A(A),
|
||||||
|
SOA(SOA),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Record {
|
||||||
|
pub fn try_into_a(self) -> CoreResult<A, Self> {
|
||||||
|
if let Self::A(v) = self {
|
||||||
|
Ok(v)
|
||||||
|
} else {
|
||||||
|
Err(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Record {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
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,
|
pub ipv4_addr: Ipv4Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for A<'_> {
|
impl FromStr for A {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
type Err = Error;
|
||||||
let Self { domain, ipv4_addr } = self;
|
|
||||||
|
|
||||||
write!(f, "{domain}\tIN\tA\t{ipv4_addr}")
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
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> {
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub domain: FQDN<'a>,
|
#[derive(Debug)]
|
||||||
pub ns: FQDN<'a>,
|
pub struct SOA {
|
||||||
}
|
pub zone: FQDN<'static>,
|
||||||
|
pub ttl: u32,
|
||||||
impl fmt::Display for Ns<'_> {
|
pub nameserver: FQDN<'static>,
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
pub admin: FQDN<'static>,
|
||||||
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 {
|
|
||||||
pub serial: u32,
|
pub serial: u32,
|
||||||
pub refresh: u32,
|
pub refresh: u32,
|
||||||
pub retry: u32,
|
pub retry: u32,
|
||||||
@ -183,112 +110,72 @@ pub struct SoaSettings {
|
|||||||
pub minimum: u32,
|
pub minimum: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SoaSettings {
|
impl FromStr for SOA {
|
||||||
fn default() -> Self {
|
type Err = Error;
|
||||||
Self {
|
|
||||||
serial: 2024010101,
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
refresh: 1800, // 30 minutes
|
let mut columns = input.split_whitespace();
|
||||||
retry: 900, // 15 minutes
|
|
||||||
expire: 604800, // 1 week
|
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] =
|
||||||
minimum: 86400, // 1 day
|
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 {
|
if class != "IN" {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
return Err(format!("unknown class: {class}").into());
|
||||||
let Self {
|
}
|
||||||
serial,
|
|
||||||
refresh,
|
|
||||||
retry,
|
|
||||||
expire,
|
|
||||||
minimum,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::Result;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_to_string() -> Result<()> {
|
fn can_parse_a_record() -> Result<()> {
|
||||||
let expected = "e.gtld-servers.net. IN A 192.12.94.30";
|
let input = "a.root-servers.net. 3600000 IN A 198.41.0.4";
|
||||||
let a = example_a()?;
|
let a: A = input.parse()?;
|
||||||
assert_eq!(expected, a.to_string());
|
|
||||||
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ns_to_string() -> Result<()> {
|
fn can_parse_soa_record() -> Result<()> {
|
||||||
let expected = "com. IN NS e.gtld-servers.net.";
|
let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400";
|
||||||
let ns = example_ns()?;
|
|
||||||
assert_eq!(expected, ns.to_string());
|
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(())
|
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<A<'static>> {
|
|
||||||
Ok(A {
|
|
||||||
domain: FQDN("e.gtld-servers.net.")?,
|
|
||||||
ipv4_addr: Ipv4Addr::new(192, 12, 94, 30),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn example_ns() -> Result<Ns<'static>> {
|
|
||||||
Ok(Ns {
|
|
||||||
domain: FQDN::COM,
|
|
||||||
ns: FQDN("e.gtld-servers.net.")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn example_soa() -> Result<Soa<'static>> {
|
|
||||||
Ok(Soa {
|
|
||||||
domain: FQDN::ROOT,
|
|
||||||
ns: FQDN("a.root-servers.net.")?,
|
|
||||||
admin: FQDN("nstld.verisign-grs.com.")?,
|
|
||||||
settings: SoaSettings::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use std::net::Ipv4Addr;
|
|||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
use crate::record::Root;
|
use crate::zone_file::Root;
|
||||||
use crate::{Result, CHMOD_RW_EVERYONE};
|
use crate::{Result, CHMOD_RW_EVERYONE};
|
||||||
|
|
||||||
pub struct RecursiveResolver {
|
pub struct RecursiveResolver {
|
||||||
@ -41,9 +41,10 @@ impl Drop for RecursiveResolver {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{RecordType, Recurse},
|
client::{Client, Recurse},
|
||||||
record::Referral,
|
name_server::NameServer,
|
||||||
Client, NameServer, FQDN,
|
record::RecordType,
|
||||||
|
FQDN,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -56,35 +57,30 @@ mod tests {
|
|||||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||||
|
|
||||||
let nameservers_domain = FQDN("nameservers.com.")?;
|
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||||
let mut nameservers_ns = NameServer::new(nameservers_domain.clone())?;
|
|
||||||
nameservers_ns
|
nameservers_ns
|
||||||
.a(root_ns.nameserver().clone(), root_ns.ipv4_addr())
|
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||||
.a(com_ns.nameserver().clone(), com_ns.ipv4_addr())
|
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||||
.a(needle.clone(), expected_ipv4_addr);
|
.a(needle.clone(), expected_ipv4_addr);
|
||||||
let nameservers_ns = nameservers_ns.start()?;
|
let nameservers_ns = nameservers_ns.start()?;
|
||||||
|
|
||||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||||
|
|
||||||
com_ns.referral(&Referral {
|
com_ns.referral(
|
||||||
domain: nameservers_domain,
|
nameservers_ns.zone().clone(),
|
||||||
ipv4_addr: nameservers_ns.ipv4_addr(),
|
nameservers_ns.fqdn().clone(),
|
||||||
ns: nameservers_ns.nameserver().clone(),
|
nameservers_ns.ipv4_addr(),
|
||||||
});
|
);
|
||||||
let com_ns = com_ns.start()?;
|
let com_ns = com_ns.start()?;
|
||||||
|
|
||||||
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
||||||
|
|
||||||
root_ns.referral(&Referral {
|
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||||
domain: FQDN::COM,
|
|
||||||
ipv4_addr: com_ns.ipv4_addr(),
|
|
||||||
ns: com_ns.nameserver().clone(),
|
|
||||||
});
|
|
||||||
let root_ns = root_ns.start()?;
|
let root_ns = root_ns.start()?;
|
||||||
|
|
||||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
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 = RecursiveResolver::start(roots)?;
|
||||||
let resolver_ip_addr = resolver.ipv4_addr();
|
let resolver_ip_addr = resolver.ipv4_addr();
|
||||||
|
|
||||||
@ -96,7 +92,7 @@ mod tests {
|
|||||||
let [answer] = output.answer.try_into().unwrap();
|
let [answer] = output.answer.try_into().unwrap();
|
||||||
let a = answer.try_into_a().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);
|
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -2,5 +2,5 @@ remote-control:
|
|||||||
control-enable: no
|
control-enable: no
|
||||||
|
|
||||||
zone:
|
zone:
|
||||||
name: {{ domain }}
|
name: {{ fqdn }}
|
||||||
zonefile: /etc/nsd/zones/main.zone
|
zonefile: /etc/nsd/zones/main.zone
|
||||||
|
287
src/zone_file.rs
Normal file
287
src/zone_file.rs
Normal file
@ -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<Entry<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Entry<'a>>) {
|
||||||
|
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<A<'a>> for Entry<'a> {
|
||||||
|
fn from(v: A<'a>) -> Self {
|
||||||
|
Self::A(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<NS<'a>> 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<A<'static>> {
|
||||||
|
Ok(A {
|
||||||
|
fqdn: FQDN("e.gtld-servers.net.")?,
|
||||||
|
ipv4_addr: Ipv4Addr::new(192, 12, 94, 30),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_ns() -> Result<NS<'static>> {
|
||||||
|
Ok(NS {
|
||||||
|
zone: FQDN::COM,
|
||||||
|
nameserver: FQDN("e.gtld-servers.net.")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_soa() -> Result<SOA<'static>> {
|
||||||
|
Ok(SOA {
|
||||||
|
zone: FQDN::ROOT,
|
||||||
|
nameserver: FQDN("a.root-servers.net.")?,
|
||||||
|
admin: FQDN("nstld.verisign-grs.com.")?,
|
||||||
|
settings: SoaSettings::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user