Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add NSEC record to debug resolve issue #183

Merged
merged 6 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 144 additions & 3 deletions src/dns_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub(crate) const TYPE_HINFO: u16 = 13;
pub(crate) const TYPE_TXT: u16 = 16;
pub(crate) const TYPE_AAAA: u16 = 28; // IPv6 address
pub(crate) const TYPE_SRV: u16 = 33;
pub(crate) const TYPE_NSEC: u16 = 47; // Negative responses
pub(crate) const TYPE_ANY: u16 = 255;

pub(crate) const CLASS_IN: u16 = 1;
Expand Down Expand Up @@ -424,6 +425,85 @@ impl DnsRecordExt for DnsHostInfo {
}
}

/// Record for negative responses
///
/// [RFC4034 section 4.1](https://datatracker.ietf.org/doc/html/rfc4034#section-4.1)
/// and
/// [RFC6762 section 6.1](https://datatracker.ietf.org/doc/html/rfc6762#section-6.1)
#[derive(Debug)]
pub(crate) struct DnsNSec {
record: DnsRecord,
next_domain: String,
type_bitmap: Vec<u8>,
}

impl DnsNSec {
fn new(name: &str, class: u16, ttl: u32, next_domain: String, type_bitmap: Vec<u8>) -> Self {
let record = DnsRecord::new(name, TYPE_NSEC, class, ttl);
Self {
record,
next_domain,
type_bitmap,
}
}

/// Returns the types marked by `type_bitmap`
pub(crate) fn _types(&self) -> Vec<u16> {
// From RFC 4034: 4.1.2 The Type Bit Maps Field
// https://datatracker.ietf.org/doc/html/rfc4034#section-4.1.2
//
// Each bitmap encodes the low-order 8 bits of RR types within the
// window block, in network bit order. The first bit is bit 0. For
// window block 0, bit 1 corresponds to RR type 1 (A), bit 2 corresponds
// to RR type 2 (NS), and so forth.

let mut bit_num = 0;
let mut results = Vec::new();

for byte in self.type_bitmap.iter() {
let mut bit_mask: u8 = 0x80; // for bit 0 in network bit order

// check every bit in this byte, one by one.
for _ in 0..8 {
if (byte & bit_mask) != 0 {
results.push(bit_num);
}
bit_num += 1;
bit_mask >>= 1; // mask for the next bit
}
}
results
}
}

impl DnsRecordExt for DnsNSec {
fn get_record(&self) -> &DnsRecord {
&self.record
}

fn get_record_mut(&mut self) -> &mut DnsRecord {
&mut self.record
}

fn write(&self, packet: &mut DnsOutPacket) {
packet.write_bytes(self.next_domain.as_bytes());
packet.write_bytes(&self.type_bitmap);
}

fn any(&self) -> &dyn Any {
self
}

fn matches(&self, other: &dyn DnsRecordExt) -> bool {
if let Some(other_record) = other.any().downcast_ref::<DnsNSec>() {
return self.next_domain == other_record.next_domain
&& self.type_bitmap == other_record.type_bitmap
&& self.record.entry == other_record.record.entry;
}
false
}
}

