resolved: fix spurious BrowseServices add/remove flapping with ifindex=0 (#42982)
## Problem A `BrowseServices` subscription with `"ifindex":0` (browse all interfaces) receives a continuous flap of `added`/`removed` events for a service that is still present — within a second, and with no goodbye packet involved. ## Root cause For `ifindex==0`, `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* discovered-service list (all interfaces, filtered only by owner family) against the single answer it's handed. So with ≥2 mDNS-relevant interfaces of the same family, a service present on interface A isn't in interface B's answer and is spuriously removed while B is reconciled, then re-added on the next revisit tick. ## Fix Accumulate the pruned cache answers from every matching mDNS scope into one combined `DnsAnswer` and reconcile **once**, so removals diff the global list against the union across interfaces. Items are merged with `dns_answer_add_full()` (not `dns_answer_extend()`, which defaults each item's `until` to `USEC_INFINITY` and would skew the RFC 6762 §5.2 TTL-maintenance schedule). The single-interface (`ifindex>0`) path is unchanged. ## Test `TEST-89-RESOLVED-MDNS.sh` gains `testcase_browse_ifindex_zero_no_flap`: it adds a service-less dummy mDNS link to guarantee ≥2 same-family scopes (the flap precondition), browses `ifindex=0`, waits for discovery, then asserts **zero** `removed` events while every publisher stays up. The subscription uses `varlinkctl --timeout=infinity`, since it sits idle after discovery and the default 45s idle timeout would sever it (and the assertion) mid-observation. ## Testing status Builds clean; `shellcheck -x` clean. First CI round: `TEST-89` (including this testcase) passed on all mkosi platforms; the failing jobs all traced to unrelated flakes/infra.
This commit is contained in:
@@ -489,10 +489,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;
|
||||
@@ -515,11 +526,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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user