Skip to content

Commit

Permalink
feat: honor cache flush
Browse files Browse the repository at this point in the history
According to RFC 6762, when the cache-flush bit is set on a RR, all previously
cache entries should be set to expire 1 second in the future if the RR created
more than 1 second ago.

This bit was previously called 'UNIQUE` which might have the same meaning, but
cache-flush seems more in line with the RFC
  • Loading branch information
lyager committed Apr 26, 2024
1 parent 1b9647d commit 4729eb6
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 33 deletions.
50 changes: 39 additions & 11 deletions src/dns_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub(crate) const TYPE_ANY: u16 = 255;

pub(crate) const CLASS_IN: u16 = 1;
pub(crate) const CLASS_MASK: u16 = 0x7FFF;
pub(crate) const CLASS_UNIQUE: u16 = 0x8000;
pub(crate) const CLASS_CACHE_FLUSH: u16 = 0x8000;

/// Max size of UDP datagram payload: 9000 bytes - IP header 20 bytes - UDP header 8 bytes.
/// Reference: RFC6762: https://datatracker.ietf.org/doc/html/rfc6762#section-17
Expand All @@ -57,7 +57,7 @@ pub(crate) struct DnsEntry {
pub(crate) name: String, // always lower case.
pub(crate) ty: u16,
class: u16,
unique: bool,
cache_flush: bool,
}

impl DnsEntry {
Expand All @@ -66,7 +66,7 @@ impl DnsEntry {
name,
ty,
class: class & CLASS_MASK,
unique: (class & CLASS_UNIQUE) != 0,
cache_flush: (class & CLASS_CACHE_FLUSH) != 0,
}
}
}
Expand Down Expand Up @@ -134,6 +134,16 @@ impl DnsRecord {
cmp::max(0, remaining_millis / 1000) as u32
}

/// Return the absolute time for this record being created
fn get_created(&self) -> u64 {
self.created
}

/// Set the absolute expiration time in millis
fn set_expire(&mut self, expire_at: u64) {
self.expires = expire_at;
}

