diff --git a/NEWS b/NEWS index f6fbf63ce3..2a509d3cf2 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,19 @@ CHANGES WITH 262 in spe: Feature Removals and Incompatible Changes: + * DNS-SD services registered via systemd-resolved's D-Bus method + org.freedesktop.resolve1.Manager.RegisterService() are now + unregistered automatically when the client that registered them + disconnects from the bus, as originally intended and matching the + behavior of the equivalent Avahi API. Due to a bug the client + tracking never worked, and such services used to stay registered + until systemd-resolved exited (but did not survive a restart of the + daemon either). Clients must now keep their bus connection open for + as long as the service shall remain registered; one-shot + registrations, e.g. via busctl or gdbus, will be withdrawn again as + soon as the tool exits. To register a DNS-SD service independently + of a client's lifetime, use *.dnssd files, see systemd.dnssd(5). + * Meson options '-Dlibiptc=' (deprecated in v259), '-Dlibidn=', '-Drc-local=', '-Dsysvinit-path=', and '-Dsysvrcnd-path=' (deprecated in v260) have been removed. diff --git a/man/org.freedesktop.resolve1.xml b/man/org.freedesktop.resolve1.xml index b8b0fade79..7a882d6326 100644 --- a/man/org.freedesktop.resolve1.xml +++ b/man/org.freedesktop.resolve1.xml @@ -450,6 +450,18 @@ node /org/freedesktop/resolve1 { DNS-SD SRV record. Finally, it takes an array of TXT record data. It returns an object path which may be used as handle to the registered service. + The lifetime of a service registered via this method is bound to the D-Bus connection of the + client that registered it: when the client disconnects from the bus the service is automatically + unregistered and goodbye packets are sent out, as if UnregisterService() had been + called. Clients hence must keep their bus connection open for as long as the service shall remain + registered. The registration also does not survive systemd-resolved losing its + own bus connection, in which case tracking clients is not possible anymore and all services registered + via this method are unregistered. To register a DNS-SD service independently of a client's lifetime, + use *.dnssd files, see + systemd.dnssd5. Note + that prior to version 262, due to a bug, the client's connection was not tracked and services + registered via this method stayed registered until the daemon exited. + The UnregisterService() method undoes the effect of RegisterService() and deletes a DNS-SD service previously created via IPC again. diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c index 53092bd6d3..650259a8c0 100644 --- a/src/resolve/resolved-bus.c +++ b/src/resolve/resolved-bus.c @@ -1894,8 +1894,8 @@ static int dnssd_registered_service_on_bus_track(sd_bus_track *t, void *userdata assert(t); - log_debug("Client of active request vanished, destroying DNS-SD service."); - dnssd_registered_service_free(s); + log_debug("Client of DNS-SD service '%s' vanished, unregistering.", s->id); + dnssd_registered_service_unregister(s); return 0; } @@ -2063,6 +2063,10 @@ static int bus_method_register_service(sd_bus_message *message, void *userdata, if (r < 0) return r; + /* The service is unregistered automatically when the client that registered it disconnects from the + * bus, hence keep the bus track object pinned for the lifetime of the service. */ + service->bus_track = TAKE_PTR(bus_track); + service->manager = m; r = hashmap_ensure_put(&m->dnssd_registered_services, &string_hash_ops, service->id, service); 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-bus.c b/src/resolve/resolved-dnssd-bus.c index 17eab683b7..8d648fe8b7 100644 --- a/src/resolve/resolved-dnssd-bus.c +++ b/src/resolve/resolved-dnssd-bus.c @@ -6,18 +6,14 @@ #include "bus-object.h" #include "bus-polkit.h" #include "hashmap.h" -#include "log.h" -#include "resolved-dns-scope.h" #include "resolved-dnssd.h" #include "resolved-dnssd-bus.h" -#include "resolved-link.h" #include "resolved-manager.h" #include "strv.h" int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error) { DnssdRegisteredService *s = ASSERT_PTR(userdata); Manager *m; - Link *l; int r; assert(message); @@ -38,35 +34,7 @@ int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_ if (r == 0) return 1; /* Polkit will call us back */ - HASHMAP_FOREACH(l, m->links) { - if (l->mdns_ipv4_scope) { - r = dns_scope_announce(l->mdns_ipv4_scope, true); - if (r < 0) - log_warning_errno(r, "Failed to send goodbye messages in IPv4 scope: %m"); - - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->sub_ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->srv_rr); - LIST_FOREACH(items, txt_data, s->txt_data_items) - dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, txt_data->rr); - } - - if (l->mdns_ipv6_scope) { - r = dns_scope_announce(l->mdns_ipv6_scope, true); - if (r < 0) - log_warning_errno(r, "Failed to send goodbye messages in IPv6 scope: %m"); - - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->sub_ptr_rr); - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->srv_rr); - LIST_FOREACH(items, txt_data, s->txt_data_items) - dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, txt_data->rr); - } - } - - dnssd_registered_service_free(s); - - manager_refresh_rrs(m); + dnssd_registered_service_unregister(s); return sd_bus_reply_method_return(message, NULL); } diff --git a/src/resolve/resolved-dnssd.c b/src/resolve/resolved-dnssd.c index 498f975f39..1d9d8f6635 100644 --- a/src/resolve/resolved-dnssd.c +++ b/src/resolve/resolved-dnssd.c @@ -6,15 +6,19 @@ #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" #include "hashmap.h" #include "hexdecoct.h" +#include "log.h" #include "path-util.h" #include "resolved-conf.h" +#include "resolved-dns-scope.h" #include "resolved-dns-zone.h" #include "resolved-dnssd.h" +#include "resolved-link.h" #include "resolved-manager.h" #include "specifier.h" #include "string-util.h" @@ -53,6 +57,8 @@ DnssdRegisteredService *dnssd_registered_service_free(DnssdRegisteredService *se if (service->manager) hashmap_remove(service->manager->dnssd_registered_services, service->id); + sd_bus_track_unref(service->bus_track); + dns_resource_record_unref(service->ptr_rr); dns_resource_record_unref(service->sub_ptr_rr); dns_resource_record_unref(service->srv_rr); @@ -68,6 +74,105 @@ 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; + + assert(service); + + Manager *m = ASSERT_PTR(service->manager); + + /* 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) + 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, ignoring: %m"); + + 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(&(*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); + + manager_refresh_rrs(m); +} + void dnssd_registered_service_clear_on_reload(Hashmap *services) { DnssdRegisteredService *service; diff --git a/src/resolve/resolved-dnssd.h b/src/resolve/resolved-dnssd.h index aa5a80b0bd..d19024e972 100644 --- a/src/resolve/resolved-dnssd.h +++ b/src/resolve/resolved-dnssd.h @@ -39,6 +39,10 @@ typedef struct DnssdRegisteredService { Manager *manager; + /* Tracks the D-Bus client that registered the service, so that it can be unregistered when the + * client disconnects from the bus. Only set for services registered via D-Bus. */ + sd_bus_track *bus_track; + /* Services registered via D-Bus are not removed on reload */ ResolveConfigSource config_source; @@ -47,6 +51,7 @@ typedef struct DnssdRegisteredService { } DnssdRegisteredService; DnssdRegisteredService *dnssd_registered_service_free(DnssdRegisteredService *service); +void dnssd_registered_service_unregister(DnssdRegisteredService *service); DnssdTxtData *dnssd_txtdata_free(DnssdTxtData *txt_data); DnssdTxtData *dnssd_txtdata_free_all(DnssdTxtData *txt_data); void dnssd_registered_service_clear_on_reload(Hashmap *services); diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh index 899c5a6b81..017397baf1 100755 --- a/test/units/TEST-75-RESOLVED.sh +++ b/test/units/TEST-75-RESOLVED.sh @@ -232,7 +232,7 @@ manual_testcase_01_resolvectl() { ip link add hoge.foo type dummy # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { ip link del hoge ip link del hoge.foo @@ -349,6 +349,7 @@ manual_testcase_02_mdns_llmnr() { ip link add hoge.foo type dummy # Cleanup + # shellcheck disable=SC2317,SC2329 cleanup() { rm -f /run/systemd/resolved.conf.d/90-mdns-llmnr.conf ip link del hoge @@ -436,6 +437,78 @@ manual_testcase_02_mdns_llmnr() { assert_in 'no' "$(resolvectl llmnr hoge)" } +# Test for the D-Bus RegisterService()/UnregisterService() API +manual_testcase_03_register_service() { + local cursor_file="" invocation_id + + # Cleanup + # shellcheck disable=SC2317,SC2329 + cleanup() { + rm -f /run/systemd/resolved.conf.d/90-mdns.conf "${cursor_file:-}" + ip link del mdns99 2>/dev/null || : + systemctl reload systemd-resolved.service + } + + trap cleanup RETURN ERR + + mkdir -p /run/systemd/resolved.conf.d + { + echo "[Resolve]" + echo "MulticastDNS=yes" + } >/run/systemd/resolved.conf.d/90-mdns.conf + restart_resolved + resolvectl log-level debug + + # resolved is restarted automatically if it crashes (Restart=always), which would let the checks below + # pass for the wrong reason, so remember the invocation and verify it stayed the same at the end. + invocation_id=$(systemctl show -P InvocationID systemd-resolved.service) + + # Make sure at least one link carries mDNS scopes, so that unregistration actually runs the + # per-scope goodbye/zone teardown. + ip link add mdns99 type dummy + ip link set mdns99 up + resolvectl mdns mdns99 yes + + cursor_file=$(mktemp) + journalctl -n 0 --cursor-file="$cursor_file" + + # The lifetime of a registered service is bound to the bus connection of the client that registered + # it. busctl disconnects right after the call, so the service should be unregistered again shortly + # after. Wait for the vanish handler log message as proof that the client tracking triggered the + # unregistration. See testcase_15_wait_online_dns() for why journalctl -f | grep -m1 is not used here. + assert_in '/org/freedesktop/resolve1/dnssd/testservice' \ + "$(busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager \ + RegisterService 'sssqqqaa{say}' testservice '%H' _testservice._tcp 1234 0 0 0)" + + timeout 30 bash -c "until journalctl --after-cursor=\"\$(cat \"$cursor_file\")\" SYSLOG_IDENTIFIER=systemd-resolved --grep \"Client of DNS-SD service 'testservice' vanished, unregistering.\" >/dev/null 2>&1; do sleep .5; done" + + assert_not_in 'dnssd/testservice' "$(busctl tree org.freedesktop.resolve1)" + + # Unregistering it again should fail specifically because the service is gone (busctl prints the + # D-Bus error message, not the error name), i.e. not because resolved crashed or the bus connection + # failed. + assert_in "DNS-SD service 'testservice' not known" \ + "$(busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager \ + UnregisterService 'o' /org/freedesktop/resolve1/dnssd/testservice 2>&1 || :)" + + # Re-registering the same identifier must succeed: before client tracking worked, the identifier + # stayed taken by the vanished client's service and this failed with DNSSD_SERVICE_EXISTS. Take a + # fresh cursor so the wait below cannot match the first round's identical message. + rm -f "$cursor_file" + journalctl -n 0 --cursor-file="$cursor_file" + assert_in '/org/freedesktop/resolve1/dnssd/testservice' \ + "$(busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager \ + RegisterService 'sssqqqaa{say}' testservice '%H' _testservice._tcp 1234 0 0 0)" + + timeout 30 bash -c "until journalctl --after-cursor=\"\$(cat \"$cursor_file\")\" SYSLOG_IDENTIFIER=systemd-resolved --grep \"Client of DNS-SD service 'testservice' vanished, unregistering.\" >/dev/null 2>&1; do sleep .5; done" + + assert_not_in 'dnssd/testservice' "$(busctl tree org.freedesktop.resolve1)" + + # resolved must not have crashed and gotten restarted along the way. + assert_eq "$(systemctl show -P InvocationID systemd-resolved.service)" "$invocation_id" + systemctl is-active systemd-resolved.service +} + testcase_03_23951() { : "--- nss-resolve/nss-myhostname tests" @@ -866,7 +939,7 @@ testcase_08_resolved() { testcase_09_resolvectl_showcache() { # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { rm -f /run/systemd/resolved.conf.d/90-resolved.conf rm -f /run/systemd/network/10-dns2.netdev @@ -928,7 +1001,7 @@ testcase_10_resolvectl_json() { local status_json # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { rm -f /run/systemd/resolved.conf.d/90-fallback.conf systemctl reload systemd-resolved.service @@ -1099,7 +1172,7 @@ testcase_11_nft() { # Test resolvectl show-server-state testcase_12_resolvectl2() { # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { rm -f /run/systemd/resolved.conf.d/90-reload.conf systemctl reload systemd-resolved.service @@ -1201,7 +1274,7 @@ testcase_13_varlink_subscribe_dns_configuration() { fi # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { echo "===== io.systemd.Resolve.Monitor.SubscribeDNSConfiguration output: =====" cat "$tmpfile" @@ -1278,7 +1351,7 @@ testcase_13_varlink_subscribe_dns_configuration() { # Test RefuseRecordTypes testcase_14_refuse_record_types() { - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { rm -f /run/systemd/resolved.conf.d/90-refuserecords.conf restart_resolved @@ -1440,7 +1513,7 @@ testcase_14_refuse_record_types() { # Test systemd-networkd-wait-online interactions with systemd-resolved testcase_15_wait_online_dns() { # Cleanup - # shellcheck disable=SC2317 + # shellcheck disable=SC2317,SC2329 cleanup() { echo "===== journalctl -u $unit =====" journalctl -b --no-pager --no-hostname --full -u "$unit" @@ -1725,6 +1798,7 @@ systemctl enable --now systemd-resolved.service # Need to be run before SETUP, otherwise things will break manual_testcase_01_resolvectl manual_testcase_02_mdns_llmnr +manual_testcase_03_register_service # Run setup setup