From ba190b26e5bd1a18f07dbffd5d45e7e7fbe03509 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Thu, 9 Jul 2026 01:03:27 +0200 Subject: [PATCH] resolved: fix spurious BrowseServices add/remove flapping with ifindex=0 When a BrowseServices subscription uses ifindex=0 ("all interfaces"), mdns_browser_revisit_cache() looked up each mDNS scope's cache separately and called mdns_manage_services_answer() once per scope. That function derives "removed" events by diffing the browser's *global* service list (all interfaces, filtered only by owner family) against the single answer it is handed. So with two or more mDNS-relevant interfaces of the same family, a service present on interface A is not contained in interface B's answer and is therefore spuriously removed (emitting "removed") while B is reconciled, then re-added (emitting "added") on the next revisit tick. The result is a continuous added/removed/added/removed flap, within a second, for a service that is still present and without any goodbye packet involved -- as reported for the mDNS/DNS-SD browsing API. Fix it by accumulating the pruned cache answers from every matching mDNS scope into a single combined answer and reconciling exactly once, so the removed pass diffs the global service list against the union across all interfaces. Each scope's cache is still pruned before lookup, so genuinely expired records still drop out of the union and real removals still fire. The items are merged with dns_answer_add_full() rather than dns_answer_extend(), so each item's ifindex, flags, rrsig and cache-expiry "until" are preserved. dns_answer_extend()/dns_answer_merge() are not expiry-aware and do not carry the source items' "until" through: it defaults to USEC_INFINITY (the "no expiry" sentinel), which would skew the RFC 6762 section 5.2 TTL maintenance schedule that mdns_manage_services_answer() derives from item->until. The single-interface (ifindex>0) path is unchanged. (cherry picked from commit 835297415805cbc93f3e11a4b941392fd5d130d1) (cherry picked from commit e445b27491abbc5a9cdf3e6f9d0c0e29d2fbbcab) --- src/resolve/resolved-dns-browse-services.c | 34 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/resolve/resolved-dns-browse-services.c b/src/resolve/resolved-dns-browse-services.c index 6a48089a594..8395d4efafe 100644 --- a/src/resolve/resolved-dns-browse-services.c +++ b/src/resolve/resolved-dns-browse-services.c @@ -487,10 +487,21 @@ int mdns_browser_revisit_cache(DnsServiceBrowser *sb, int owner_family) { assert(sb); assert(sb->manager); - /* ifindex=0 means "all interfaces" */ + /* ifindex=0 means "all interfaces". Collect the cached answers from + * every matching mDNS scope into a single combined answer and reconcile + * once. Reconciling per-scope would be wrong: mdns_manage_services_answer() + * derives removals by diffing the browser's global service list against + * the answer it is handed, so a single scope's answer would spuriously + * "remove" (and then, on the next scope/pass, re-"add") services that are + * still present on other interfaces — resulting in a continuous + * added/removed event flap for services seen on more than the current + * scope. */ if (sb->ifindex == 0) { + _cleanup_(dns_answer_unrefp) DnsAnswer *combined = NULL; + LIST_FOREACH(scopes, scope, sb->manager->dns_scopes) { _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnsAnswerItem *item; if (scope->protocol != DNS_PROTOCOL_MDNS) continue; @@ -513,11 +524,24 @@ int mdns_browser_revisit_cache(DnsServiceBrowser *sb, int owner_family) { return log_error_errno(r, "Failed to look up DNS cache for service browser key on scope %s: %m", dns_scope_ifname(scope) ?: "global"); - r = mdns_manage_services_answer(sb, answer, owner_family); - if (r < 0) - return log_error_errno(r, "Failed to manage mDNS services after cache lookup on scope %s: %m", - dns_scope_ifname(scope) ?: "global"); + /* Merge preserving each item's ifindex, flags, rrsig and + * cache-expiry 'until'. (dns_answer_extend()/merge() would + * reset 'until' to USEC_INFINITY, which would skew the RFC + * 6762 §5.2 TTL-maintenance schedule that + * mdns_manage_services_answer() derives from item->until.) */ + DNS_ANSWER_FOREACH_ITEM(item, answer) { + r = dns_answer_add_extend_full(&combined, item->rr, item->ifindex, + item->flags, item->rrsig, item->until); + if (r < 0) + return log_error_errno(r, "Failed to merge mDNS cache answer from scope %s: %m", + dns_scope_ifname(scope) ?: "global"); + } } + + r = mdns_manage_services_answer(sb, combined, owner_family); + if (r < 0) + return log_error_errno(r, "Failed to manage mDNS services after cache lookup for all interfaces: %m"); + return 0; }