diff --git a/src/decode/error.rs b/src/decode/error.rs index dcfe06b..fe46f7e 100644 --- a/src/decode/error.rs +++ b/src/decode/error.rs @@ -83,6 +83,8 @@ pub enum DecodeError { SSHFPAlgorithm(u8), #[error("Could not decode SSHFPType: {0}")] SSHFPType(u8), + #[error("The bitmap length must be between 1 and 32 bytes: {0}")] + NSECBitmapLength(u8), #[error("Could not decode AlgorithmType: {0}")] AlgorithmType(u8), #[error("Could not decode DigestType: {0}")] diff --git a/src/decode/rr/enums.rs b/src/decode/rr/enums.rs index 5494f14..af601be 100644 --- a/src/decode/rr/enums.rs +++ b/src/decode/rr/enums.rs @@ -70,6 +70,7 @@ impl<'a, 'b: 'a> Decoder<'b, 'b> { Type::URI => RR::URI(r_data.rr_uri(header)?), Type::EID => RR::EID(r_data.rr_eid(header)?), Type::NIMLOC => RR::NIMLOC(r_data.rr_nimloc(header)?), + Type::NSEC => RR::NSEC(r_data.rr_nsec(header)?), Type::DNSKEY => RR::DNSKEY(r_data.rr_dnskey(header)?), Type::DS => RR::DS(r_data.rr_ds(header)?), Type::CAA => RR::CAA(r_data.rr_caa(header)?), diff --git a/src/decode/rr/rfc_4034.rs b/src/decode/rr/rfc_4034.rs index 3e0af44..dc5fe8e 100644 --- a/src/decode/rr/rfc_4034.rs +++ b/src/decode/rr/rfc_4034.rs @@ -1,9 +1,11 @@ use super::Header; use crate::rr::{ - AlgorithmType, DigestType, DNSKEY, DNSKEY_ZERO_MASK, DS, SECURE_ENTRY_POINT_FLAG, ZONE_KEY_FLAG, + AlgorithmType, DigestType, Type, DNSKEY, DNSKEY_ZERO_MASK, DS, NSEC, SECURE_ENTRY_POINT_FLAG, + ZONE_KEY_FLAG, }; use crate::DecodeResult; use crate::{decode::Decoder, DecodeError}; +use std::collections::BTreeSet; use std::convert::TryFrom; impl<'a, 'b: 'a> Decoder<'a, 'b> { @@ -66,4 +68,35 @@ impl<'a, 'b: 'a> Decoder<'a, 'b> { }; Ok(ds) } + + pub(super) fn rr_nsec(&mut self, header: Header) -> DecodeResult { + let class = header.get_class()?; + let next_domain_name = self.domain_name()?; + let mut type_bit_maps = BTreeSet::new(); + while self.remaining()? > 0 { + let window = self.u8()?; + let bitmap_length = self.u8()?; + if bitmap_length == 0 || bitmap_length > 32 { + return Err(DecodeError::NSECBitmapLength(bitmap_length)); + } + let bitmap = self.read(bitmap_length as usize)?; + for (index, byte) in bitmap.iter().enumerate() { + for bit in 0..8 { + if byte & (0x80 >> bit) != 0 { + let type_number = u16::from(window) * 256 + (index as u16 * 8 + bit); + let type_ = Type::try_from(type_number).map_err(DecodeError::Type)?; + type_bit_maps.insert(type_); + } + } + } + } + let nsec = NSEC { + domain_name: header.domain_name, + ttl: header.ttl, + class, + next_domain_name, + type_bit_maps, + }; + Ok(nsec) + } } diff --git a/src/encode/domain_name.rs b/src/encode/domain_name.rs index 214f413..3fd925a 100644 --- a/src/encode/domain_name.rs +++ b/src/encode/domain_name.rs @@ -71,6 +71,22 @@ impl Encoder { self.merge_domain_name_index(domain_name_index, 0)?; Ok(()) } + + pub(super) fn domain_name_without_compression( + &mut self, + domain_name: &DomainName, + ) -> EncodeResult<()> { + let mut domain_name_index = HashMap::new(); + for (label, domain_name) in domain_name.iter() { + let index = self.label(&label)?; + if index <= MAX_OFFSET { + domain_name_index.insert(domain_name, index); + } + } + self.string_with_len("")?; + self.merge_domain_name_index(domain_name_index, 0)?; + Ok(()) + } } impl DomainName { diff --git a/src/encode/rr/enums.rs b/src/encode/rr/enums.rs index 5d748a6..33d3bc0 100644 --- a/src/encode/rr/enums.rs +++ b/src/encode/rr/enums.rs @@ -56,6 +56,7 @@ impl Encoder { RR::URI(uri) => self.rr_uri(uri), RR::EID(eid) => self.rr_eid(eid), RR::NIMLOC(nimloc) => self.rr_nimloc(nimloc), + RR::NSEC(nsec) => self.rr_nsec(nsec), RR::DNSKEY(dnskey) => self.rr_dnskey(dnskey), RR::DS(ds) => self.rr_ds(ds), RR::CAA(caa) => self.rr_caa(caa), diff --git a/src/encode/rr/rfc_4034.rs b/src/encode/rr/rfc_4034.rs index 0c99de2..b2b587f 100644 --- a/src/encode/rr/rfc_4034.rs +++ b/src/encode/rr/rfc_4034.rs @@ -1,6 +1,7 @@ use crate::encode::Encoder; -use crate::rr::{AlgorithmType, DigestType, Type, DNSKEY, DS}; +use crate::rr::{AlgorithmType, DigestType, Type, DNSKEY, DS, NSEC}; use crate::EncodeResult; +use std::collections::BTreeMap; impl Encoder { fn rr_algorithm_type(&mut self, algorithm_type: AlgorithmType) { @@ -36,4 +37,39 @@ impl Encoder { self.vec(&ds.digest); self.set_length_index(length_index) } + + pub(super) fn rr_nsec(&mut self, nsec: &NSEC) -> EncodeResult<()> { + self.domain_name(&nsec.domain_name)?; + self.rr_type(&Type::NSEC); + self.rr_class(&nsec.class); + self.u32(nsec.ttl); + let length_index = self.create_length_index(); + self.domain_name_without_compression(&nsec.next_domain_name)?; + + let mut windows: BTreeMap = BTreeMap::new(); + for type_ in &nsec.type_bit_maps { + let type_value = *type_ as u16; + let window = (type_value / 256) as u8; + let offset = (type_value % 256) as u8; + let bitmap = windows.entry(window).or_insert([0u8; 32]); + let byte_index = (offset / 8) as usize; + let bit_index = offset % 8; + bitmap[byte_index] |= 0x80 >> bit_index; + } + + for (window, bitmap) in windows { + let mut length = bitmap.len(); + while length > 0 && bitmap[length - 1] == 0 { + length -= 1; + } + if length == 0 { + continue; + } + self.u8(window); + self.u8(length as u8); + self.vec(&bitmap[..length]); + } + + self.set_length_index(length_index) + } } diff --git a/src/rr/enums.rs b/src/rr/enums.rs index 6cd5713..d081b9b 100644 --- a/src/rr/enums.rs +++ b/src/rr/enums.rs @@ -1,7 +1,7 @@ pub use super::{ A, AAAA, AFSDB, APL, CAA, CNAME, DNAME, DNSKEY, DS, EID, EUI48, EUI64, GPOS, HINFO, ISDN, KX, - L32, L64, LOC, LP, MB, MD, MF, MG, MINFO, MR, MX, NID, NIMLOC, NS, NSAP, NULL, OPT, PTR, PX, - RP, RT, SOA, SRV, SSHFP, TXT, URI, WKS, X25, + L32, L64, LOC, LP, MB, MD, MF, MG, MINFO, MR, MX, NID, NIMLOC, NS, NSAP, NSEC, NULL, OPT, PTR, + PX, RP, RT, SOA, SRV, SSHFP, TXT, URI, WKS, X25, }; use crate::rr::draft_ietf_dnsop_svcb_https::ServiceBinding; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -31,7 +31,7 @@ try_from_enum_to_integer! { /// /// [type]: https://tools.ietf.org/html/rfc1035#section-3.2.2 /// [resource records]: crate::rr::RR - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Type { /// The [IPv4] [host address] type. /// @@ -176,10 +176,10 @@ try_from_enum_to_integer! { /// The [DNSKEY] type. /// /// [DNSKEY]: https://tools.ietf.org/html/rfc4034#section-2 - DNSKEY = 48, - DHCID = 49, - NSEC3 = 50, - NSEC3PARAM = 51, + DNSKEY = 48, + DHCID = 49, + NSEC3 = 50, + NSEC3PARAM = 51, TLSA = 52, SMIMEA = 53, @@ -280,6 +280,7 @@ pub enum RR { EUI48(EUI48), EUI64(EUI64), DS(DS), + NSEC(NSEC), DNSKEY(DNSKEY), CAA(CAA), SVCB(ServiceBinding), @@ -331,6 +332,7 @@ impl RR { RR::URI(uri) => Some(uri.ttl), RR::EID(eid) => Some(eid.ttl), RR::DS(ds) => Some(ds.ttl), + RR::NSEC(nsec) => Some(nsec.ttl), RR::DNSKEY(dnskey) => Some(dnskey.ttl), RR::CAA(caa) => Some(caa.ttl), RR::SVCB(svcb) => Some(svcb.ttl), @@ -382,6 +384,7 @@ impl RR { RR::URI(uri) => Some(uri.class), RR::EID(eid) => Some(eid.class), RR::DS(ds) => Some(ds.class), + RR::NSEC(nsec) => Some(nsec.class), RR::DNSKEY(dnskey) => Some(dnskey.class), RR::CAA(caa) => Some(caa.class), RR::SVCB(_) => Some(Class::IN), @@ -435,6 +438,7 @@ impl Display for RR { RR::URI(uri) => uri.fmt(f), RR::EID(eid) => eid.fmt(f), RR::DS(ds) => ds.fmt(f), + RR::NSEC(nsec) => nsec.fmt(f), RR::DNSKEY(dnskey) => dnskey.fmt(f), RR::CAA(caa) => caa.fmt(f), RR::SVCB(svcb) => svcb.fmt(f), diff --git a/src/rr/mod.rs b/src/rr/mod.rs index 7949bbb..790311b 100644 --- a/src/rr/mod.rs +++ b/src/rr/mod.rs @@ -86,7 +86,8 @@ pub use rfc_3123::{APItem, APL, APL_NEGATION_MASK}; pub use rfc_3596::AAAA; pub use rfc_3658::{SSHFPAlgorithm, SSHFPType, SSHFP}; pub use rfc_4034::{ - AlgorithmType, DigestType, DNSKEY, DNSKEY_ZERO_MASK, DS, SECURE_ENTRY_POINT_FLAG, ZONE_KEY_FLAG, + AlgorithmType, DigestType, DNSKEY, DNSKEY_ZERO_MASK, DS, NSEC, SECURE_ENTRY_POINT_FLAG, + ZONE_KEY_FLAG, }; pub use rfc_6672::DNAME; pub use rfc_6742::{L32, L64, LP, NID}; diff --git a/src/rr/rfc_4034.rs b/src/rr/rfc_4034.rs index 5f58e5d..101e2ab 100644 --- a/src/rr/rfc_4034.rs +++ b/src/rr/rfc_4034.rs @@ -1,6 +1,8 @@ use crate::rr::Class; +use crate::rr::Type; use crate::DomainName; use hex::encode; +use std::collections::BTreeSet; use std::fmt::{Display, Formatter, Result as FmtResult}; /// The bit at offset 7 of the DNSKEY flags field is the [Zone Key flag]. @@ -116,3 +118,30 @@ impl Display for DS { ) } } + +#[derive(Debug, PartialEq, Clone, Eq, Hash)] +pub struct NSEC { + pub domain_name: DomainName, + pub ttl: u32, + pub class: Class, + pub next_domain_name: DomainName, + pub type_bit_maps: BTreeSet, +} + +impl Display for NSEC { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!( + f, + "{} {} {} NSEC {} {}", + self.domain_name, + self.ttl, + self.class, + self.next_domain_name, + self.type_bit_maps + .iter() + .map(|type_| format!("{:?}", type_)) + .collect::>() + .join(" ") + ) + } +} diff --git a/tests/decode_encode_decode.rs b/tests/decode_encode_decode.rs index 43195a7..7b1799a 100644 --- a/tests/decode_encode_decode.rs +++ b/tests/decode_encode_decode.rs @@ -1,5 +1,10 @@ use bytes::Bytes; -use dns_message_parser::{Dns, Flags, Opcode, RCode}; +use dns_message_parser::{ + question::{QClass, QType, Question}, + rr::{Class, Type, NSEC, RR}, + Dns, DomainName, Flags, Opcode, RCode, +}; +use std::collections::BTreeSet; fn decode_msg(msg: &[u8]) -> Dns { // Decode BytesMut to message @@ -17,6 +22,26 @@ fn decode_encode_decode(msg: &[u8]) { assert_eq!(dns_1, dns_2); } +fn skip_name(bytes: &[u8], mut offset: usize) -> usize { + loop { + let length = bytes[offset]; + offset += 1; + + if length & 0xc0 == 0xc0 { + offset += 1; + break; + } + + if length == 0 { + break; + } + + offset += length as usize; + } + + offset +} + #[test] fn flags() { let flags_1 = Flags { @@ -768,3 +793,102 @@ fn example_net_edns_ede_forged() { \xea\x00\x00\x09\x3a\x80\x00\x00\x29\x04\xd0\x00\x00\x00\x00\x00\x06\x00\x0f\x00\x02\x00\x12"; decode_encode_decode(&msg[..]); } + +#[test] +fn nsec_response_roundtrip() { + let domain_name: DomainName = "example.org".parse().unwrap(); + let next_domain_name: DomainName = "ns.example.org".parse().unwrap(); + let flags = Flags { + qr: true, + opcode: Opcode::Query, + aa: true, + tc: false, + rd: false, + ra: false, + ad: false, + cd: false, + rcode: RCode::NoError, + }; + + let dns = Dns { + id: 0x6b6a, + flags, + questions: vec![Question { + domain_name: domain_name.clone(), + q_class: QClass::IN, + q_type: QType::NSEC, + }], + answers: vec![RR::NSEC(NSEC { + domain_name, + ttl: 3600, + class: Class::IN, + next_domain_name, + type_bit_maps: BTreeSet::from([Type::RRSIG, Type::A, Type::MX]), + })], + authorities: Vec::new(), + additionals: Vec::new(), + }; + + let msg = dns.encode().unwrap(); + decode_encode_decode(msg.as_ref()); +} + +#[test] +fn nsec_next_domain_not_compressed() { + let domain_name: DomainName = "example.org".parse().unwrap(); + let next_domain_name: DomainName = "ns.example.org".parse().unwrap(); + let flags = Flags { + qr: true, + opcode: Opcode::Query, + aa: true, + tc: false, + rd: false, + ra: false, + ad: false, + cd: false, + rcode: RCode::NoError, + }; + + let dns = Dns { + id: 0x6b6a, + flags, + questions: vec![Question { + domain_name: domain_name.clone(), + q_class: QClass::IN, + q_type: QType::NSEC, + }], + answers: vec![RR::NSEC(NSEC { + domain_name, + ttl: 3600, + class: Class::IN, + next_domain_name: next_domain_name.clone(), + type_bit_maps: BTreeSet::from([Type::RRSIG, Type::A, Type::MX]), + })], + authorities: Vec::new(), + additionals: Vec::new(), + }; + + let msg = dns.encode().unwrap(); + + let mut offset = 12; // header + offset = skip_name(msg.as_ref(), offset); // question name + offset += 4; // qtype + qclass + + offset = skip_name(msg.as_ref(), offset); // answer name + offset += 2; // type + offset += 2; // class + offset += 4; // ttl + + let rdlength = u16::from_be_bytes([msg[offset], msg[offset + 1]]) as usize; + offset += 2; + + let expected_next_domain = next_domain_name.encode().unwrap(); + assert!(msg.len() >= offset + expected_next_domain.len()); + assert_ne!(msg[offset] & 0xc0, 0xc0); + assert_eq!( + &msg[offset..offset + expected_next_domain.len()], + expected_next_domain.as_ref() + ); + + assert!(rdlength >= expected_next_domain.len()); +} diff --git a/tests/display.rs b/tests/display.rs index 7e4dfe8..c169ea0 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -6,10 +6,10 @@ use dns_message_parser::{ ExtendedDNSErrors, Padding, ECS, }, APItem, Address, AlgorithmType, Class, DigestType, ISDNAddress, PSDNAddress, - SSHFPAlgorithm, SSHFPType, ServiceBinding, ServiceParameter, Tag, A, AAAA, APL, CAA, CNAME, - DNAME, DNSKEY, DS, EID, EUI48, EUI64, GPOS, HINFO, ISDN, KX, L32, L64, LP, MB, MD, MF, MG, - MINFO, MR, MX, NID, NIMLOC, NS, OPT, PTR, PX, RP, RR, RT, SA, SOA, SRV, SSHFP, TXT, URI, - X25, + SSHFPAlgorithm, SSHFPType, ServiceBinding, ServiceParameter, Tag, Type, A, AAAA, APL, CAA, + CNAME, DNAME, DNSKEY, DS, EID, EUI48, EUI64, GPOS, HINFO, ISDN, KX, L32, L64, LP, MB, MD, + MF, MG, MINFO, MR, MX, NID, NIMLOC, NS, NSEC, OPT, PTR, PX, RP, RR, RT, SA, SOA, SRV, + SSHFP, TXT, URI, X25, }, Dns, Flags, Opcode, RCode, }; @@ -757,6 +757,20 @@ fn rr_ds() { ); } +#[test] +fn rr_nsec() { + let domain_name = "example.org".parse().unwrap(); + let next_domain_name = "ns.example.org".parse().unwrap(); + let rr = RR::NSEC(NSEC { + domain_name, + ttl: 3600, + class: Class::IN, + next_domain_name, + type_bit_maps: BTreeSet::from([Type::A, Type::MX, Type::RRSIG]), + }); + check_output(&rr, "example.org. 3600 IN NSEC ns.example.org. A MX RRSIG"); +} + #[test] fn rr_caa() { let domain_name = "caa.example.org".parse().unwrap();