diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 87830df9..7acf4e6a 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -282,6 +282,10 @@ pub const CLASS_MASK: u16 = 0x7FFF; /// Cache-flush bit: the most significant bit of the rrclass field of the resource record. pub const CLASS_CACHE_FLUSH: u16 = 0x8000; +/// RFC 6762 §6.7: The resource record TTL given in a legacy unicast response SHOULD NOT +/// be greater than ten seconds. +pub const LEGACY_UNICAST_MAX_TTL: u32 = 10; + /// Absolute max size of UDP datagram payload for an mDNS packet over IPv4. /// /// RFC 6762 section 17: @@ -2041,19 +2045,28 @@ impl DnsOutgoing { self.questions.push(q); } - /// Clear the cache-flush (unique) bit on every answer and additional - /// record. Required for RFC 6762 §6.7 (Legacy Unicast Responses) and - /// §10.2 — a legacy resolver doesn't know about the cache-flush bit - /// and may misinterpret responses where it is set. - pub fn clear_cache_flush_bits(&mut self) { + /// Adjust records so the message is a valid legacy unicast response: + /// + /// - Clear the cache-flush (unique) bit: a legacy resolver + /// doesn't know about it and may misinterpret responses where it is set. + /// - Cap the TTL at [`LEGACY_UNICAST_MAX_TTL`] seconds: legacy resolvers + /// cache records without the mDNS cache-coherency mechanisms, so the true + /// (longer) TTL must not leak out to them. + /// Refer to [RFC 6762 Section 6.7] for details. + pub fn update_records_for_legacy_unicast(&mut self) { + let update = |rec: &mut DnsRecordBox| { + let record = rec.get_record_mut(); + record.entry.cache_flush = false; + record.ttl = record.ttl.min(LEGACY_UNICAST_MAX_TTL); + }; for (rec, _) in &mut self.answers { - rec.get_record_mut().entry.cache_flush = false; + update(rec); } for rec in &mut self.additionals { - rec.get_record_mut().entry.cache_flush = false; + update(rec); } for rec in &mut self.authorities { - rec.get_record_mut().entry.cache_flush = false; + update(rec); } } diff --git a/src/service_daemon.rs b/src/service_daemon.rs index ecdc3627..31b553e8 100644 --- a/src/service_daemon.rs +++ b/src/service_daemon.rs @@ -3550,8 +3550,9 @@ impl Zeroconf { // getaddrinfo, iOS resolver fallback). The response MUST be unicast // back to the querier's source IP and port; multicast replies will // never reach the querier's ephemeral socket. Legacy unicast - // responses must also echo the question section and clear the - // cache-flush bit, since legacy resolvers don't understand it. + // responses must also echo the question section, clear the + // cache-flush bit (legacy resolvers don't understand it), and cap + // record TTLs to 10 seconds (see update_records_for_legacy_unicast). let unicast_dest = if querier_addr.port() != MDNS_PORT { Some(querier_addr) } else { @@ -3562,7 +3563,7 @@ impl Zeroconf { for q in msg.questions() { out.add_question(q.entry_name(), q.entry_type()); } - out.clear_cache_flush_bits(); + out.update_records_for_legacy_unicast(); out.set_multicast(false); } else if msg.num_authorities() == 0 { // RFC 6762 §6: a record MUST NOT be multicast on an interface @@ -5235,7 +5236,7 @@ mod tests { use crate::{ dns_parser::{ DnsEntryExt, DnsIncoming, DnsOutgoing, DnsPointer, InterfaceId, RRType, ScopedIp, - CLASS_IN, FLAGS_AA, FLAGS_QR_QUERY, FLAGS_QR_RESPONSE, + CLASS_IN, FLAGS_AA, FLAGS_QR_QUERY, FLAGS_QR_RESPONSE, LEGACY_UNICAST_MAX_TTL, }, service_daemon::{add_answer_of_service, check_hostname}, }; @@ -5576,6 +5577,13 @@ mod tests { "legacy unicast responses must clear the cache-flush bit" ); + assert!( + answer.get_record().get_ttl() <= LEGACY_UNICAST_MAX_TTL, + "legacy unicast response TTL {} exceeds the {}s cap", + answer.get_record().get_ttl(), + LEGACY_UNICAST_MAX_TTL + ); + daemon.shutdown().unwrap(); }