#[derive(PartialEq)]
enum PacketState {
Init = 0,
Expand Down Expand Up @@ -1022,8 +1102,15 @@ impl DnsIncoming {
ttl,
self.read_ipv6().into(),
))),
_ => {
debug!("Unknown DNS record type");
TYPE_NSEC => Some(Box::new(DnsNSec::new(
&name,
class,
ttl,
self.read_name()?,
self.read_type_bitmap()?,
))),
x => {
debug!("Unknown DNS record type: {} name: {}", x, &name);
self.offset += length;
None
}
Expand Down Expand Up @@ -1059,6 +1146,46 @@ impl DnsIncoming {
num
}

/// Reads the "Type Bit Map" block for a DNS NSEC record.
fn read_type_bitmap(&mut self) -> Result<Vec<u8>> {
// From RFC 6762: 6.1. Negative Responses
// https://datatracker.ietf.org/doc/html/rfc6762#section-6.1
// o The Type Bit Map block number is 0.
// o The Type Bit Map block length byte is a value in the range 1-32.
// o The Type Bit Map data is 1-32 bytes, as indicated by length
// byte.
let block_num = self.data[self.offset];
self.offset += 1;
if block_num != 0 {
return Err(Error::Msg(format!(
"NSEC block number is not 0: {}",
block_num
)));
}

let block_len = self.data[self.offset] as usize;
if !(1..=32).contains(&block_len) {
return Err(Error::Msg(format!(
"NSEC block length must be in the range 1-32: {}",
block_len
)));
}
self.offset += 1;

let end = self.offset + block_len;
if end > self.data.len() {
return Err(Error::Msg(format!(
"NSEC block overflow: {} over RData len {}",
end,
self.data.len()
)));
}
let bitmap = self.data[self.offset..end].to_vec();
self.offset += block_len;

Ok(bitmap)
}

fn read_vec(&mut self, length: usize) -> Vec<u8> {
let v = self.data[self.offset..self.offset + length].to_vec();
self.offset += length;
Expand Down Expand Up @@ -1201,8 +1328,10 @@ fn get_expiration_time(created: u64, ttl: u32, percent: u32) -> u64 {

#[cfg(test)]
mod tests {
use crate::dns_parser::{TYPE_A, TYPE_AAAA};

use super::{
DnsIncoming, DnsOutgoing, DnsSrv, CLASS_IN, CLASS_UNIQUE, FLAGS_QR_QUERY,
DnsIncoming, DnsNSec, DnsOutgoing, DnsSrv, CLASS_IN, CLASS_UNIQUE, FLAGS_QR_QUERY,
FLAGS_QR_RESPONSE, TYPE_PTR,
};

Expand Down Expand Up @@ -1272,4 +1401,16 @@ mod tests {
println!("error: {}", e);
}
}

#[test]
fn test_dns_nsec() {
let name = "instance1._nsec_test._udp.local.";
let next_domain = name.to_string();
let type_bitmap = vec![64, 0, 0, 8]; // Two bits set to '1': bit 1 and bit 28.
let nsec = DnsNSec::new(name, CLASS_IN | CLASS_UNIQUE, 1, next_domain, type_bitmap);
let absent_types = nsec._types();
assert_eq!(absent_types.len(), 2);
assert_eq!(absent_types[0], TYPE_A);
assert_eq!(absent_types[1], TYPE_AAAA);
}
}
12 changes: 10 additions & 2 deletions src/service_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ use crate::{
dns_parser::{
current_time_millis, DnsAddress, DnsIncoming, DnsOutgoing, DnsPointer, DnsRecordBox,
DnsRecordExt, DnsSrv, DnsTxt, CLASS_IN, CLASS_UNIQUE, FLAGS_AA, FLAGS_QR_QUERY,
FLAGS_QR_RESPONSE, MAX_MSG_ABSOLUTE, TYPE_A, TYPE_AAAA, TYPE_ANY, TYPE_PTR, TYPE_SRV,
TYPE_TXT,
FLAGS_QR_RESPONSE, MAX_MSG_ABSOLUTE, TYPE_A, TYPE_AAAA, TYPE_ANY, TYPE_NSEC, TYPE_PTR,
TYPE_SRV, TYPE_TXT,
},
error::{Error, Result},
service_info::{ifaddr_subnet, split_sub_domain, ServiceInfo},
Expand Down Expand Up @@ -2005,6 +2005,10 @@ struct DnsCache {

/// A reverse lookup table from "instance fullname" to "subtype PTR name"
subtype: HashMap<String, String>,

/// Negative responses:
/// A map from "instance fullname" to DnsNSec.
nsec: HashMap<String, Vec<DnsRecordBox>>,
}

impl DnsCache {
Expand All @@ -2015,6 +2019,7 @@ impl DnsCache {
txt: HashMap::new(),
addr: HashMap::new(),
subtype: HashMap::new(),
nsec: HashMap::new(),
}
}

Expand Down Expand Up @@ -2055,15 +2060,18 @@ impl DnsCache {
}
}

// get the existing records for the type.
let record_vec = match incoming.get_type() {
TYPE_PTR => self.ptr.entry(entry_name).or_default(),
TYPE_SRV => self.srv.entry(entry_name).or_default(),
TYPE_TXT => self.txt.entry(entry_name).or_default(),
TYPE_A => self.addr.entry(entry_name).or_default(),
TYPE_AAAA => self.addr.entry(entry_name).or_default(),
TYPE_NSEC => self.nsec.entry(entry_name).or_default(),
_ => return None,
};

// update TTL for existing record or create a new record.
let (idx, updated) = match record_vec
.iter_mut()
.enumerate()
Expand Down
13 changes: 11 additions & 2 deletions src/service_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl ServiceInfo {
let fullname = format!("{}.{}", my_name, ty_domain);
let ty_domain = ty_domain.to_string();
let sub_domain = sub_domain.map(str::to_string);
let server = host_name.to_string();
let server = normalize_hostname(host_name.to_string());
let addresses = ip.as_ip_addrs()?;
let txt_properties = properties.into_txt_properties();

Expand Down Expand Up @@ -285,7 +285,7 @@ impl ServiceInfo {
}

pub(crate) fn set_hostname(&mut self, hostname: String) {
self.server = hostname;
self.server = normalize_hostname(hostname);
}

/// Returns true if properties are updated.
Expand All @@ -304,6 +304,15 @@ impl ServiceInfo {
}
}

/// Removes potentially duplicated ".local." at the end of "hostname".
fn normalize_hostname(mut hostname: String) -> String {
if hostname.ends_with(".local.local.") {
let new_len = hostname.len() - "local.".len();
hostname.truncate(new_len);
}
hostname
}

/// This trait allows for parsing an input into a set of one or multiple [`Ipv4Addr`].
pub trait AsIpAddrs {
fn as_ip_addrs(&self) -> Result<HashSet<IpAddr>>;
Expand Down