From a3a8e82f25d1179e156aecd4f614042ba0cefa4b Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 31 Aug 2026 20:23:41 +0200 Subject: [PATCH] resolved: only send goodbye for the RRs of the service being unregistered dns_scope_announce(scope, /* goodbye= */ true) is not scoped to a specific service: it walks the whole per-link mDNS zone and flags every established RR as goodbye (TTL 0). Unregistering a single DNS-SD service via the UnregisterService() D-Bus call hence withdrew the host's own A/AAAA records and every other published service on the link from remote caches, with nothing re-announcing them afterwards. Introduce dns_scope_send_goodbye(), which sends an unsolicited response for a caller-provided RR set, and let the DNS-SD unregistration path send goodbye only for the RRs of the service actually being removed, mirroring the flag logic of dns_scope_announce(). The goodbye answer is built per scope, and only includes records actually established in the respective zone (or under re-verification, in which case they are still served from it): a per-service goodbye must take care not to withdraw records that are still being probed for uniqueness, were withdrawn again after a conflict (in which case the name belongs to the neighbor), or were never published in the scope in the first place (e.g. on MulticastDNS=resolve links, which have an mDNS scope but do not publish DNS-SD services). This becomes more important with the next commit, which promotes unregistration from an explicit method call to an automatic consequence of the registering client disconnecting from the bus. --- src/resolve/resolved-dns-scope.c | 36 ++++++++++++ src/resolve/resolved-dns-scope.h | 1 + src/resolve/resolved-dnssd.c | 98 +++++++++++++++++++++++++------- 3 files changed, 114 insertions(+), 21 deletions(-) diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index d48896494f..6a20fa20da 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -1533,6 +1533,42 @@ static int on_announcement_timeout(sd_event_source *s, usec_t usec, void *userda return 0; } +int dns_scope_send_goodbye(DnsScope *scope, DnsAnswer *answer) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + assert(scope); + assert(answer); + + /* Sends an unsolicited response withdrawing the specified RRs, which are expected to be flagged as + * goodbye (TTL 0), without touching anything else published in the zone. */ + + if (scope->protocol != DNS_PROTOCOL_MDNS) + return 0; + + r = sd_event_get_state(scope->manager->event); + if (r < 0) + return log_debug_errno(r, "Failed to get event loop state: %m"); + + /* If this is called on exit, through manager_free() -> link_free(), then we cannot announce. */ + if (r == SD_EVENT_FINISHED) + return 0; + + if (dns_answer_isempty(answer)) + return 0; + + r = dns_scope_make_reply_packet(scope, /* id= */ 0, DNS_RCODE_SUCCESS, /* q= */ NULL, answer, + /* soa= */ NULL, /* tentative= */ false, &p); + if (r < 0) + return log_debug_errno(r, "Failed to build reply packet: %m"); + + r = dns_scope_emit_udp(scope, -EBADF, AF_UNSPEC, p); + if (r < 0) + return log_debug_errno(r, "Failed to send reply packet: %m"); + + return 0; +} + int dns_scope_announce(DnsScope *scope, bool goodbye) { _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; diff --git a/src/resolve/resolved-dns-scope.h b/src/resolve/resolved-dns-scope.h index 1367f55242..9f97a5ffcb 100644 --- a/src/resolve/resolved-dns-scope.h +++ b/src/resolve/resolved-dns-scope.h @@ -114,6 +114,7 @@ int dns_scope_ifindex(DnsScope *s); const char* dns_scope_ifname(DnsScope *s); int dns_scope_announce(DnsScope *scope, bool goodbye); +int dns_scope_send_goodbye(DnsScope *scope, DnsAnswer *answer); int dns_scope_add_dnssd_registered_services(DnsScope *scope); int dns_scope_remove_dnssd_registered_services(DnsScope *scope); diff --git a/src/resolve/resolved-dnssd.c b/src/resolve/resolved-dnssd.c index 39cb67fefe..5617293cb7 100644 --- a/src/resolve/resolved-dnssd.c +++ b/src/resolve/resolved-dnssd.c @@ -6,6 +6,7 @@ #include "conf-files.h" #include "conf-parser.h" #include "constants.h" +#include "dns-answer.h" #include "dns-domain.h" #include "dns-rr.h" #include "extract-word.h" @@ -71,6 +72,67 @@ DnssdRegisteredService *dnssd_registered_service_free(DnssdRegisteredService *se return mfree(service); } +static int dnssd_registered_service_make_goodbye( + DnssdRegisteredService *service, + DnsScope *scope, + DnsAnswer **ret) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + size_t n_rrs = 3; + int r; + + assert(service); + assert(scope); + assert(ret); + + /* Collects those of the service's RRs that are established in the scope's zone (or under + * re-verification, in which case they are still served from the zone), flagged as goodbye (TTL 0), + * so that only this service is withdrawn, and only where we actually asserted ownership of the + * records, i.e. not while a record is still being probed for uniqueness, was withdrawn again after + * a conflict, or was never published in this scope in the first place. */ + + LIST_FOREACH(items, txt_data, service->txt_data_items) + n_rrs++; + + answer = dns_answer_new(n_rrs); + if (!answer) + return -ENOMEM; + + DnsResourceRecord *rrs[] = { service->ptr_rr, service->sub_ptr_rr, service->srv_rr }; + + FOREACH_ELEMENT(rr, rrs) { + if (!*rr) + continue; + + DnsZoneItem *i = dns_zone_get(&scope->zone, *rr); + if (!i || !IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + r = dns_answer_add(answer, *rr, /* ifindex= */ 0, + dns_resource_key_is_dnssd_ptr((*rr)->key) ? + DNS_ANSWER_GOODBYE : (DNS_ANSWER_GOODBYE|DNS_ANSWER_CACHE_FLUSH), + /* rrsig= */ NULL); + if (r < 0) + return r; + } + + LIST_FOREACH(items, txt_data, service->txt_data_items) { + if (!txt_data->rr) + continue; + + DnsZoneItem *i = dns_zone_get(&scope->zone, txt_data->rr); + if (!i || !IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + r = dns_answer_add(answer, txt_data->rr, /* ifindex= */ 0, + DNS_ANSWER_GOODBYE|DNS_ANSWER_CACHE_FLUSH, /* rrsig= */ NULL); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(answer); + return 0; +} + void dnssd_registered_service_unregister(DnssdRegisteredService *service) { Link *l; int r; @@ -82,32 +144,26 @@ void dnssd_registered_service_unregister(DnssdRegisteredService *service) { /* Takes the service out of service: sends goodbye messages for it, removes its RRs from all mDNS * zones, and drops it from the manager. Note that this also frees the passed object. */ - HASHMAP_FOREACH(l, m->links) { - if (l->mdns_ipv4_scope) { - r = dns_scope_announce(l->mdns_ipv4_scope, /* goodbye= */ true); + HASHMAP_FOREACH(l, m->links) + FOREACH_ELEMENT(scope, ((DnsScope*[]) { l->mdns_ipv4_scope, l->mdns_ipv6_scope })) { + if (!*scope) + continue; + + _cleanup_(dns_answer_unrefp) DnsAnswer *goodbye = NULL; + + r = dnssd_registered_service_make_goodbye(service, *scope, &goodbye); + if (r >= 0) + r = dns_scope_send_goodbye(*scope, goodbye); if (r < 0) - log_warning_errno(r, "Failed to send goodbye messages in IPv4 scope, ignoring: %m"); + log_warning_errno(r, "Failed to send goodbye messages, ignoring: %m"); - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, service->ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, service->sub_ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, service->srv_rr); + dns_zone_remove_rr(&(*scope)->zone, service->ptr_rr); + dns_zone_remove_rr(&(*scope)->zone, service->sub_ptr_rr); + dns_zone_remove_rr(&(*scope)->zone, service->srv_rr); LIST_FOREACH(items, txt_data, service->txt_data_items) - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, txt_data->rr); + dns_zone_remove_rr(&(*scope)->zone, txt_data->rr); } - if (l->mdns_ipv6_scope) { - r = dns_scope_announce(l->mdns_ipv6_scope, /* goodbye= */ true); - if (r < 0) - log_warning_errno(r, "Failed to send goodbye messages in IPv6 scope, ignoring: %m"); - - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, service->ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, service->sub_ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, service->srv_rr); - LIST_FOREACH(items, txt_data, service->txt_data_items) - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, txt_data->rr); - } - } - /* This drops the service from the manager's hashmap, hence the subsequent refresh will not simply * add its RRs back. */ dnssd_registered_service_free(service);