From f753f898ed06206c7dd246ce25796bd4c249d360 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sun, 22 Feb 2026 23:38:59 +0100 Subject: [PATCH 1/3] resolved: Track per-service item ifindex in DnssdDiscoveredService The interface where each service was discovered needs to be remembered so it can be correctly reported when the service is later removed. Previously, service removal would use sb->ifindex, losing the actual interface information from the original discovery. This change: - Adds an ifindex field to DnssdDiscoveredService struct - Stores the discovered interface index when adding new services, preferring the per-item ifindex from DnsAnswerItem over the service browser's ifindex - Uses the stored ifindex when reporting service removal events This ensures that service removal notifications include the correct interface index where the service was originally discovered, matching the behavior of the corresponding service addition notifications. Assisted-by: Claude Opus 4.6 (Eclipse Theia IDE AI) --- src/resolve/resolved-dns-browse-services.c | 21 +++++++++++++++------ src/resolve/resolved-dns-browse-services.h | 3 ++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/resolve/resolved-dns-browse-services.c b/src/resolve/resolved-dns-browse-services.c index 6c47197639..dbb21e6f0d 100644 --- a/src/resolve/resolved-dns-browse-services.c +++ b/src/resolve/resolved-dns-browse-services.c @@ -155,7 +155,7 @@ static int mdns_maintenance_query(sd_event_source *s, uint64_t usec, void *userd return 0; } -int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family, usec_t until) { +int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family, int ifindex, usec_t until) { _cleanup_(dnssd_discovered_service_unrefp) DnssdDiscoveredService *s = NULL; int r; @@ -173,6 +173,7 @@ int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_ .service_browser = sb, .rr = dns_resource_record_copy(rr), .family = owner_family, + .ifindex = ifindex, .until = until, .query = NULL, .rr_ttl_state = DNS_RECORD_TTL_STATE_80_PERCENT, @@ -326,6 +327,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow DNS_ANSWER_FOREACH_ITEM(item, answer) { _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL; + int ifindex; if (dns_service_match_and_update(sb->dns_services, item->rr, owner_family, item->until)) continue; @@ -349,7 +351,10 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow if (!type) continue; - r = dns_add_new_service(sb, item->rr, owner_family, item->until); + /* Prefer the per-item ifindex, fall back to the service browser's ifindex */ + ifindex = item->ifindex > 0 ? item->ifindex : sb->ifindex; + + r = dns_add_new_service(sb, item->rr, owner_family, ifindex, item->until); if (r < 0) { log_error_errno(r, "Failed to add new DNS service: %m"); goto finish; @@ -360,7 +365,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow strna(type), strna(domain), strna(af_to_ipv4_ipv6(owner_family)), - sb->ifindex); + ifindex); r = sd_json_buildo( &entry, @@ -375,7 +380,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow !isempty(type), "type", SD_JSON_BUILD_STRING(type)), SD_JSON_BUILD_PAIR_CONDITION( !isempty(domain), "domain", SD_JSON_BUILD_STRING(domain)), - SD_JSON_BUILD_PAIR_INTEGER("ifindex", sb->ifindex)); + SD_JSON_BUILD_PAIR_INTEGER("ifindex", ifindex)); if (r < 0) { log_error_errno(r, "Failed to build JSON for new service: %m"); goto finish; @@ -392,6 +397,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow LIST_FOREACH(dns_services, service, sb->dns_services) { _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL; + int ifindex; if (service->family != owner_family) continue; @@ -416,6 +422,9 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow } } + /* Capture ifindex before removing the service */ + ifindex = service->ifindex; + dns_remove_service(sb, service); log_debug("Remove from the list %s, %s, %s, %s, %d", @@ -423,7 +432,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow strna(type), strna(domain), strna(af_to_ipv4_ipv6(owner_family)), - sb->ifindex); + ifindex); r = sd_json_buildo( &entry, @@ -435,7 +444,7 @@ int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int ow SD_JSON_BUILD_PAIR_STRING("name", name ?: ""), SD_JSON_BUILD_PAIR_STRING("type", type ?: ""), SD_JSON_BUILD_PAIR_STRING("domain", domain ?: ""), - SD_JSON_BUILD_PAIR_INTEGER("ifindex", sb->ifindex)); + SD_JSON_BUILD_PAIR_INTEGER("ifindex", ifindex)); if (r < 0) { log_error_errno(r, "Failed to build JSON for removed service: %m"); goto finish; diff --git a/src/resolve/resolved-dns-browse-services.h b/src/resolve/resolved-dns-browse-services.h index 243d23cb2d..424b2855a5 100644 --- a/src/resolve/resolved-dns-browse-services.h +++ b/src/resolve/resolved-dns-browse-services.h @@ -30,6 +30,7 @@ struct DnssdDiscoveredService { sd_event_source *schedule_event; DnsResourceRecord *rr; int family; + int ifindex; usec_t until; DnsRecordTTLState rr_ttl_state; DnsQuery *query; @@ -66,7 +67,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DnssdDiscoveredService *, dnssd_discovered_service_u bool dns_service_match_and_update(DnssdDiscoveredService *services, DnsResourceRecord *rr, int owner_family, usec_t until); int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int owner_family); -int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family, usec_t until); +int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family, int ifindex, usec_t until); int mdns_service_update(DnssdDiscoveredService *service, DnsResourceRecord *rr, usec_t t, usec_t until); int mdns_browser_revisit_cache(DnsServiceBrowser *sb, int owner_family); int dns_subscribe_browse_service( From 9ed12a81b2516357adc92fd690d704d85d9a1c60 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sun, 22 Feb 2026 23:51:22 +0100 Subject: [PATCH 2/3] resolved: Add ifindex=0 support for BrowseServices to browse all mDNS interfaces Avahi provides AVAHI_IF_UNSPEC (-1) to browse mDNS services on all interfaces simultaneously. Currently, systemd-resolved's BrowseServices varlink API requires a specific interface index and lacks the ability to browse on all available interfaces. This change adds support for ifindex = 0 to mean \"browse on all mDNS-enabled interfaces\" to match the Avahi API. When ifindex = 0 is specified the browser will now iterate all mDNS scopes instead of a single interface. This enables applications to discover services on any network interface without needing to know the specific interface index in advance. Assisted-by: Claude Opus 4.6 (Eclipse Theia IDE AI) --- src/resolve/resolved-dns-browse-services.c | 51 ++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/resolve/resolved-dns-browse-services.c b/src/resolve/resolved-dns-browse-services.c index dbb21e6f0d..f08b83cf5d 100644 --- a/src/resolve/resolved-dns-browse-services.c +++ b/src/resolve/resolved-dns-browse-services.c @@ -482,19 +482,61 @@ finish: int mdns_browser_revisit_cache(DnsServiceBrowser *sb, int owner_family) { _cleanup_(dns_answer_unrefp) DnsAnswer *lookup_ret_answer = NULL; - DnsScope *scope; int r; assert(sb); assert(sb->manager); - scope = manager_find_scope_from_protocol(sb->manager, sb->ifindex, DNS_PROTOCOL_MDNS, owner_family); + /* ifindex=0 means "all interfaces" */ + if (sb->ifindex == 0) { + LIST_FOREACH(scopes, scope, sb->manager->dns_scopes) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + + if (scope->protocol != DNS_PROTOCOL_MDNS) + continue; + + if (scope->family != owner_family) + continue; + + dns_cache_prune(&scope->cache); + + r = dns_cache_lookup( + &scope->cache, + sb->key, + sb->flags, + /* ret_rcode= */ NULL, + &answer, + /* ret_full_packet= */ NULL, + /* ret_query_flags= */ NULL, + /* ret_dnssec_result= */ NULL); + if (r < 0) + 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"); + } + return 0; + } + + /* Single scope for specifically requested interface */ + DnsScope *scope = manager_find_scope_from_protocol(sb->manager, sb->ifindex, DNS_PROTOCOL_MDNS, owner_family); if (!scope) return 0; dns_cache_prune(&scope->cache); - r = dns_cache_lookup(&scope->cache, sb->key, sb->flags, NULL, &lookup_ret_answer, NULL, NULL, NULL); + r = dns_cache_lookup( + &scope->cache, + sb->key, + sb->flags, + /* ret_rcode= */ NULL, + &lookup_ret_answer, + /* ret_full_packet= */ NULL, + /* ret_query_flags= */ NULL, + /* ret_dnssec_result= */ NULL); if (r < 0) return log_error_errno(r, "Failed to look up DNS cache for service browser key: %m"); @@ -676,6 +718,9 @@ int dns_subscribe_browse_service( if (ifindex < 0) return sd_varlink_error_invalid_parameter_name(link, "ifindex"); + if (ifindex == 0) + log_debug("BrowseServices: browsing all mDNS interfaces"); + if (isempty(type)) type = NULL; else if (!dnssd_srv_type_is_valid(type)) From 1fcd11d56c52b293a9d77e2bda1614308f1d8a4b Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Thu, 18 Dec 2025 12:46:56 +0100 Subject: [PATCH 3/3] resolved: Add test for ifindex=0 BrowseServices functionality Add integration tests for the new ifindex=0 support in BrowseServices that allows browsing all mDNS interfaces. Integration test (in TEST-89-RESOLVED-MDNS.sh): - testcase_browse_all_interfaces_ifindex_zero: Test with ifindex=0 Assisted-by: Claude Opus 4.5 (Preview) --- test/units/TEST-89-RESOLVED-MDNS.sh | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/units/TEST-89-RESOLVED-MDNS.sh b/test/units/TEST-89-RESOLVED-MDNS.sh index ae18cd49e8..8e8c4e38a9 100755 --- a/test/units/TEST-89-RESOLVED-MDNS.sh +++ b/test/units/TEST-89-RESOLVED-MDNS.sh @@ -193,6 +193,50 @@ testcase_single_service_multiple_times() { done } +# Helper function to run browse services with a custom ifindex +run_and_check_services_with_ifindex() { + local service_id="${1:?}" + local check_func="${2:?}" + local ifindex="${3:?}" + local unit_name="varlinkctl-$service_id-$SRANDOM.service" + local i out_file parameters service_type svc tmp_file + + out_file="$(mktemp)" + error_file="$(mktemp)" + tmp_file="$(mktemp)" + service_type="_testService$service_id._udp" + parameters="{ \"domain\": \"$service_type.local\", \"type\": \"\", \"ifindex\": $ifindex, \"flags\": 16785432 }" + + systemd-run --unit="$unit_name" --service-type=exec -p StandardOutput="file:$out_file" -p StandardError="file:$error_file" \ + varlinkctl call --more /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.BrowseServices "$parameters" + + # shellcheck disable=SC2064 + # Note: same as above about unregistering the trap once it's fired + trap "trap - RETURN; systemctl stop $unit_name" RETURN + + for _ in {0..14}; do + if [[ -s "$out_file" ]]; then + grep -o '"name":"[^"]*"' "$out_file" | sed 's/"name":"//;s/"//g' | sort | tee "$tmp_file" + if "$check_func" "$service_id" "$tmp_file"; then + return 0 + fi + fi + + sleep 2 + done + + cat "$out_file" + cat "$error_file" + return 1 +} + +testcase_browse_all_interfaces_ifindex_zero() { + : "Test browsing all interfaces with ifindex=0" + resolvectl flush-caches + # Using ifindex=0 should discover services on all mDNS interfaces + run_and_check_services_with_ifindex 0 check_both 0 +} + testcase_second_unreachable() { : "Test each service type while the second container is unreachable" systemd-run -M "$CONTAINER_2" --wait --pipe -- networkctl down host0