drop most zone_file entry types

instead use the record types in zone files

the main difference between e.g. zone_file::A and record::A was that the
latter had a TTL filed and the former didn't

to eliminate code duplication we make the `ZoneFile` API use the
`record` types and discard the zone_file entry types
This commit is contained in:
Jorge Aparicio 2024-02-19 11:51:51 +01:00
parent a83b6629a3
commit 66d6061ffc
9 changed files with 587 additions and 489 deletions

View File

@ -2,7 +2,7 @@ use std::net::Ipv4Addr;
use dns_test::client::{Client, Dnssec, Recurse};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::Root;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
@ -18,9 +18,9 @@ fn can_resolve() -> Result<()> {
let mut nameservers_ns =
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
.a(needle_fqdn.clone(), expected_ipv4_addr);
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()))
.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let nameservers_ns = nameservers_ns.start()?;
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
@ -75,8 +75,8 @@ fn nxdomain() -> Result<()> {
let mut nameservers_ns =
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
let nameservers_ns = nameservers_ns.start()?;
com_ns.referral(

View File

@ -2,7 +2,7 @@ use std::net::Ipv4Addr;
use dns_test::client::{Client, Dnssec, Recurse};
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::Root;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
@ -12,7 +12,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
fn can_validate_without_delegation() -> Result<()> {
let network = Network::new()?;
let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?;
ns.a(ns.fqdn().clone(), ns.ipv4_addr());
ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr()));
let ns = ns.sign()?;
let root_ksk = ns.key_signing_key().clone();
@ -61,9 +61,9 @@ fn can_validate_with_delegation() -> Result<()> {
let mut nameservers_ns =
NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
.a(needle_fqdn.clone(), expected_ipv4_addr);
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()))
.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr));
let nameservers_ns = nameservers_ns.sign()?;
let nameservers_ds = nameservers_ns.ds().clone();
let nameservers_ns = nameservers_ns.start()?;
@ -76,7 +76,7 @@ fn can_validate_with_delegation() -> Result<()> {
nameservers_ns.fqdn().clone(),
nameservers_ns.ipv4_addr(),
)
.ds(nameservers_ds);
.add(nameservers_ds);
let com_ns = com_ns.sign()?;
let com_ds = com_ns.ds().clone();
let com_ns = com_ns.start()?;
@ -85,7 +85,7 @@ fn can_validate_with_delegation() -> Result<()> {
root_ns
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
.ds(com_ds);
.add(com_ds);
let root_ns = root_ns.sign()?;
let root_ksk = root_ns.key_signing_key().clone();
let root_zsk = root_ns.zone_signing_key().clone();

View File

@ -2,7 +2,7 @@ use std::sync::mpsc;
use dns_test::client::Client;
use dns_test::name_server::NameServer;
use dns_test::record::RecordType;
use dns_test::record::{Record, RecordType};
use dns_test::zone_file::Root;
use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN};
@ -19,8 +19,8 @@ fn main() -> Result<()> {
let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
let nameservers_ns = nameservers_ns.sign()?;
let nameservers_ds = nameservers_ns.ds().clone();
let nameservers_ns = nameservers_ns.start()?;
@ -31,14 +31,14 @@ fn main() -> Result<()> {
nameservers_ns.fqdn().clone(),
nameservers_ns.ipv4_addr(),
)
.ds(nameservers_ds);
.add(nameservers_ds);
let com_ns = com_ns.sign()?;
let com_ds = com_ns.ds().clone();
let com_ns = com_ns.start()?;
root_ns
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
.ds(com_ds);
.add(com_ds);
let root_ns = root_ns.sign()?;
let root_ksk = root_ns.key_signing_key().clone();
let root_zsk = root_ns.zone_signing_key().clone();

View File

@ -10,9 +10,6 @@ pub use crate::fqdn::FQDN;
pub use crate::resolver::Resolver;
pub use crate::trust_anchor::TrustAnchor;
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = core::result::Result<T, Error>;
pub mod client;
mod container;
mod fqdn;
@ -23,6 +20,12 @@ mod trust_anchor;
pub mod tshark;
pub mod zone_file;
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = core::result::Result<T, Error>;
// TODO maybe this should be a TLS variable that each unit test (thread) can override
const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day
#[derive(Clone)]
pub enum Implementation {
Unbound,

View File

@ -2,9 +2,10 @@ use core::sync::atomic::{self, AtomicUsize};
use std::net::Ipv4Addr;
use crate::container::{Child, Container, Network};
use crate::record::{self, Record, SoaSettings, DS, SOA};
use crate::tshark::Tshark;
use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS};
use crate::{Implementation, Result, FQDN};
use crate::zone_file::{self, ZoneFile};
use crate::{Implementation, Result, DEFAULT_TTL, FQDN};
pub struct NameServer<State> {
container: Container,
@ -34,18 +35,16 @@ impl NameServer<Stopped> {
let ns_count = ns_count();
let nameserver = primary_ns(ns_count);
let soa = zone_file::SOA {
let soa = SOA {
zone: zone.clone(),
ttl: DEFAULT_TTL,
nameserver: nameserver.clone(),
admin: admin_ns(ns_count),
settings: SoaSettings::default(),
};
let mut zone_file = ZoneFile::new(zone.clone(), soa);
let mut zone_file = ZoneFile::new(soa);
zone_file.entry(zone_file::NS {
zone,
nameserver: nameserver.clone(),
});
zone_file.add(Record::ns(zone, nameserver.clone()));
let image = implementation.into();
Ok(Self {
@ -61,15 +60,9 @@ impl NameServer<Stopped> {
self
}
/// Adds an A record pair to the zone file
pub fn a(&mut self, fqdn: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self {
self.zone_file.entry(zone_file::A { fqdn, ipv4_addr });
self
}
/// Adds a DS record to the zone file
pub fn ds(&mut self, ds: DS) -> &mut Self {
self.zone_file.entry(ds);
/// Adds a record to the name server's zone file
pub fn add(&mut self, record: impl Into<Record>) -> &mut Self {
self.zone_file.add(record);
self
}
@ -89,19 +82,19 @@ impl NameServer<Stopped> {
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?;
let zone = &zone_file.origin;
let zone = zone_file.origin();
let zsk_keygen =
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key");
let zsk: DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?;
let zsk: zone_file::DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?;
let ksk_keygen =
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key");
let ksk: DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?;
let ksk: zone_file::DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?;
// -n = use NSEC3 instead of NSEC
// -p = set the opt-out flag on all nsec3 rrs
@ -120,15 +113,17 @@ impl NameServer<Stopped> {
container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?;
let signed_zone_file = container.stdout(&["cat", &zone_file_path])?;
let ttl = zone_file.soa.ttl;
Ok(NameServer {
container,
zone_file,
state: Signed {
ds,
ksk,
signed_zone_file,
zsk,
// inherit SOA's TTL value
ksk: ksk.with_ttl(ttl),
zsk: zsk.with_ttl(ttl),
},
})
}
@ -144,7 +139,7 @@ impl NameServer<Stopped> {
// for PID file
container.status_ok(&["mkdir", "-p", "/run/nsd/"])?;
container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?;
container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?;
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
container.cp(&zone_file_path(), &zone_file.to_string())?;
@ -183,7 +178,7 @@ impl NameServer<Signed> {
// for PID file
container.status_ok(&["mkdir", "-p", "/run/nsd/"])?;
container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?;
container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?;
let child = container.spawn(&["nsd", "-d"])?;
@ -194,11 +189,11 @@ impl NameServer<Signed> {
})
}
pub fn key_signing_key(&self) -> &DNSKEY {
pub fn key_signing_key(&self) -> &record::DNSKEY {
&self.state.ksk
}
pub fn zone_signing_key(&self) -> &DNSKEY {
pub fn zone_signing_key(&self) -> &record::DNSKEY {
&self.state.zsk
}
@ -256,7 +251,7 @@ impl<S> NameServer<S> {
}
pub fn zone(&self) -> &FQDN {
&self.zone_file.origin
self.zone_file.origin()
}
pub fn fqdn(&self) -> &FQDN {
@ -268,8 +263,8 @@ pub struct Stopped;
pub struct Signed {
ds: DS,
zsk: DNSKEY,
ksk: DNSKEY,
zsk: record::DNSKEY,
ksk: record::DNSKEY,
signed_zone_file: String,
}

View File

@ -1,24 +1,29 @@
//! Text representation of DNS records
use core::array;
use core::result::Result as CoreResult;
use core::str::FromStr;
use core::{array, fmt};
use std::fmt::Write;
use std::net::Ipv4Addr;
use crate::{Error, Result, FQDN};
use crate::{Error, Result, DEFAULT_TTL, FQDN};
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, PartialEq)]
pub enum RecordType {
A,
DS,
NS,
SOA,
// excluded because cannot appear in RRSIG.type_covered
// RRSIG,
}
impl RecordType {
pub fn as_str(&self) -> &'static str {
match self {
RecordType::A => "A",
RecordType::DS => "DS",
RecordType::SOA => "SOA",
RecordType::NS => "NS",
}
@ -31,6 +36,7 @@ impl FromStr for RecordType {
fn from_str(input: &str) -> CoreResult<Self, Self::Err> {
let record_type = match input {
"A" => Self::A,
"DS" => Self::DS,
"SOA" => Self::SOA,
"NS" => Self::NS,
_ => return Err(format!("unknown record type: {input}").into()),
@ -40,14 +46,59 @@ impl FromStr for RecordType {
}
}
impl fmt::Display for RecordType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
RecordType::A => "A",
RecordType::DS => "DS",
RecordType::NS => "NS",
RecordType::SOA => "SOA",
};
f.write_str(s)
}
}
#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub enum Record {
A(A),
DS(DS),
NS(NS),
RRSIG(RRSIG),
SOA(SOA),
}
impl From<DS> for Record {
fn from(v: DS) -> Self {
Self::DS(v)
}
}
impl From<A> for Record {
fn from(v: A) -> Self {
Self::A(v)
}
}
impl From<NS> for Record {
fn from(v: NS) -> Self {
Self::NS(v)
}
}
impl From<RRSIG> for Record {
fn from(v: RRSIG) -> Self {
Self::RRSIG(v)
}
}
impl From<SOA> for Record {
fn from(v: SOA) -> Self {
Self::SOA(v)
}
}
impl Record {
pub fn try_into_a(self) -> CoreResult<A, Self> {
if let Self::A(v) = self {
@ -68,6 +119,24 @@ impl Record {
pub fn is_soa(&self) -> bool {
matches!(self, Self::SOA(..))
}
pub fn a(fqdn: FQDN, ipv4_addr: Ipv4Addr) -> Self {
A {
fqdn,
ttl: DEFAULT_TTL,
ipv4_addr,
}
.into()
}
pub fn ns(zone: FQDN, nameserver: FQDN) -> Self {
NS {
zone,
ttl: DEFAULT_TTL,
nameserver,
}
.into()
}
}
impl FromStr for Record {
@ -91,6 +160,18 @@ impl FromStr for Record {
}
}
impl fmt::Display for Record {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Record::A(a) => write!(f, "{a}"),
Record::DS(ds) => write!(f, "{ds}"),
Record::NS(ns) => write!(f, "{ns}"),
Record::RRSIG(rrsig) => write!(f, "{rrsig}"),
Record::SOA(soa) => write!(f, "{soa}"),
}
}
}
#[derive(Debug)]
pub struct A {
pub fqdn: FQDN,
@ -129,6 +210,189 @@ impl FromStr for A {
}
}
impl fmt::Display for A {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
fqdn,
ttl,
ipv4_addr,
} = self;
write!(f, "{fqdn}\t{ttl}\tIN\tA\t{ipv4_addr}")
}
}
// integer types chosen based on bit sizes in section 2.1 of RFC4034
#[derive(Clone, Debug)]
pub struct DNSKEY {
pub zone: FQDN,
pub ttl: u32,
pub flags: u16,
pub protocol: u8,
pub algorithm: u8,
pub public_key: String,
}
impl DNSKEY {
/// formats the `DNSKEY` in the format `delv` expects
pub(super) fn delv(&self) -> String {
let Self {
zone,
flags,
protocol,
algorithm,
public_key,
..
} = self;
format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n")
}
}
impl FromStr for DNSKEY {
type Err = Error;
fn from_str(input: &str) -> CoreResult<Self, Self::Err> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm)] =
array::from_fn(|_| columns.next())
else {
return Err("expected at least 7 columns".into());
};
if record_type != "DNSKEY" {
return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into());
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
let mut public_key = String::new();
for column in columns {
public_key.push_str(column);
}
Ok(Self {
zone: zone.parse()?,
ttl: ttl.parse()?,
flags: flags.parse()?,
protocol: protocol.parse()?,
algorithm: algorithm.parse()?,
public_key,
})
}
}
impl fmt::Display for DNSKEY {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
ttl,
flags,
protocol,
algorithm,
public_key,
} = self;
write!(
f,
"{zone}\t{ttl}\tIN\tDNSKEY\t{flags} {protocol} {algorithm}"
)?;
write_split_long_string(f, public_key)
}
}
#[derive(Clone, Debug)]
pub struct DS {
zone: FQDN,
ttl: u32,
key_tag: u16,
algorithm: u8,
digest_type: u8,
digest: String,
}
impl FromStr for DS {
type Err = Error;
fn from_str(input: &str) -> CoreResult<Self, Self::Err> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type)] =
array::from_fn(|_| columns.next())
else {
return Err("expected at least 7 columns".into());
};
let expected = "DS";
if record_type != expected {
return Err(
format!("tried to parse `{record_type}` entry as a {expected} entry").into(),
);
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
let mut digest = String::new();
for column in columns {
digest.push_str(column);
}
Ok(Self {
zone: zone.parse()?,
ttl: ttl.parse()?,
key_tag: key_tag.parse()?,
algorithm: algorithm.parse()?,
digest_type: digest_type.parse()?,
digest,
})
}
}
impl fmt::Display for DS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
ttl,
key_tag,
algorithm,
digest_type,
digest,
} = self;
write!(
f,
"{zone}\t{ttl}\tIN\tDS\t{key_tag} {algorithm} {digest_type}"
)?;
write_split_long_string(f, digest)
}
}
#[derive(Debug)]
pub struct NS {
pub zone: FQDN,
pub ttl: u32,
pub nameserver: FQDN,
}
impl fmt::Display for NS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
ttl,
nameserver,
} = self;
write!(f, "{zone}\t{ttl}\tIN\tNS {nameserver}")
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug)]
pub struct RRSIG {
@ -190,6 +454,28 @@ impl FromStr for RRSIG {
}
}
impl fmt::Display for RRSIG {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
fqdn,
ttl,
type_covered,
algorithm,
labels,
original_ttl,
signature_expiration,
signature_inception,
key_tag,
signer_name,
signature,
} = self;
write!(f, "{fqdn}\t{ttl}\tIN\tRRSIG\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?;
write_split_long_string(f, signature)
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug)]
pub struct SOA {
@ -197,11 +483,7 @@ pub struct SOA {
pub ttl: u32,
pub nameserver: FQDN,
pub admin: FQDN,
pub serial: u32,
pub refresh: u32,
pub retry: u32,
pub expire: u32,
pub minimum: u32,
pub settings: SoaSettings,
}
impl FromStr for SOA {
@ -229,34 +511,198 @@ impl FromStr for SOA {
ttl: ttl.parse()?,
nameserver: nameserver.parse()?,
admin: admin.parse()?,
serial: serial.parse()?,
refresh: refresh.parse()?,
retry: retry.parse()?,
expire: expire.parse()?,
minimum: minimum.parse()?,
settings: SoaSettings {
serial: serial.parse()?,
refresh: refresh.parse()?,
retry: retry.parse()?,
expire: expire.parse()?,
minimum: minimum.parse()?,
},
})
}
}
impl fmt::Display for SOA {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
ttl,
nameserver,
admin,
settings,
} = self;
write!(f, "{zone}\t{ttl}\tIN\tSOA\t{nameserver} {admin} {settings}")
}
}
#[derive(Debug)]
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}")
}
}
fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Result {
for (index, c) in field.chars().enumerate() {
if index % 56 == 0 {
f.write_char(' ')?;
}
f.write_char(c)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[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()?;
fn a() -> Result<()> {
// dig A a.root-servers.net
let input = "a.root-servers.net. 77859 IN A 198.41.0.4";
let a @ A {
fqdn,
ttl,
ipv4_addr,
} = &input.parse()?;
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);
assert_eq!("a.root-servers.net.", fqdn.as_str());
assert_eq!(77859, *ttl);
assert_eq!(Ipv4Addr::new(198, 41, 0, 4), *ipv4_addr);
let output = a.to_string();
assert_eq!(output, input);
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";
fn dnskey() -> Result<()> {
// dig DNSKEY .
let input = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU=";
let dnskey @ DNSKEY {
zone,
ttl,
flags,
protocol,
algorithm,
public_key,
} = &input.parse()?;
assert_eq!(FQDN::ROOT, *zone);
assert_eq!(1116, *ttl);
assert_eq!(257, *flags);
assert_eq!(3, *protocol);
assert_eq!(8, *algorithm);
let expected = "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=";
assert_eq!(expected, public_key);
let output = dnskey.to_string();
assert_eq!(output, input);
Ok(())
}
#[test]
fn ds() -> Result<()> {
// dig DS com.
let input = "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A";
let ds @ DS {
zone,
ttl,
key_tag,
algorithm,
digest_type,
digest,
} = &input.parse()?;
assert_eq!(FQDN::COM, *zone);
assert_eq!(7612, *ttl);
assert_eq!(19718, *key_tag);
assert_eq!(13, *algorithm);
assert_eq!(2, *digest_type);
let expected = "8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D771D7805A";
assert_eq!(expected, digest);
let output = ds.to_string();
assert_eq!(output, input);
Ok(())
}
#[test]
fn rrsig() -> Result<()> {
// dig +dnssec SOA .
let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY=";
let rrsig @ RRSIG {
fqdn,
ttl,
type_covered,
algorithm,
labels,
original_ttl,
signature_expiration,
signature_inception,
key_tag,
signer_name,
signature,
} = &input.parse()?;
assert_eq!(FQDN::ROOT, *fqdn);
assert_eq!(1800, *ttl);
assert_eq!(RecordType::SOA, *type_covered);
assert_eq!(7, *algorithm);
assert_eq!(0, *labels);
assert_eq!(1800, *original_ttl);
assert_eq!(20240306132701, *signature_expiration);
assert_eq!(20240207132701, *signature_inception);
assert_eq!(11264, *key_tag);
assert_eq!(FQDN::ROOT, *signer_name);
let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY=";
assert_eq!(expected, signature);
let output = rrsig.to_string();
assert_eq!(input, output);
Ok(())
}
#[test]
fn soa() -> Result<()> {
// dig SOA .
let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400";
let soa: SOA = input.parse()?;
@ -264,32 +710,15 @@ mod tests {
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);
let settings = &soa.settings;
assert_eq!(2024020501, settings.serial);
assert_eq!(1800, settings.refresh);
assert_eq!(900, settings.retry);
assert_eq!(604800, settings.expire);
assert_eq!(86400, settings.minimum);
Ok(())
}
#[test]
fn can_parse_rrsig_record() -> Result<()> {
let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY=";
let rrsig: RRSIG = input.parse()?;
assert_eq!(FQDN::ROOT, rrsig.fqdn);
assert_eq!(1800, rrsig.ttl);
assert_eq!(RecordType::SOA, rrsig.type_covered);
assert_eq!(7, rrsig.algorithm);
assert_eq!(0, rrsig.labels);
assert_eq!(20240306132701, rrsig.signature_expiration);
assert_eq!(20240207132701, rrsig.signature_inception);
assert_eq!(11264, rrsig.key_tag);
assert_eq!(FQDN::ROOT, rrsig.signer_name);
let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY=";
assert_eq!(expected, rrsig.signature);
let output = soa.to_string();
assert_eq!(output, input);
Ok(())
}

View File

@ -1,6 +1,6 @@
use core::fmt;
use crate::zone_file::DNSKEY;
use crate::record::DNSKEY;
pub struct TrustAnchor {
keys: Vec<DNSKEY>,

View File

@ -246,7 +246,7 @@ struct Ip {
mod tests {
use crate::client::{Client, Dnssec, Recurse};
use crate::name_server::NameServer;
use crate::record::RecordType;
use crate::record::{Record, RecordType};
use crate::zone_file::Root;
use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN};
@ -297,8 +297,8 @@ mod tests {
let mut nameservers_ns =
NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?;
nameservers_ns
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
.add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr()))
.add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr()));
let nameservers_ns = nameservers_ns.start()?;
com_ns.referral(

View File

@ -1,72 +1,63 @@
//! BIND-style zone file
//! BIND-style zone files
//!
//! 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::{array, fmt};
use core::fmt;
use std::array;
use std::net::Ipv4Addr;
use std::str::FromStr;
use crate::{Error, FQDN};
use crate::record::{self, Record, SOA};
use crate::{Error, Result, DEFAULT_TTL, FQDN};
pub struct ZoneFile {
pub origin: FQDN,
pub ttl: u32,
origin: FQDN,
pub soa: SOA,
pub entries: Vec<Entry>,
pub records: Vec<Record>,
}
impl ZoneFile {
/// Convenience constructor that uses "reasonable" defaults
pub fn new(origin: FQDN, soa: SOA) -> Self {
pub fn new(soa: SOA) -> Self {
Self {
origin,
ttl: 1800,
origin: soa.zone.clone(),
soa,
entries: Vec::new(),
records: Vec::new(),
}
}
/// Appends an entry
pub fn entry(&mut self, entry: impl Into<Entry>) {
self.entries.push(entry.into())
/// Adds the given `record` to the zone file
pub fn add(&mut self, record: impl Into<Record>) {
self.records.push(record.into())
}
/// Appends a NS + A entry pair
/// Shortcut method for adding a referral (NS + A record pair)
pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) {
self.entry(NS {
zone: zone.clone(),
nameserver: nameserver.clone(),
});
self.entry(A {
fqdn: nameserver,
ipv4_addr,
});
self.add(Record::ns(zone, nameserver.clone()));
self.add(Record::a(nameserver, ipv4_addr));
}
pub(crate) fn origin(&self) -> &FQDN {
&self.origin
}
}
impl fmt::Display for ZoneFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
origin,
ttl,
soa,
entries,
} = self;
let Self { soa, records, .. } = self;
writeln!(f, "$ORIGIN {origin}")?;
writeln!(f, "$TTL {ttl}")?;
writeln!(f, "{soa}")?;
for entry in entries {
writeln!(f, "{entry}")?;
for record in records {
writeln!(f, "{record}")?;
}
Ok(())
}
}
/// A root (server) hint
pub struct Root {
pub ipv4_addr: Ipv4Addr,
pub ns: FQDN,
@ -79,7 +70,7 @@ impl Root {
Self {
ipv4_addr,
ns,
ttl: 3600000, // 1000 hours
ttl: DEFAULT_TTL,
}
}
}
@ -93,100 +84,47 @@ impl fmt::Display for Root {
}
}
pub enum Entry {
A(A),
DNSKEY(DNSKEY),
DS(DS),
NS(NS),
}
impl From<DS> for Entry {
fn from(v: DS) -> Self {
Self::DS(v)
}
}
impl From<A> for Entry {
fn from(v: A) -> Self {
Self::A(v)
}
}
impl From<NS> for Entry {
fn from(v: NS) -> 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::DNSKEY(dnskey) => dnskey.fmt(f),
Entry::DS(ds) => ds.fmt(f),
Entry::NS(ns) => ns.fmt(f),
}
}
}
#[derive(Clone)]
pub struct A {
pub fqdn: FQDN,
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}")
}
}
// integer types chosen based on bit sizes in section 2.1 of RFC4034
#[derive(Clone, Debug)]
pub struct DNSKEY {
// NOTE compared to `record::DNSKEY`, this zone file entry lacks the TTL field
#[allow(clippy::upper_case_acronyms)]
pub(crate) struct DNSKEY {
zone: FQDN,
flags: u16,
protocol: u8,
algorithm: u8,
public_key: String,
// extra information in `+multiline` format and `ldns-keygen`'s output
bits: u16,
key_tag: u16,
}
impl DNSKEY {
pub fn bits(&self) -> u16 {
self.bits
}
pub fn key_tag(&self) -> u16 {
self.key_tag
}
/// formats the `DNSKEY` in the format `delv` expects
pub(super) fn delv(&self) -> String {
pub fn with_ttl(self, ttl: u32) -> record::DNSKEY {
let Self {
zone,
flags,
protocol,
algorithm,
public_key,
..
} = self;
format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n")
record::DNSKEY {
zone,
ttl,
flags,
protocol,
algorithm,
public_key,
}
}
}
impl FromStr for DNSKEY {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let (before, after) = input.split_once(';').ok_or("comment was not found")?;
let mut columns = before.split_whitespace();
fn from_str(mut input: &str) -> Result<Self> {
// discard trailing comment
if let Some((before, _after)) = input.split_once(';') {
input = before.trim();
}
let mut columns = input.split_whitespace();
let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] =
array::from_fn(|_| columns.next())
@ -202,306 +140,39 @@ impl FromStr for DNSKEY {
return Err(format!("unknown class: {class}").into());
}
// {id = 24975 (zsk), size = 1024b}
let error = "invalid comment syntax";
let (id_expr, size_expr) = after.split_once(',').ok_or(error)?;
// {id = 24975 (zsk)
let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?;
if id_lhs.trim() != "{id" {
return Err(error.into());
}
// 24975 (zsk)
let (key_tag, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?;
// size = 1024b}
let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?;
if size_lhs.trim() != "size" {
return Err(error.into());
}
let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?;
Ok(Self {
zone: zone.parse()?,
flags: flags.parse()?,
protocol: protocol.parse()?,
algorithm: algorithm.parse()?,
public_key: public_key.to_string(),
key_tag: key_tag.parse()?,
bits,
})
}
}
impl fmt::Display for DNSKEY {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
flags,
protocol,
algorithm,
public_key,
bits: _,
key_tag: _,
} = self;
write!(
f,
"{zone}\tIN\tDNSKEY\t{flags}\t{protocol}\t{algorithm}\t{public_key}"
)
}
}
#[derive(Clone)]
pub struct DS {
zone: FQDN,
_ttl: u32,
key_tag: u16,
algorithm: u8,
digest_type: u8,
digest: String,
}
impl FromStr for DS {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type), Some(digest), None] =
array::from_fn(|_| columns.next())
else {
return Err("expected 8 columns".into());
};
let expected = "DS";
if record_type != expected {
return Err(
format!("tried to parse `{record_type}` entry as a {expected} entry").into(),
);
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
Ok(Self {
zone: zone.parse()?,
_ttl: ttl.parse()?,
key_tag: key_tag.parse()?,
algorithm: algorithm.parse()?,
digest_type: digest_type.parse()?,
digest: digest.to_string(),
})
}
}
/// NOTE does NOT include the TTL field
impl fmt::Display for DS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
_ttl,
key_tag,
algorithm,
digest_type,
digest,
} = self;
write!(
f,
"{zone}\tIN\tDS\t{key_tag}\t{algorithm}\t{digest_type}\t{digest}"
)
}
}
pub struct NS {
pub zone: FQDN,
pub nameserver: FQDN,
}
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 {
pub zone: FQDN,
pub nameserver: FQDN,
pub admin: FQDN,
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());
fn dnskey() -> Result<()> {
let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}";
let DNSKEY {
zone,
flags,
protocol,
algorithm,
public_key,
} = input.parse()?;
assert_eq!(FQDN::ROOT, zone);
assert_eq!(256, flags);
assert_eq!(3, protocol);
assert_eq!(7, algorithm);
let expected = "AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB";
assert_eq!(expected, public_key);
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(())
}
// not quite roundtrip because we drop the TTL field when doing `to_string`
#[test]
fn ds_roundtrip() -> Result<()> {
let input =
". 1800 IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b";
let ds: DS = input.parse()?;
let output = ds.to_string();
let expected =
". IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b";
assert_eq!(expected, output);
Ok(())
}
#[test]
fn dnskey_roundtrip() -> Result<()> {
let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}";
let dnskey: DNSKEY = input.parse()?;
assert_eq!(256, dnskey.flags);
assert_eq!(3, dnskey.protocol);
assert_eq!(7, dnskey.algorithm);
let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb";
assert_eq!(expected, dnskey.public_key);
assert_eq!(1024, dnskey.bits());
assert_eq!(24975, dnskey.key_tag());
let output = dnskey.to_string();
assert!(input.starts_with(&output));
Ok(())
}
fn example_a() -> Result<A> {
Ok(A {
fqdn: FQDN("e.gtld-servers.net.")?,
ipv4_addr: Ipv4Addr::new(192, 12, 94, 30),
})
}
fn example_ns() -> Result<NS> {
Ok(NS {
zone: FQDN::COM,
nameserver: FQDN("e.gtld-servers.net.")?,
})
}
fn example_soa() -> Result<SOA> {
Ok(SOA {
zone: FQDN::ROOT,
nameserver: FQDN("a.root-servers.net.")?,
admin: FQDN("nstld.verisign-grs.com.")?,
settings: SoaSettings::default(),
})
}
}