resolved: actually unregister DNS-SD services when the D-Bus client vanishes (#43578)

Since the D-Bus DNS-SD API was introduced in c3036641f0ed (v236),
`RegisterService()` has created an `sd_bus_track` object to destroy the
registered service when the registering client disconnects from the bus.
However, the track object is a `_cleanup_(sd_bus_track_unrefp)` local in
`bus_method_register_service()` that is never stored anywhere, so it is
destroyed again as soon as the method handler returns. The vanish
handler (`dnssd_registered_service_on_bus_track()`) has thus been dead
code for the past ~8 years.

The observable effect: services registered via D-Bus silently persist
after the client disconnects — unowned, advertised until resolved exits,
and impossible to re-register since the identifier stays taken
(`DNSSD_SERVICE_EXISTS`). At the same time, the registration doesn't
survive a resolved restart, so the current behavior is neither reliably
ephemeral nor reliably persistent. Noticed while reviewing a project
that (unknowingly relying on this bug) registers a service with a
one-shot `gdbus call` and expects it to persist.

This PR:
1. splits the goodbye/zone-removal logic out of
`bus_dnssd_method_unregister()` into a helper — the vanish path needs
the full teardown, not just `dnssd_registered_service_free()`, which
would leave the service's RRs published in the per-link mDNS zones;
2. stores the track object in `DnssdRegisteredService`, making client
tracking work as originally intended;
3. documents the lifetime semantics in `org.freedesktop.resolve1(5)`
(matching the equivalent Avahi D-Bus API, whose entry groups are
likewise bound to the client connection) and points to `*.dnssd` files
for registrations independent of a client's lifetime;
4. adds a TEST-75 case covering the connection-bound lifetime.

Note this is a behavioral change for fire-and-forget clients (one-shot
`busctl`/`gdbus` invocations). Arguably those were never working
reliably (registrations silently vanished on any resolved restart, and
stale entries blocked re-registration), but if this is deemed too risky,
the alternative would be to drop the dead tracking code and codify the
persist-until-exit behavior instead — I'd argue tracking as originally
intended is the better contract.

Verification: code compiles; the new integration test is included.
This commit is contained in:
Yu Watanabe
2026-09-03 06:14:31 +09:00
committed by GitHub
9 changed files with 260 additions and 42 deletions
+81 -7
View File
@@ -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