Skip to content
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
32 changes: 29 additions & 3 deletions src/service_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ use crate::{
CLASS_CACHE_FLUSH, CLASS_IN, FLAGS_AA, FLAGS_QR_QUERY, FLAGS_QR_RESPONSE, MAX_MSG_ABSOLUTE,
},
error::{e_fmt, Error, Result},
service_info::{valid_ip_on_intf, DnsRegistry, MyIntf, Probe, ServiceInfo, ServiceStatus},
service_info::{
valid_ip_on_intf, DnsRegistry, MyIntf, Probe, ServiceInfo, ServiceStatus,
MULTICAST_RATE_LIMIT_MILLIS,
},
Receiver, ResolvedService, TxtProperties,
};
use flume::{bounded, Sender, TrySendError};
Expand Down Expand Up @@ -78,6 +81,25 @@ const LOOPBACK_V4: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);

const RESOLVE_WAIT_IN_MILLIS: u64 = 500;

/// RFC 6762 §8.3: the two unsolicited announcements are sent "one second apart".
/// We schedule the second one strictly wider than the §6 multicast rate-limit
/// window ([`MULTICAST_RATE_LIMIT_MILLIS`]) so that scheduling skew — the few
/// millis between capturing this base time and actually stamping the records as
/// multicast — can never make the rate limit throttle the second announcement
/// away. A small random jitter is added on top (see `ANNOUNCE_SECOND_JITTER_MILLIS`)
/// to de-synchronize announcements across hosts and services.
const ANNOUNCE_SECOND_DELAY_MILLIS: u64 = MULTICAST_RATE_LIMIT_MILLIS + 100;

/// Upper bound (exclusive) of the random jitter added to the second announcement
/// delay. Kept small so the spacing stays close to the RFC's "one second".
const ANNOUNCE_SECOND_JITTER_MILLIS: u64 = 50;

// The §8.3 announcement spacing MUST stay strictly wider than the §6 rate-limit
// window, or the rate limit throttles the second announcement away (leaving only
// one unsolicited response). Enforced at compile time so the two can't drift.
#[allow(clippy::assertions_on_constants)]
const _: () = assert!(ANNOUNCE_SECOND_DELAY_MILLIS > MULTICAST_RATE_LIMIT_MILLIS);

/// Response status code for the service `unregister` call.
#[derive(Debug)]
pub enum UnregisterStatus {
Expand Down Expand Up @@ -2150,7 +2172,9 @@ impl Zeroconf {
// RFC 6762 section 8.3.
// ..The Multicast DNS responder MUST send at least two unsolicited
// responses, one second apart.
let next_time = current_time_millis() + 1000;
let next_time = current_time_millis()
+ ANNOUNCE_SECOND_DELAY_MILLIS
+ fastrand::u64(0..ANNOUNCE_SECOND_JITTER_MILLIS);
for if_index in outgoing_intfs {
self.add_retransmission(
next_time,
Expand Down Expand Up @@ -2240,7 +2264,9 @@ impl Zeroconf {
};

if announced_v4 || announced_v6 {
let next_time = now + 1000;
let next_time = now
+ ANNOUNCE_SECOND_DELAY_MILLIS
+ fastrand::u64(0..ANNOUNCE_SECOND_JITTER_MILLIS);
let command =
Command::RegisterResend(info.get_fullname().to_string(), *if_index);
self.retransmissions.push(ReRun { next_time, command });
Expand Down
9 changes: 7 additions & 2 deletions src/service_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ impl DnsRegistry {

// Prune stale entries so the map stays bounded across name changes;
// any record older than the one-second window is irrelevant now.
last_multicast.retain(|_, last| now.saturating_sub(*last) < 1000);
last_multicast.retain(|_, last| now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS);

out.retain_answers(|record| keep_after_rate_limit(last_multicast, record, now));

Expand Down Expand Up @@ -1342,6 +1342,11 @@ impl DnsRegistry {
}
}

/// RFC 6762 section 6 per-record, per-interface multicast rate-limit window:
/// a record must not be re-multicast until at least this many millis have
/// elapsed since it was last multicast on that interface.
pub(crate) const MULTICAST_RATE_LIMIT_MILLIS: u64 = 1000;

/// Returns whether `record` may still be multicast under the RFC 6762 section 6
/// rate limit, updating `last_multicast` to `now` when it is kept.
fn keep_after_rate_limit(
Expand All @@ -1351,7 +1356,7 @@ fn keep_after_rate_limit(
) -> bool {
let key = rate_limit_key(record);
match last_multicast.get(&key) {
Some(last) if now.saturating_sub(*last) < 1000 => false,
Some(last) if now.saturating_sub(*last) < MULTICAST_RATE_LIMIT_MILLIS => false,
_ => {
last_multicast.insert(key, now);
true
Expand Down