parse more record types
This commit is contained in:
parent
66d6061ffc
commit
57a1fc9231
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -18,3 +18,4 @@ doctest = false
|
|||
|
||||
[dev-dependencies]
|
||||
ctrlc = "3.4.2"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
11
packages/dns-test/src/zone_file/muster.zone
Normal file
11
packages/dns-test/src/zone_file/muster.zone
Normal 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=
|
Loading…
Reference in New Issue
Block a user