parse more record types

This commit is contained in:
Jorge Aparicio 2024-02-20 15:10:10 +01:00
parent 66d6061ffc
commit 57a1fc9231
5 changed files with 497 additions and 123 deletions

23
Cargo.lock generated
View File

@ -143,12 +143,19 @@ dependencies = [
"serde",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "dns-test"
version = "0.1.0"
dependencies = [
"ctrlc",
"minijinja",
"pretty_assertions",
"serde",
"serde_json",
"serde_with",
@ -358,6 +365,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
@ -701,3 +718,9 @@ name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

View File

@ -18,3 +18,4 @@ doctest = false
[dev-dependencies]
ctrlc = "3.4.2"
pretty_assertions = "1.4.0"

View File

@ -3,72 +3,77 @@
use core::result::Result as CoreResult;
use core::str::FromStr;
use core::{array, fmt};
use std::any;
use std::fmt::Write;
use std::net::Ipv4Addr;
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,
}
const CLASS: &str = "IN"; // "internet"
impl RecordType {
pub fn as_str(&self) -> &'static str {
match self {
RecordType::A => "A",
RecordType::DS => "DS",
RecordType::SOA => "SOA",
RecordType::NS => "NS",
macro_rules! record_types {
($($variant:ident),*) => {
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, PartialEq)]
pub enum RecordType {
$($variant),*
}
}
impl RecordType {
pub fn as_str(&self) -> &'static str {
match self {
$(Self::$variant => stringify!($variant)),*
}
}
}
impl FromStr for RecordType {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
$(if input == stringify!($variant) {
return Ok(Self::$variant);
})*
Err(format!("unknown record type: {input}").into())
}
}
impl fmt::Display for RecordType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
};
}
impl FromStr for RecordType {
type Err = Error;
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()),
};
Ok(record_type)
}
}
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)
}
}
record_types!(A, AAAA, DNSKEY, DS, MX, NS, NSEC3, NSEC3PARAM, RRSIG, SOA, TXT);
#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub enum Record {
A(A),
DNSKEY(DNSKEY),
DS(DS),
NS(NS),
NSEC3(NSEC3),
NSEC3PARAM(NSEC3PARAM),
RRSIG(RRSIG),
SOA(SOA),
}
impl From<NSEC3> for Record {
fn from(v: NSEC3) -> Self {
Self::NSEC3(v)
}
}
impl From<DNSKEY> for Record {
fn from(v: DNSKEY) -> Self {
Self::DNSKEY(v)
}
}
impl From<DS> for Record {
fn from(v: DS) -> Self {
Self::DS(v)
@ -150,7 +155,11 @@ impl FromStr for Record {
let record = match record_type {
"A" => Record::A(input.parse()?),
"NS" => todo!(),
"DNSKEY" => Record::DNSKEY(input.parse()?),
"DS" => Record::DS(input.parse()?),
"NS" => Record::NS(input.parse()?),
"NSEC3" => Record::NSEC3(input.parse()?),
"NSEC3PARAM" => Record::NSEC3PARAM(input.parse()?),
"RRSIG" => Record::RRSIG(input.parse()?),
"SOA" => Record::SOA(input.parse()?),
_ => return Err(format!("unknown record type: {record_type}").into()),
@ -165,7 +174,10 @@ impl fmt::Display for Record {
match self {
Record::A(a) => write!(f, "{a}"),
Record::DS(ds) => write!(f, "{ds}"),
Record::DNSKEY(dnskey) => write!(f, "{dnskey}"),
Record::NS(ns) => write!(f, "{ns}"),
Record::NSEC3(nsec3) => write!(f, "{nsec3}"),
Record::NSEC3PARAM(nsec3param) => write!(f, "{nsec3param}"),
Record::RRSIG(rrsig) => write!(f, "{rrsig}"),
Record::SOA(soa) => write!(f, "{soa}"),
}
@ -191,16 +203,8 @@ impl FromStr for A {
return Err("expected 5 columns".into());
};
let expected = "A";
if record_type != expected {
return Err(
format!("tried to parse `{record_type}` record as an {expected} record").into(),
);
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
check_record_type::<Self>(record_type)?;
check_class(class)?;
Ok(Self {
fqdn: fqdn.parse()?,
@ -218,7 +222,8 @@ impl fmt::Display for A {
ipv4_addr,
} = self;
write!(f, "{fqdn}\t{ttl}\tIN\tA\t{ipv4_addr}")
let record_type = unqualified_type_name::<Self>();
write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{ipv4_addr}")
}
}
@ -252,7 +257,11 @@ impl DNSKEY {
impl FromStr for DNSKEY {
type Err = Error;
fn from_str(input: &str) -> CoreResult<Self, Self::Err> {
fn from_str(mut input: &str) -> Result<Self> {
if let Some((rr, _comment)) = input.rsplit_once(" ;") {
input = rr.trim_end();
}
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm)] =
@ -261,13 +270,8 @@ impl FromStr for DNSKEY {
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());
}
check_record_type::<Self>(record_type)?;
check_class(class)?;
let mut public_key = String::new();
for column in columns {
@ -296,9 +300,10 @@ impl fmt::Display for DNSKEY {
public_key,
} = self;
let record_type = unqualified_type_name::<Self>();
write!(
f,
"{zone}\t{ttl}\tIN\tDNSKEY\t{flags} {protocol} {algorithm}"
"{zone}\t{ttl}\t{CLASS}\t{record_type}\t{flags} {protocol} {algorithm}"
)?;
write_split_long_string(f, public_key)
@ -318,7 +323,7 @@ pub struct DS {
impl FromStr for DS {
type Err = Error;
fn from_str(input: &str) -> CoreResult<Self, Self::Err> {
fn from_str(input: &str) -> Result<Self> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type)] =
@ -327,16 +332,8 @@ impl FromStr for DS {
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());
}
check_record_type::<Self>(record_type)?;
check_class(class)?;
let mut digest = String::new();
for column in columns {
@ -365,9 +362,10 @@ impl fmt::Display for DS {
digest,
} = self;
let record_type = unqualified_type_name::<Self>();
write!(
f,
"{zone}\t{ttl}\tIN\tDS\t{key_tag} {algorithm} {digest_type}"
"{zone}\t{ttl}\t{CLASS}\t{record_type}\t{key_tag} {algorithm} {digest_type}"
)?;
write_split_long_string(f, digest)
@ -389,7 +387,158 @@ impl fmt::Display for NS {
nameserver,
} = self;
write!(f, "{zone}\t{ttl}\tIN\tNS {nameserver}")
let record_type = unqualified_type_name::<Self>();
write!(f, "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{nameserver}")
}
}
impl FromStr for NS {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(nameserver), None] =
array::from_fn(|_| columns.next())
else {
return Err("expected 5 columns".into());
};
check_record_type::<Self>(record_type)?;
check_class(class)?;
Ok(Self {
zone: zone.parse()?,
ttl: ttl.parse()?,
nameserver: nameserver.parse()?,
})
}
}
// integer types chosen based on bit sizes in section 3.2 of RFC5155
#[derive(Debug)]
pub struct NSEC3 {
pub fqdn: FQDN,
pub ttl: u32,
pub hash_alg: u8,
pub flags: u8,
pub iterations: u16,
pub salt: String,
pub next_hashed_owner_name: String,
pub record_types: Vec<RecordType>,
}
impl FromStr for NSEC3 {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut columns = input.split_whitespace();
let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(hash_alg), Some(flags), Some(iterations), Some(salt), Some(next_hashed_owner_name)] =
array::from_fn(|_| columns.next())
else {
return Err("expected at least 9 columns".into());
};
check_record_type::<Self>(record_type)?;
check_class(class)?;
let mut record_types = vec![];
for column in columns {
record_types.push(column.parse()?);
}
Ok(Self {
fqdn: fqdn.parse()?,
ttl: ttl.parse()?,
hash_alg: hash_alg.parse()?,
flags: flags.parse()?,
iterations: iterations.parse()?,
salt: salt.to_string(),
next_hashed_owner_name: next_hashed_owner_name.to_string(),
record_types,
})
}
}
impl fmt::Display for NSEC3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
fqdn,
ttl,
hash_alg,
flags,
iterations,
salt,
next_hashed_owner_name,
record_types,
} = self;
let record_type = unqualified_type_name::<Self>();
write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} {salt} {next_hashed_owner_name}")?;
for record_type in record_types {
write!(f, " {record_type}")?;
}
Ok(())
}
}
// integer types chosen based on bit sizes in section 4.2 of RFC5155
#[derive(Debug)]
pub struct NSEC3PARAM {
pub zone: FQDN,
pub ttl: u32,
pub hash_alg: u8,
pub flags: u8,
pub iterations: u16,
}
impl FromStr for NSEC3PARAM {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut columns = input.split_whitespace();
let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(hash_alg), Some(flags), Some(iterations), Some(dash), None] =
array::from_fn(|_| columns.next())
else {
return Err("expected 8 columns".into());
};
check_record_type::<Self>(record_type)?;
check_class(class)?;
if dash != "-" {
todo!("salt is not implemented")
}
Ok(Self {
zone: zone.parse()?,
ttl: ttl.parse()?,
hash_alg: hash_alg.parse()?,
flags: flags.parse()?,
iterations: iterations.parse()?,
})
}
}
impl fmt::Display for NSEC3PARAM {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
zone,
ttl,
hash_alg,
flags,
iterations,
} = self;
let record_type = unqualified_type_name::<Self>();
write!(
f,
"{zone}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} -"
)
}
}
@ -422,16 +571,8 @@ impl FromStr for RRSIG {
return Err("expected at least 12 columns".into());
};
let expected = "RRSIG";
if record_type != expected {
return Err(
format!("tried to parse `{record_type}` record as a {expected} record").into(),
);
}
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
check_record_type::<Self>(record_type)?;
check_class(class)?;
let mut signature = String::new();
for column in columns {
@ -470,7 +611,8 @@ impl fmt::Display for RRSIG {
signature,
} = self;
write!(f, "{fqdn}\t{ttl}\tIN\tRRSIG\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?;
let record_type = unqualified_type_name::<Self>();
write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?;
write_split_long_string(f, signature)
}
@ -498,13 +640,8 @@ impl FromStr for SOA {
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());
}
check_record_type::<Self>(record_type)?;
check_class(class)?;
Ok(Self {
zone: zone.parse()?,
@ -532,7 +669,11 @@ impl fmt::Display for SOA {
settings,
} = self;
write!(f, "{zone}\t{ttl}\tIN\tSOA\t{nameserver} {admin} {settings}")
let record_type = unqualified_type_name::<Self>();
write!(
f,
"{zone}\t{ttl}\t{CLASS}\t{record_type}\t{nameserver} {admin} {settings}"
)
}
}
@ -571,6 +712,32 @@ impl fmt::Display for SoaSettings {
}
}
fn check_class(class: &str) -> Result<()> {
if class != "IN" {
return Err(format!("unknown class: {class}").into());
}
Ok(())
}
fn check_record_type<T>(record_type: &str) -> Result<()> {
let expected = unqualified_type_name::<T>();
if record_type == expected {
Ok(())
} else {
Err(format!("tried to parse `{record_type}` record as an {expected} record").into())
}
}
fn unqualified_type_name<T>() -> &'static str {
let name = any::type_name::<T>();
if let Some((_rest, component)) = name.rsplit_once(':') {
component
} else {
name
}
}
fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Result {
for (index, c) in field.chars().enumerate() {
if index % 56 == 0 {
@ -585,31 +752,34 @@ fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Resu
mod tests {
use super::*;
use pretty_assertions::assert_eq;
// dig A a.root-servers.net
const A_INPUT: &str = "a.root-servers.net. 77859 IN A 198.41.0.4";
#[test]
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()?;
} = &A_INPUT.parse()?;
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);
assert_eq!(A_INPUT, output);
Ok(())
}
// dig DNSKEY .
const DNSKEY_INPUT: &str = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU=";
#[test]
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,
@ -617,7 +787,7 @@ mod tests {
protocol,
algorithm,
public_key,
} = &input.parse()?;
} = &DNSKEY_INPUT.parse()?;
assert_eq!(FQDN::ROOT, *zone);
assert_eq!(1116, *ttl);
@ -628,16 +798,30 @@ mod tests {
assert_eq!(expected, public_key);
let output = dnskey.to_string();
assert_eq!(output, input);
assert_eq!(DNSKEY_INPUT, output);
Ok(())
}
#[test]
fn ds() -> Result<()> {
// dig DS com.
let input = "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A";
fn parsing_dnskey_ignores_trailing_comment() -> Result<()> {
// `ldns-signzone`'s output
const DNSKEY_INPUT2: &str = ". 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15 ;{id = 11387 (zsk), size = 1024b}";
let DNSKEY { public_key, .. } = DNSKEY_INPUT2.parse()?;
let expected = "AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15";
assert_eq!(expected, public_key);
Ok(())
}
// dig DS com.
const DS_INPUT: &str =
"com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A";
#[test]
fn ds() -> Result<()> {
let ds @ DS {
zone,
ttl,
@ -645,7 +829,7 @@ mod tests {
algorithm,
digest_type,
digest,
} = &input.parse()?;
} = &DS_INPUT.parse()?;
assert_eq!(FQDN::COM, *zone);
assert_eq!(7612, *ttl);
@ -656,16 +840,109 @@ mod tests {
assert_eq!(expected, digest);
let output = ds.to_string();
assert_eq!(output, input);
assert_eq!(DS_INPUT, output);
Ok(())
}
// dig NS .
const NS_INPUT: &str = ". 86400 IN NS f.root-servers.net.";
#[test]
fn ns() -> Result<()> {
let ns @ NS {
zone,
ttl,
nameserver,
} = &NS_INPUT.parse()?;
assert_eq!(FQDN::ROOT, *zone);
assert_eq!(86400, *ttl);
assert_eq!("f.root-servers.net.", nameserver.as_str());
let output = ns.to_string();
assert_eq!(NS_INPUT, output);
Ok(())
}
// dig +dnssec A unicorn.example.com.
const NSEC3_INPUT: &str = "abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com. 3571 IN NSEC3 1 0 5 53BCBC5805D2B761 GVPMD82B8ER38VUEGP72I721LIH19RGR A NS SOA MX TXT AAAA RRSIG DNSKEY NSEC3PARAM";
#[test]
fn nsec3() -> Result<()> {
let nsec3 @ NSEC3 {
fqdn,
ttl,
hash_alg,
flags,
iterations,
salt,
next_hashed_owner_name,
record_types,
} = &NSEC3_INPUT.parse()?;
assert_eq!(
"abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com.",
fqdn.as_str()
);
assert_eq!(3571, *ttl);
assert_eq!(1, *hash_alg);
assert_eq!(0, *flags);
assert_eq!(5, *iterations);
assert_eq!("53BCBC5805D2B761", salt);
assert_eq!("GVPMD82B8ER38VUEGP72I721LIH19RGR", next_hashed_owner_name);
assert_eq!(
[
RecordType::A,
RecordType::NS,
RecordType::SOA,
RecordType::MX,
RecordType::TXT,
RecordType::AAAA,
RecordType::RRSIG,
RecordType::DNSKEY,
RecordType::NSEC3PARAM
],
record_types.as_slice()
);
let output = nsec3.to_string();
assert_eq!(NSEC3_INPUT, output);
Ok(())
}
// dig NSEC3PARAM com.
const NSEC3PARAM_INPUT: &str = "com. 86238 IN NSEC3PARAM 1 0 0 -";
#[test]
fn nsec3param() -> Result<()> {
let nsec3param @ NSEC3PARAM {
zone,
ttl,
hash_alg,
flags,
iterations,
} = &NSEC3PARAM_INPUT.parse()?;
assert_eq!(FQDN::COM, *zone);
assert_eq!(86238, *ttl);
assert_eq!(1, *hash_alg);
assert_eq!(0, *flags);
assert_eq!(0, *iterations);
let output = nsec3param.to_string();
assert_eq!(NSEC3PARAM_INPUT, output);
Ok(())
}
// dig +dnssec SOA .
const RRSIG_INPUT: &str = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY=";
#[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,
@ -678,7 +955,7 @@ mod tests {
key_tag,
signer_name,
signature,
} = &input.parse()?;
} = &RRSIG_INPUT.parse()?;
assert_eq!(FQDN::ROOT, *fqdn);
assert_eq!(1800, *ttl);
@ -694,17 +971,18 @@ mod tests {
assert_eq!(expected, signature);
let output = rrsig.to_string();
assert_eq!(input, output);
assert_eq!(RRSIG_INPUT, output);
Ok(())
}
// dig SOA .
const SOA_INPUT: &str =
". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400";
#[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()?;
let soa: SOA = SOA_INPUT.parse()?;
assert_eq!(".", soa.zone.as_str());
assert_eq!(15633, soa.ttl);
@ -718,7 +996,21 @@ mod tests {
assert_eq!(86400, settings.minimum);
let output = soa.to_string();
assert_eq!(output, input);
assert_eq!(SOA_INPUT, output);
Ok(())
}
#[test]
fn any() -> Result<()> {
assert!(matches!(A_INPUT.parse()?, Record::A(..)));
assert!(matches!(DNSKEY_INPUT.parse()?, Record::DNSKEY(..)));
assert!(matches!(DS_INPUT.parse()?, Record::DS(..)));
assert!(matches!(NS_INPUT.parse()?, Record::NS(..)));
assert!(matches!(NSEC3_INPUT.parse()?, Record::NSEC3(..)));
assert!(matches!(NSEC3PARAM_INPUT.parse()?, Record::NSEC3PARAM(..)));
assert!(matches!(RRSIG_INPUT.parse()?, Record::RRSIG(..)));
assert!(matches!(SOA_INPUT.parse()?, Record::SOA(..)));
Ok(())
}

View File

@ -57,6 +57,40 @@ impl fmt::Display for ZoneFile {
}
}
impl FromStr for ZoneFile {
type Err = Error;
fn from_str(input: &str) -> Result<Self> {
let mut records = vec![];
let mut maybe_soa = None;
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let record: Record = line.parse()?;
if let Record::SOA(soa) = record {
if maybe_soa.is_some() {
return Err("found more than one SOA record".into());
}
maybe_soa = Some(soa);
} else {
records.push(record)
}
}
let soa = maybe_soa.ok_or("no SOA record found in zone file")?;
Ok(Self {
origin: soa.zone.clone(),
soa,
records,
})
}
}
/// A root (server) hint
pub struct Root {
pub ipv4_addr: Ipv4Addr,
@ -154,6 +188,8 @@ impl FromStr for DNSKEY {
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn dnskey() -> Result<()> {
let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}";
@ -175,4 +211,15 @@ mod tests {
Ok(())
}
#[test]
fn roundtrip() -> Result<()> {
// `ldns-signzone`'s output minus trailing comments; long trailing fields have been split as well
let input = include_str!("muster.zone");
let zone: ZoneFile = input.parse()?;
let output = zone.to_string();
assert_eq!(input, output);
Ok(())
}
}

View File

@ -0,0 +1,11 @@
. 86400 IN SOA primary0.nameservers.com. admin0.nameservers.com. 2024022028 1800 900 604800 86400
. 86400 IN RRSIG SOA 7 0 86400 20240319104519 20240220104519 11387 . Ks9b5tMyNxxrvw3JkgGkR2H5NPqTDwAwmwh3B7iNC0UHAYGU4B01ZJHj DIsJqDoJ2hsKG5oq0hQuwBSKv2nSBA1oSQcNrBDzOk105gu6tsXg2O8V ZCpAtEColco5ziOX8AWRqRMM5adSfA4xyj5H3NToMjRVDLpVpZsU4BAa 4dU=
. 86400 IN NS primary0.nameservers.com.
. 86400 IN RRSIG NS 7 0 86400 20240319104519 20240220104519 11387 . rZpACeVX3m2CwI/gY/rVYNOAs6ge4h+M74yV+CoAZYJaJLjeHd+jY0YV ixU3hap9bbFCZqhXKU5WSpJSsc/9PrgxEt2XycpbvAJwvIwdqWLUW741 /AOwnyrgv+7PLp4vkDdeLI9tcsY5V/ABpQrYW2i8Gtz90OEpvEEd5+4C LyU=
. 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQ iakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJP QtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nu hfWE4w15
. 86400 IN DNSKEY 257 3 7 AwEAAco2Ck4XM5M4RO+QiwZhMFW9Hf8s0cOWH6QZ8OUQisjP6n+gYsbE pOOHhRiABN+QuVhRK9BN+Mt0LqMSBjSy53t5P3NerckqUTQ4HlZkn2QK bhc+TOgvHN5iDj0RBMkTaJ09y5vYmeNv5npFk6hV+VsbBoFRLXTuPSms 8LsH72W6y1HEHNzvAd5H3ro1d2awp66CXRTOcbXbFAIELpTgAU6ZJjEo RBMASZ3Ug4oZ96yvegy2OZnAyFsxBGdOvecs+zoYKeezqaq21YMpnZkf eE7RYexGPm1p8/7smQjBph/uoVDp5k5DuPkTmzpafVOn2YHGB395vT37 uLi9B5Oef9c=
. 86400 IN RRSIG DNSKEY 7 0 86400 20240319104519 20240220104519 11245 . yH/aEcWQhgfmf8RjByMYDDuglaquWsHECA+nRmedIA4Kz7Vc74f77JLi QrhvFFSIFkQNyixNsTugLmTZunphbLrbQNKTWw8gpgd/8u6Oc9OdTYJu T+ADL+Rrgge7mDkPjDRKNhQ6VkIiRzwLBFhoYTA1LZF98CAnJGQcpw4W 1YCkqPbXIzsa3hq2OajC8NzZMEgeI95N1CJ/o5AmhwLtWVuv04q+seGX roSiTlWIQRKGsbCR2v97UjMG4l9XIbijzZbY4dK2/4WrCIw9mjp/cSE8 r/AdfegTi1oqOM9i4QebKvyU9c3rnJRVbFMhEXL1e0M/5bZNytXp43ex VTTHcA==
. 3600 IN NSEC3PARAM 1 0 1 -
. 3600 IN RRSIG NSEC3PARAM 7 0 3600 20240319104519 20240220104519 11387 . IhM+g5s6DwlFKbj6+zd+f/CqN1I4/QtF0aTOMvf0c+s5l+emx/yZEVCT 8LdX4cmz72eYeC4w/dM2btrhhHohhb/hdK1v7ukxtBVgvk6pOmuye2/E cuGkll7B59l+wlRaSmeXAQjiCUX6gyg9tlvmtcnomWVgtjIgMKJpggy8 B6k=
fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN NSEC3 1 1 1 - fasdp12mo9fh69ahu5bseugoh3np33tc NS SOA RRSIG DNSKEY NSEC3PARAM
fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN RRSIG NSEC3 7 1 86400 20240319104519 20240220104519 11387 . dsdwsTOGL5BvrC1v/5bmDy5Bz8wnN/IG3XRAg6RqKVMK0fLPMsd5uhXm U2gPJ5xUg1RkBQ5+etlBRm2p7vSDjbMa/hjRbvUJgP+c4dL68g+FcHv4 v9fb1Jaao9Goy/ZxZ1dbwXAdxhi+pyvikCdNcKsdiCtFD9pX7V5Nh2Cc GQQ=