--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "af-list.h"
+#include "event-util.h"
+#include "dns-domain.h"
+#include "json-util.h"
+#include "random-util.h"
+#include "resolved-dns-browse-services.h"
+#include "resolved-dns-cache.h"
+#include "resolved-dns-question.h"
+#include "resolved-dns-query.h"
+#include "resolved-dns-rr.h"
+#include "resolved-dns-scope.h"
+#include "resolved-manager.h"
+#include "resolved-varlink.h"
+#include "string-table.h"
+
+typedef enum BrowseServiceUpdateEvent {
+ BROWSE_SERVICE_UPDATE_ADDED,
+ BROWSE_SERVICE_UPDATE_REMOVED,
+ _BROWSE_SERVICE_UPDATE_MAX,
+ _BROWSE_SERVICE_UPDATE_INVALID = -EINVAL,
+} BrowseServiceUpdateEvent;
+
+static const char * const browse_service_update_event_table[_BROWSE_SERVICE_UPDATE_MAX] = {
+ [BROWSE_SERVICE_UPDATE_ADDED] = "added",
+ [BROWSE_SERVICE_UPDATE_REMOVED] = "removed",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(browse_service_update_event, BrowseServiceUpdateEvent);
+
+/* RFC6762 5.2
+ * The intervals between successive queries MUST increase by at least a
+ * factor of two. When the interval between queries reaches or exceeds
+ * 60 minutes, perform subsequent queries at a steady-state rate of one
+ * query per hour. */
+static usec_t mdns_calculate_next_query_delay(usec_t current_delay) {
+ assert(current_delay <= 60 * 60 * USEC_PER_SEC);
+
+ if (current_delay == 0)
+ return USEC_PER_SEC;
+
+ return current_delay < 60 * 60 / 2 * USEC_PER_SEC ? current_delay * 2 : 60 * 60 * USEC_PER_SEC;
+}
+
+/* RFC 6762 section 5.2
+ * The querier should plan to issue a query at 80% of
+ * the record lifetime, and then if no answer is received, at 85%, 90%, and 95%. */
+static inline int DNS_RECORD_TTL_STATE_TO_PERCENT(DnsRecordTTLState ttl_state) {
+ static const int ttl_percent_table[_DNS_RECORD_TTL_STATE_MAX] = {
+ [DNS_RECORD_TTL_STATE_80_PERCENT] = 80,
+ [DNS_RECORD_TTL_STATE_85_PERCENT] = 85,
+ [DNS_RECORD_TTL_STATE_90_PERCENT] = 90,
+ [DNS_RECORD_TTL_STATE_95_PERCENT] = 95,
+ [DNS_RECORD_TTL_STATE_100_PERCENT] = 100,
+ };
+ if (ttl_state < 0 || ttl_state >= _DNS_RECORD_TTL_STATE_MAX)
+ return -EINVAL;
+ return ttl_percent_table[ttl_state];
+}
+
+static usec_t mdns_maintenance_next_time(usec_t until, uint32_t ttl, DnsRecordTTLState ttl_state) {
+ assert(ttl_state >= DNS_RECORD_TTL_STATE_80_PERCENT);
+ assert(ttl_state < _DNS_RECORD_TTL_STATE_MAX);
+
+ int percent = DNS_RECORD_TTL_STATE_TO_PERCENT(ttl_state);
+ assert(percent > 0);
+ assert(percent <= 100);
+
+ return usec_sub_unsigned(until, (100 - percent) * ttl * USEC_PER_SEC / 100);
+}
+
+/* RFC 6762 section 5.2
+ * A random variation of 2% of the record TTL should
+ * be added to maintenance queries. */
+static usec_t mdns_maintenance_jitter(uint32_t ttl) {
+ return random_u64_range(2 * ttl * USEC_PER_SEC / 100);
+}
+
+static void mdns_maintenance_query_complete(DnsQuery *q) {
+ _cleanup_(dns_service_browser_unrefp) DnsServiceBrowser *sb = NULL;
+ _cleanup_(dns_query_freep) DnsQuery *query = q;
+ DnssdDiscoveredService *service = NULL;
+ int r;
+
+ assert(query);
+ assert(query->manager);
+
+ if (query->state != DNS_TRANSACTION_SUCCESS)
+ return;
+
+ service = dnssd_discovered_service_ref(query->dnsservice_request);
+ if (!service)
+ return;
+
+ sb = dns_service_browser_ref(service->service_browser);
+ if (!sb)
+ return;
+
+ r = mdns_browser_revisit_cache(sb, query->answer_family);
+ if (r < 0)
+ return (void) log_error_errno(r, "Failed to revisit cache for family %s: %m", af_to_name(query->answer_family));
+}
+
+static int mdns_maintenance_query(sd_event_source *s, uint64_t usec, void *userdata) {
+ DnssdDiscoveredService *service = ASSERT_PTR(userdata);
+ _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+ int r;
+
+ /* Check if the TTL state has reached the maximum value, then revisit
+ * cache */
+ if (service->rr_ttl_state++ == DNS_RECORD_TTL_STATE_100_PERCENT)
+ return mdns_browser_revisit_cache(service->service_browser, service->family);
+
+ /* Create a new DNS query */
+ r = dns_query_new(
+ service->service_browser->manager,
+ &q,
+ service->service_browser->question_utf8,
+ service->service_browser->question_idna,
+ /* question_bypass= */ NULL,
+ service->service_browser->ifindex,
+ service->service_browser->flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create mDNS query for maintenance: %m");
+
+ q->complete = mdns_maintenance_query_complete;
+ q->varlink_request = sd_varlink_ref(service->service_browser->link);
+ q->dnsservice_request = dnssd_discovered_service_ref(service);
+
+ /* Schedule the next maintenance query based on the TTL */
+ usec_t next_time = mdns_maintenance_next_time(service->until, service->rr->ttl, service->rr_ttl_state);
+
+ r = event_reset_time(
+ service->service_browser->manager->event,
+ &service->schedule_event,
+ CLOCK_BOOTTIME,
+ next_time,
+ /* accuracy= */ 0,
+ mdns_maintenance_query,
+ service,
+ /* priority= */ 0,
+ "mdns-next-query-schedule",
+ /* force_reset= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to schedule next mDNS maintenance query: %m");
+
+ /* Perform the query */
+ r = dns_query_go(q);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send mDNS maintenance query: %m");
+
+ TAKE_PTR(q);
+ return 0;
+}
+
+int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family, usec_t until) {
+ _cleanup_(dnssd_discovered_service_unrefp) DnssdDiscoveredService *s = NULL;
+ int r;
+
+ assert(sb);
+ assert(rr);
+
+ s = new(DnssdDiscoveredService, 1);
+ if (!s)
+ return log_oom();
+
+ usec_t usec = now(CLOCK_BOOTTIME);
+
+ *s = (DnssdDiscoveredService) {
+ .n_ref = 1,
+ .service_browser = sb,
+ .rr = dns_resource_record_copy(rr),
+ .family = owner_family,
+ .until = until,
+ .query = NULL,
+ .rr_ttl_state = DNS_RECORD_TTL_STATE_80_PERCENT,
+ };
+
+ LIST_PREPEND(dns_services, sb->dns_services, s);
+
+ /* Schedule the first cache maintenance query at 80% of the record's
+ * TTL. Subsequent queries issued at 5% increments until 100% of the
+ * TTL. RFC 6762 section 5.2. If service is being added after 80% of the
+ * TTL has already elapsed, schedule the next query at the next 5%
+ * increment. */
+ usec_t next_time = 0;
+ while (s->rr_ttl_state >= DNS_RECORD_TTL_STATE_80_PERCENT &&
+ s->rr_ttl_state < _DNS_RECORD_TTL_STATE_MAX) {
+ next_time = mdns_maintenance_next_time(s->until, s->rr->ttl, s->rr_ttl_state);
+ if (next_time >= usec)
+ break;
+
+ s->rr_ttl_state++;
+ }
+
+ if (next_time < usec) {
+ /* If next_time is still in the past, the service is being added
+ * after it has already expired. Just schedule a 100%
+ * maintenance query. */
+ next_time = usec + USEC_PER_SEC;
+ s->rr_ttl_state = DNS_RECORD_TTL_STATE_100_PERCENT;
+ }
+
+ usec_t jitter = mdns_maintenance_jitter(rr->ttl);
+
+ r = sd_event_add_time(
+ sb->manager->event,
+ &s->schedule_event,
+ CLOCK_BOOTTIME,
+ usec_add(next_time, jitter),
+ /* accuracy= */ 0,
+ mdns_maintenance_query,
+ s);
+ if (r < 0)
+ return log_error_errno(
+ r,
+ "Failed to schedule mDNS maintenance query for DNS service: %m");
+
+ TAKE_PTR(s);
+ return 0;
+}
+
+void dns_remove_service(DnsServiceBrowser *sb, DnssdDiscoveredService *service) {
+ assert(sb);
+ assert(service);
+
+ LIST_REMOVE(dns_services, sb->dns_services, service);
+ dnssd_discovered_service_unref(service);
+}
+
+DnssdDiscoveredService *dns_service_free(DnssdDiscoveredService *service) {
+ if (!service)
+ return NULL;
+
+ service->schedule_event = sd_event_source_disable_unref(service->schedule_event);
+
+ if (service->query && DNS_TRANSACTION_IS_LIVE(service->query->state))
+ dns_query_complete(service->query, DNS_TRANSACTION_ABORTED);
+
+ service->rr = dns_resource_record_unref(service->rr);
+
+ return mfree(service);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(DnssdDiscoveredService, dnssd_discovered_service, dns_service_free);
+
+int mdns_service_update(DnssdDiscoveredService *service, DnsResourceRecord *rr, usec_t t, usec_t until) {
+ assert(service);
+ assert(rr);
+
+ service->until = until;
+ service->rr->ttl = rr->ttl;
+
+ /* Update the 80% TTL maintenance event based on new record received
+ * from the network. RFC 6762 section 5.2 */
+ if (service->schedule_event) {
+ usec_t next_time = mdns_maintenance_next_time(
+ service->until, service->rr->ttl, DNS_RECORD_TTL_STATE_80_PERCENT);
+ usec_t jitter = mdns_maintenance_jitter(service->rr->ttl);
+
+ return sd_event_source_set_time(service->schedule_event, usec_add(next_time, jitter));
+ }
+
+ return 0;
+}
+
+bool dns_service_match_and_update(DnssdDiscoveredService *services, DnsResourceRecord *rr, int owner_family, usec_t until) {
+ usec_t t = now(CLOCK_BOOTTIME);
+
+ /* Check if a discovered service matching the given resource record and owner family exists in the list.
+ * If found, update the service's expiration time if the new 'until' is later, unless the TTL is <= 1 (goodbye packet).
+ * Return true if a matching service is found, false otherwise. */
+
+ LIST_FOREACH(dns_services, service, services)
+ if (dns_resource_record_equal(service->rr, rr) > 0 && service->family == owner_family) {
+ if (rr->ttl <= 1)
+ return true;
+
+ if (service->until < until)
+ mdns_service_update(service, rr, t, until);
+
+ return true;
+ }
+
+ return false;
+}
+
+void dns_browse_services_purge(Manager *m, int family) {
+ int r = 0;
+
+ /* Called after caches are flushed.
+ * Clear local service records and notify varlink client. */
+ if (!m)
+ return;
+
+ DnsServiceBrowser *sb;
+ HASHMAP_FOREACH(sb, m->dns_service_browsers) {
+ r = sd_event_source_set_enabled(sb->schedule_event, SD_EVENT_OFF);
+ if (r < 0)
+ log_error_errno(r, "Failed to disable event source for service browser, ignoring: %m");
+
+ if (IN_SET(family, AF_INET, AF_UNSPEC)) {
+ r = mdns_browser_revisit_cache(sb, AF_INET);
+ if (r < 0)
+ log_error_errno(r, "Failed to revisit cache for IPv4, ignoring: %m");
+ }
+
+ if (IN_SET(family, AF_INET6, AF_UNSPEC)) {
+ r = mdns_browser_revisit_cache(sb, AF_INET6);
+ if (r < 0)
+ log_error_errno(r, "Failed to revisit cache for IPv6, ignoring: %m");
+ }
+ }
+}
+
+int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int owner_family) {
+ DnsAnswerItem *item;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
+ int r;
+
+ assert(sb);
+
+ /* Check for new service added */
+ 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;
+
+ if (dns_service_match_and_update(sb->dns_services, item->rr, owner_family, item->until))
+ continue;
+
+ r = dns_service_split(item->rr->ptr.name, &name, &type, &domain);
+ if (r < 0) {
+ log_error_errno(r, "Failed to split DNS service name: %m");
+ goto finish;
+ }
+
+ if (!name) {
+ type = mfree(type);
+ domain = mfree(domain);
+ r = dns_service_split(dns_resource_key_name(item->rr->key), &name, &type, &domain);
+ if (r < 0) {
+ log_error_errno(r, "Failed to split DNS service name (fallback): %m");
+ goto finish;
+ }
+ }
+
+ if (!type)
+ continue;
+
+ r = dns_add_new_service(sb, item->rr, owner_family, item->until);
+ if (r < 0) {
+ log_error_errno(r, "Failed to add new DNS service: %m");
+ goto finish;
+ }
+
+ log_debug("Add into the list %s, %s, %s, %s, %d",
+ strna(name),
+ strna(type),
+ strna(domain),
+ strna(af_to_ipv4_ipv6(owner_family)),
+ sb->ifindex);
+
+ r = sd_json_buildo(
+ &entry,
+ SD_JSON_BUILD_PAIR(
+ "updateFlag",
+ SD_JSON_BUILD_STRING(browse_service_update_event_to_string(
+ BROWSE_SERVICE_UPDATE_ADDED))),
+ SD_JSON_BUILD_PAIR("family", SD_JSON_BUILD_INTEGER(owner_family)),
+ SD_JSON_BUILD_PAIR_CONDITION(
+ !isempty(name), "name", SD_JSON_BUILD_STRING(name)),
+ SD_JSON_BUILD_PAIR_CONDITION(
+ !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("ifindex", SD_JSON_BUILD_INTEGER(sb->ifindex)));
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON for new service: %m");
+ goto finish;
+ }
+
+ r = sd_json_variant_append_array(&array, entry);
+ if (r < 0) {
+ log_error_errno(r, "Failed to append JSON entry to array: %m");
+ goto finish;
+ }
+ }
+
+ /* Check for services removed */
+ 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;
+
+ if (service->family != owner_family)
+ continue;
+
+ if (dns_answer_contains(answer, service->rr))
+ continue;
+
+ r = dns_service_split(service->rr->ptr.name, &name, &type, &domain);
+ if (r < 0) {
+ log_error_errno(r, "Failed to split DNS service name from list: %m");
+ goto finish;
+ }
+
+ if (!name) {
+ type = mfree(type);
+ domain = mfree(domain);
+ r = dns_service_split(dns_resource_key_name(service->rr->key), &name, &type, &domain);
+ if (r < 0) {
+ log_error_errno(r,
+ "Failed to split DNS service name (fallback) from list: %m");
+ goto finish;
+ }
+ }
+
+ dns_remove_service(sb, service);
+
+ log_debug("Remove from the list %s, %s, %s, %s, %d",
+ strna(name),
+ strna(type),
+ strna(domain),
+ strna(af_to_ipv4_ipv6(owner_family)),
+ sb->ifindex);
+
+ r = sd_json_buildo(
+ &entry,
+ SD_JSON_BUILD_PAIR(
+ "updateFlag",
+ SD_JSON_BUILD_STRING(browse_service_update_event_to_string(
+ BROWSE_SERVICE_UPDATE_REMOVED))),
+ SD_JSON_BUILD_PAIR("family", SD_JSON_BUILD_INTEGER(owner_family)),
+ SD_JSON_BUILD_PAIR("name", SD_JSON_BUILD_STRING(name ?: "")),
+ SD_JSON_BUILD_PAIR("type", SD_JSON_BUILD_STRING(type ?: "")),
+ SD_JSON_BUILD_PAIR("domain", SD_JSON_BUILD_STRING(domain ?: "")),
+ SD_JSON_BUILD_PAIR("ifindex", SD_JSON_BUILD_INTEGER(sb->ifindex)));
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON for removed service: %m");
+ goto finish;
+ }
+
+ r = sd_json_variant_append_array(&array, entry);
+ if (r < 0) {
+ log_error_errno(r, "Failed to append JSON entry to array: %m");
+ goto finish;
+ }
+ }
+
+ if (!sd_json_variant_is_blank_array(array)) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *vm = NULL;
+
+ r = sd_json_buildo(&vm, SD_JSON_BUILD_PAIR("browserServiceData", SD_JSON_BUILD_VARIANT(array)));
+ if (r < 0) {
+ log_error_errno(r,
+ "Failed to build JSON object for browser service data: %m");
+ goto finish;
+ }
+
+ r = sd_varlink_notify(sb->link, vm);
+ if (r < 0) {
+ log_error_errno(r, "Failed to notify via varlink: %m");
+ goto finish;
+ }
+ }
+
+ return 0;
+
+finish:
+ return sd_varlink_error_errno(sb->link, r);
+}
+
+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);
+ 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);
+ if (r < 0)
+ return log_error_errno(r, "Failed to look up DNS cache for service browser key: %m");
+
+ r = mdns_manage_services_answer(sb, lookup_ret_answer, owner_family);
+ if (r < 0)
+ return log_error_errno(r, "Failed to manage mDNS services after cache lookup: %m");
+
+ return 0;
+}
+
+int mdns_notify_browsers_goodbye(DnsScope *scope) {
+ DnsServiceBrowser *sb;
+ int r;
+
+ if (!scope)
+ return 0;
+
+ HASHMAP_FOREACH(sb, scope->manager->dns_service_browsers) {
+ r = mdns_browser_revisit_cache(sb, scope->family);
+ if (r < 0)
+ return log_error_errno(
+ r,
+ "Failed to revisit cache for service browser with family %d: %m",
+ scope->family);
+ }
+
+ return 0;
+}
+
+int mdns_notify_browsers_unsolicited_updates(Manager *m, DnsAnswer *answer, int owner_family) {
+ DnsServiceBrowser *sb;
+ int r;
+
+ assert(m);
+
+ if (!answer)
+ return 0;
+
+ HASHMAP_FOREACH(sb, m->dns_service_browsers) {
+
+ r = dns_answer_match_key(answer, sb->key, NULL);
+ if (r < 0)
+ return log_error_errno(
+ r,
+ "Failed to match answer key with service browser's key: %m");
+ if (r == 0)
+ continue;
+
+ r = mdns_browser_revisit_cache(sb, owner_family);
+ if (r < 0)
+ return log_error_errno(r, "Failed to revisit cache for service browser: %m");
+ }
+
+ return 0;
+}
+
+static void mdns_browse_service_query_complete(DnsQuery *q) {
+ _cleanup_(dns_service_browser_unrefp) DnsServiceBrowser *sb = NULL;
+ _cleanup_(dns_query_freep) DnsQuery *query = q;
+ int r;
+
+ assert(query);
+ assert(query->manager);
+
+ if (query->state != DNS_TRANSACTION_SUCCESS)
+ return;
+
+ sb = dns_service_browser_ref(query->service_browser_request);
+ if (!sb)
+ return;
+
+ r = mdns_browser_revisit_cache(sb, query->answer_family);
+ if (r < 0)
+ return (void) log_error_errno(r, "Failed to revisit cache for service browser: %m");
+
+ /* When the query is answered from cache, we only get answers for one
+ * answer_family i.e. either ipv4 or ipv6. We need to perform another
+ * cache lookup for the other answer_family */
+ if (query->answer_query_flags == SD_RESOLVED_FROM_CACHE) {
+ r = mdns_browser_revisit_cache(sb, query->answer_family == AF_INET ? AF_INET6 : AF_INET);
+ if (r < 0)
+ return (void) log_error_errno(r, "Failed to revisit cache for service browser: %m");
+ }
+}
+
+static int mdns_next_query_schedule(sd_event_source *s, uint64_t usec, void *userdata) {
+ _cleanup_(dns_service_browser_unrefp) DnsServiceBrowser *sb = NULL;
+ _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+ int r;
+
+ assert(userdata);
+ assert_se(sb = dns_service_browser_ref(userdata));
+
+ /* Enable the answer from the cache for the very first query */
+ if (sb->delay == 0)
+ SET_FLAG(sb->flags, SD_RESOLVED_NO_CACHE, false);
+
+ /* Set the flag indicating that the query is continuous.
+ * RFC 6762 Section 5.2 outlines timing requirements for continuous queries.
+ */
+ sb->flags |= SD_RESOLVED_QUERY_CONTINUOUS;
+
+ r = dns_query_new(sb->manager, &q, sb->question_utf8, sb->question_idna, NULL, sb->ifindex, sb->flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create new DNS query: %m");
+
+ q->complete = mdns_browse_service_query_complete;
+ q->service_browser_request = dns_service_browser_ref(sb);
+ q->varlink_request = sd_varlink_ref(sb->link);
+ sd_varlink_set_userdata(sb->link, q);
+
+ r = dns_query_go(q);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send DNS query: %m");
+
+ /* Calculate the next query delay */
+ sb->delay = mdns_calculate_next_query_delay(sb->delay);
+
+ SET_FLAG(sb->flags, SD_RESOLVED_NO_CACHE, true);
+
+ r = event_reset_time_relative(
+ sb->manager->event,
+ &sb->schedule_event,
+ CLOCK_BOOTTIME,
+ sb->delay,
+ /* accuracy= */ 0,
+ mdns_next_query_schedule,
+ sb,
+ /* priority= */ 0,
+ "mdns-next-query-schedule",
+ /* force_reset= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reset event time for next query schedule: %m");
+
+ TAKE_PTR(q);
+
+ return 0;
+}
+
+void dns_browse_services_restart(Manager *m) {
+ int r;
+
+ if (!(m && m->dns_service_browsers))
+ return;
+
+ DnsServiceBrowser *sb;
+
+ HASHMAP_FOREACH(sb, m->dns_service_browsers) {
+ sb->delay = 0;
+
+ r = event_reset_time_relative(
+ sb->manager->event,
+ &sb->schedule_event,
+ CLOCK_BOOTTIME,
+ (sb->delay * USEC_PER_SEC),
+ /* accuracy= */ 0,
+ mdns_next_query_schedule,
+ sb,
+ /* priority= */ 0,
+ "mdns-next-query-schedule",
+ /* force_reset= */ true);
+
+ if (r < 0)
+ log_error_errno(r,
+ "Failed to reset mDNS service subscriber event for service browser: %m");
+ }
+}
+
+int dns_subscribe_browse_service(
+ Manager *m, sd_varlink *link, const char *domain, const char *type, int ifindex, uint64_t flags) {
+
+ _cleanup_(dns_service_browser_unrefp) DnsServiceBrowser *sb = NULL;
+ _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL;
+ int r;
+
+ assert(m);
+ assert(link);
+
+ if (ifindex < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "ifindex");
+
+ if (isempty(type))
+ type = NULL;
+ else if (!dnssd_srv_type_is_valid(type))
+ return sd_varlink_error_invalid_parameter_name(link, "type");
+
+ if (isempty(domain))
+ domain = "local";
+ else {
+ r = dns_name_is_valid(domain);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return sd_varlink_error_invalid_parameter_name(link, "domain");
+ }
+
+ r = dns_question_new_service_pointer(
+ &question_utf8, type, domain, /* convert_idna= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create DNS question for UTF8 version: %m");
+
+ r = dns_question_new_service_pointer(
+ &question_idna, type, domain, /* convert_idna= */ true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create DNS question for IDNA version: %m");
+
+ sb = new(DnsServiceBrowser, 1);
+ if (!sb)
+ return log_oom();
+
+ *sb = (DnsServiceBrowser) {
+ .n_ref = 1,
+ .manager = m,
+ .link = sd_varlink_ref(link),
+ .question_utf8 = dns_question_ref(question_utf8),
+ .question_idna = dns_question_ref(question_idna),
+ .key = dns_question_first_key(question_utf8),
+ .ifindex = ifindex,
+ .flags = flags,
+ .delay = 0,
+ };
+
+ /* Only mDNS continuous querying is currently supported. See RFC 6762 */
+ if (!FLAGS_SET(flags, SD_RESOLVED_MDNS))
+ return -EINVAL;
+
+ r = sd_event_add_time_relative(
+ m->event,
+ &sb->schedule_event,
+ CLOCK_BOOTTIME,
+ sb->delay,
+ /* accuracy= */ 0,
+ mdns_next_query_schedule,
+ sb);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&m->dns_service_browsers, NULL, link, sb);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add service browser to the hashmap: %m");
+
+ TAKE_PTR(sb);
+
+ return 0;
+}
+
+DnsServiceBrowser *dns_service_browser_free(DnsServiceBrowser *sb) {
+ DnsQuery *q;
+
+ if (!sb)
+ return NULL;
+
+ while (sb->dns_services)
+ dns_remove_service(sb, sb->dns_services);
+
+ sb->schedule_event = sd_event_source_disable_unref(sb->schedule_event);
+
+ q = sd_varlink_get_userdata(sb->link);
+ if (q && DNS_TRANSACTION_IS_LIVE(q->state))
+ dns_query_complete(q, DNS_TRANSACTION_ABORTED);
+
+ sb->question_idna = dns_question_unref(sb->question_idna);
+ sb->question_utf8 = dns_question_unref(sb->question_utf8);
+
+ sb->link = sd_varlink_unref(sb->link);
+
+ return mfree(sb);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsServiceBrowser, dns_service_browser, dns_service_browser_free);
SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0));
+static SD_VARLINK_DEFINE_ENUM_TYPE(
+ BrowseServiceUpdateFlag,
+ SD_VARLINK_FIELD_COMMENT("Indicates that the service was added."),
+ SD_VARLINK_DEFINE_ENUM_VALUE(added),
+ SD_VARLINK_FIELD_COMMENT("Indicates that the service was removed."),
+ SD_VARLINK_DEFINE_ENUM_VALUE(removed));
+
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+ ServiceData,
+ SD_VARLINK_DEFINE_FIELD_BY_TYPE(updateFlag, BrowseServiceUpdateFlag, 0),
+ SD_VARLINK_FIELD_COMMENT("The address family of the service, one of AF_INET or AF_INET6."),
+ SD_VARLINK_DEFINE_FIELD(family, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("The name of the service, e.g., 'My Service'. May be null if not specified."),
+ SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The type of service, e.g., '_http._tcp'."),
+ SD_VARLINK_DEFINE_FIELD(type, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The domain in which the service resides, e.g., 'local'."),
+ SD_VARLINK_DEFINE_FIELD(domain, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The Linux interface index for the network interface associated with this service."),
+ SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, 0));
+
static SD_VARLINK_DEFINE_METHOD(
ResolveAddress,
SD_VARLINK_FIELD_COMMENT("The Linux interface index for the network interface to search on. Typically left unspecified, in order to search on all interfaces."),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(rrs, ResolvedRecord, SD_VARLINK_ARRAY),
SD_VARLINK_DEFINE_OUTPUT(flags, SD_VARLINK_INT, 0));
+static SD_VARLINK_DEFINE_METHOD_FULL(
+ BrowseServices,
+ SD_VARLINK_SUPPORTS_MORE,
+ SD_VARLINK_FIELD_COMMENT("The domain to browse for services. If null, the default browsing domain local is used."),
+ SD_VARLINK_DEFINE_INPUT(domain, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The service type to browse for (e.g., '_http._tcp')."),
+ SD_VARLINK_DEFINE_INPUT(type, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The Linux interface index for the network interface to search on."),
+ SD_VARLINK_DEFINE_INPUT(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Various browsing flags to modify the operation."),
+ SD_VARLINK_DEFINE_INPUT(flags, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("An array of service data containing information about discovered services."),
+ SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(browserServiceData, ServiceData, SD_VARLINK_ARRAY));
+
static SD_VARLINK_DEFINE_ERROR(NoNameServers);
static SD_VARLINK_DEFINE_ERROR(NoSuchResourceRecord);
static SD_VARLINK_DEFINE_ERROR(QueryTimedOut);
&vl_method_ResolveService,
SD_VARLINK_SYMBOL_COMMENT("Resolves a domain name to one or more DNS resource records."),
&vl_method_ResolveRecord,
+ SD_VARLINK_SYMBOL_COMMENT("Starts browsing for DNS-SD services of specified type."),
+ &vl_method_BrowseServices,
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a resolved address."),
&vl_type_ResolvedAddress,
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a resolved host name."),
&vl_type_ResourceRecord,
SD_VARLINK_SYMBOL_COMMENT("Encapsulates information about a resolved DNS resource record "),
&vl_type_ResolvedRecord,
+ SD_VARLINK_SYMBOL_COMMENT("Describes the update flag for browsing services, indicating whether a service was added or removed during browsing."),
+ &vl_type_BrowseServiceUpdateFlag,
+ SD_VARLINK_SYMBOL_COMMENT("Encapsulates the service data obtained from browsing."),
+ &vl_type_ServiceData,
&vl_error_NoNameServers,
&vl_error_NoSuchResourceRecord,
&vl_error_QueryTimedOut,