fn reset_ttl(&mut self, other: &DnsRecord) {
self.ttl = other.ttl;
self.created = other.created;
Expand All @@ -157,6 +167,10 @@ pub(crate) trait DnsRecordExt: fmt::Debug {
/// Returns whether `other` record is considered the same except TTL.
fn matches(&self, other: &dyn DnsRecordExt) -> bool;

fn get_cache_flush(&self) -> bool {
self.get_record().entry.cache_flush
}

fn get_name(&self) -> &str {
self.get_record().entry.name.as_str()
}
Expand All @@ -168,6 +182,14 @@ pub(crate) trait DnsRecordExt: fmt::Debug {
self.get_record_mut().reset_ttl(other.get_record());
}

fn get_created(&self) -> u64 {
self.get_record().get_created()
}

fn set_expire(&mut self, expire_at: u64) {
self.get_record_mut().set_expire(expire_at);
}

/// Returns true if another record has matched content,
/// and if its TTL is at least half of this record's.
fn suppressed_by_answer(&self, other: &dyn DnsRecordExt) -> bool {
Expand Down Expand Up @@ -546,9 +568,9 @@ impl DnsOutPacket {
let record = record_ext.get_record();
self.write_name(&record.entry.name);
self.write_short(record.entry.ty);
if record.entry.unique {
if record.entry.cache_flush {
// check "multicast"
self.write_short(record.entry.class | CLASS_UNIQUE);
self.write_short(record.entry.class | CLASS_CACHE_FLUSH);
} else {
self.write_short(record.entry.class);
}
Expand Down Expand Up @@ -825,7 +847,7 @@ impl DnsOutgoing {
// https://tools.ietf.org/html/rfc6763#section-12.1.
self.add_additional_answer(Box::new(DnsSrv::new(
service.get_fullname(),
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
service.get_priority(),
service.get_weight(),
Expand All @@ -836,7 +858,7 @@ impl DnsOutgoing {
self.add_additional_answer(Box::new(DnsTxt::new(
service.get_fullname(),
TYPE_TXT,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
service.generate_txt(),
)));
Expand All @@ -850,7 +872,7 @@ impl DnsOutgoing {
self.add_additional_answer(Box::new(DnsAddress::new(
service.get_hostname(),
t,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
address,
)));
Expand Down Expand Up @@ -1331,7 +1353,7 @@ mod tests {
use crate::dns_parser::{TYPE_A, TYPE_AAAA};

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

Expand Down Expand Up @@ -1379,7 +1401,7 @@ mod tests {
let mut response = DnsOutgoing::new(FLAGS_QR_RESPONSE);
response.add_additional_answer(Box::new(DnsSrv::new(
name,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
1,
1,
1,
Expand Down Expand Up @@ -1407,7 +1429,13 @@ mod tests {
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 nsec = DnsNSec::new(
name,
CLASS_IN | CLASS_CACHE_FLUSH,
1,
next_domain,
type_bitmap,
);
let absent_types = nsec._types();
assert_eq!(absent_types.len(), 2);
assert_eq!(absent_types[0], TYPE_A);
Expand Down
37 changes: 26 additions & 11 deletions src/service_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::log::{debug, error, warn};
use crate::{
dns_parser::{
current_time_millis, DnsAddress, DnsIncoming, DnsOutgoing, DnsPointer, DnsRecordBox,
DnsRecordExt, DnsSrv, DnsTxt, CLASS_IN, CLASS_UNIQUE, FLAGS_AA, FLAGS_QR_QUERY,
DnsRecordExt, DnsSrv, DnsTxt, CLASS_CACHE_FLUSH, CLASS_IN, FLAGS_AA, FLAGS_QR_QUERY,
FLAGS_QR_RESPONSE, MAX_MSG_ABSOLUTE, TYPE_A, TYPE_AAAA, TYPE_ANY, TYPE_NSEC, TYPE_PTR,
TYPE_SRV, TYPE_TXT,
},
Expand Down Expand Up @@ -1418,7 +1418,7 @@ impl Zeroconf {
out.add_answer_at_time(
Box::new(DnsSrv::new(
info.get_fullname(),
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
info.get_host_ttl(),
info.get_priority(),
info.get_weight(),
Expand All @@ -1431,7 +1431,7 @@ impl Zeroconf {
Box::new(DnsTxt::new(
info.get_fullname(),
TYPE_TXT,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
info.get_other_ttl(),
info.generate_txt(),
)),
Expand All @@ -1452,7 +1452,7 @@ impl Zeroconf {
Box::new(DnsAddress::new(
info.get_hostname(),
t,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
info.get_host_ttl(),
addr,
)),
Expand Down Expand Up @@ -1494,7 +1494,7 @@ impl Zeroconf {
out.add_answer_at_time(
Box::new(DnsSrv::new(
info.get_fullname(),
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
0,
info.get_priority(),
info.get_weight(),
Expand All @@ -1507,7 +1507,7 @@ impl Zeroconf {
Box::new(DnsTxt::new(
info.get_fullname(),
TYPE_TXT,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
0,
info.generate_txt(),
)),
Expand All @@ -1523,7 +1523,7 @@ impl Zeroconf {
Box::new(DnsAddress::new(
info.get_hostname(),
t,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
0,
addr,
)),
Expand Down Expand Up @@ -2036,7 +2036,7 @@ impl Zeroconf {
Box::new(DnsAddress::new(
&question.entry.name,
t,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
address,
)),
Expand All @@ -2057,7 +2057,7 @@ impl Zeroconf {
&msg,
Box::new(DnsSrv::new(
&question.entry.name,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
service.get_priority(),
service.get_weight(),
Expand All @@ -2073,7 +2073,7 @@ impl Zeroconf {
Box::new(DnsTxt::new(
&question.entry.name,
TYPE_TXT,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
service.generate_txt(),
)),
Expand All @@ -2097,7 +2097,7 @@ impl Zeroconf {
out.add_additional_answer(Box::new(DnsAddress::new(
service.get_hostname(),
t,
CLASS_IN | CLASS_UNIQUE,
CLASS_IN | CLASS_CACHE_FLUSH,
service.get_host_ttl(),
address,
)));
Expand Down Expand Up @@ -2356,6 +2356,21 @@ impl DnsCache {
_ => return None,
};

if incoming.get_cache_flush() {
// Mark all existing records of this type as expired, and prepend the new record.
let now = current_time_millis();
record_vec.iter_mut().for_each(|r| {
// When cache flush is asked, we set expire date to 1 second in the future
// if created more than 1 second ago
// Ref: RFC 6762 Section 10.2
if now - r.get_created() > 1000 {
r.set_expire(now + 1000);
}
});
record_vec.insert(0, incoming);
return Some((record_vec.first().unwrap(), true));
}

// update TTL for existing record or create a new record.
let (idx, updated) = match record_vec
.iter_mut()
Expand Down
22 changes: 11 additions & 11 deletions tests/mdns_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ fn integration_success() {
.expect("Failed to register our service");

// Browse for a service
let resolve_count = Arc::new(Mutex::new(0));
let resolve_count_clone = resolve_count.clone();
let resolved_ips: Arc<Mutex<HashSet<IpAddr>>> = Arc::new(Mutex::new(HashSet::new()));
let resolved_ips_clone = Arc::clone(&resolved_ips);
let remove_count = Arc::new(Mutex::new(0));
let remove_count_clone = remove_count.clone();
let stopped_count = Arc::new(Mutex::new(0));
Expand Down Expand Up @@ -83,8 +83,8 @@ fn integration_success() {
println!("{}", a);
}
if info.get_fullname().contains(&instance_name) {
let mut num = resolve_count_clone.lock().unwrap();
*num += 1;
let mut ip_map = resolved_ips_clone.lock().unwrap();
ip_map.extend(addrs);
}
let hostname = info.get_hostname();
assert_eq!(hostname, host_name);
Expand Down Expand Up @@ -146,13 +146,13 @@ fn integration_success() {
let count = addr_count.lock().unwrap();
assert_eq!(*count, my_addrs_count);

// `resolve_count` is not guaranteed to always be 1
// or `my_addrs_count`. If `my_addrs_count` > 1, these
// addrs could be resolved in a single message from
// the daemon, or in separate messages.
let count = resolve_count.lock().unwrap();
assert!(*count >= 1);
assert!(*count <= my_addrs_count);
// IP's can get resolved more than once if fx a cache-flush is asked from the sender of the
// MDNS records, so we look at unique IP addresses to see if they match the number of the
// network interfaces.
let unique_ip_count = resolved_ips.lock().unwrap();
assert_eq!(unique_ip_count.len(), my_addrs_count);
assert!(unique_ip_count.len() >= 1);
assert!(unique_ip_count.len() <= my_addrs_count);

let count = remove_count.lock().unwrap();
assert_eq!(*count, 1);
Expand Down

0 comments on commit 4729eb6

Please sign in to comment.