diff --git a/src/service_daemon.rs b/src/service_daemon.rs index fee00fb..8dcf1a4 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -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}; @@ -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 { @@ -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, @@ -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 }); diff --git a/src/service_info.rs b/src/service_info.rs index 7b13710..c2c5b79 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -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)); @@ -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( @@ -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