Compare commits
4 Commits
dc81256b26
...
4c641d2457
Author | SHA1 | Date |
---|---|---|
Vishal Chillara | 4c641d2457 | |
Frantisek Sumsal | e9cfc2bd94 | |
Vishal Chillara Srinivas | 08860aa147 | |
Vishal Chillara Srinivas | 2e5bf2774e |
|
@ -8,6 +8,7 @@ basic_dns_sources = files(
|
|||
'resolved-dns-rr.c',
|
||||
'resolved-dns-answer.c',
|
||||
'resolved-dns-question.c',
|
||||
'resolved-dns-browse-services.c',
|
||||
'resolved-util.c',
|
||||
'dns-type.c',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,687 @@
|
|||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "af-list.h"
|
||||
#include "event-util.h"
|
||||
#include "json-util.h"
|
||||
#include "random-util.h"
|
||||
#include "resolved-dns-browse-services.h"
|
||||
#include "resolved-dns-cache.h"
|
||||
#include "resolved-varlink.h"
|
||||
|
||||
/* 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 usec_t mdns_maintenance_next_time(usec_t until, uint32_t ttl, int ttl_state) {
|
||||
return usec_sub_unsigned(until, (20 - ttl_state * 5) * 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(100) * 2 * ttl * USEC_PER_SEC / 10000;
|
||||
}
|
||||
|
||||
#define MDNS_80_PERCENT 80
|
||||
#define MDNS_5_PERCENT 5
|
||||
|
||||
static void mdns_find_service_from_query(DnsService **service, DnsServiceBrowser *sb, DnsQuery *q) {
|
||||
assert(sb);
|
||||
|
||||
/* Find the service that owns the query. */
|
||||
LIST_FOREACH(dns_services, s, sb->dns_services) {
|
||||
if (s->query == q) {
|
||||
*service = s;
|
||||
return;
|
||||
}
|
||||
}
|
||||
*service = NULL;
|
||||
}
|
||||
|
||||
static void mdns_maintenance_query_complete(DnsQuery *q) {
|
||||
_cleanup_(dns_service_browser_unrefp) DnsServiceBrowser *sb = NULL;
|
||||
_cleanup_(dns_query_freep) DnsQuery *query = q;
|
||||
DnsService *service = NULL;
|
||||
int r;
|
||||
|
||||
assert(query);
|
||||
assert(query->manager);
|
||||
|
||||
sb = dns_service_browser_ref(hashmap_get(query->manager->dns_service_browsers, query->varlink_request));
|
||||
if (!sb)
|
||||
return;
|
||||
|
||||
if (query->state != DNS_TRANSACTION_SUCCESS) {
|
||||
r = 0;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
r = dns_answer_match_key(query->answer, sb->key, NULL);
|
||||
if (r <= 0)
|
||||
goto finish;
|
||||
|
||||
r = mdns_browser_lookup_cache(sb, query->answer_family);
|
||||
|
||||
finish:
|
||||
if (r < 0)
|
||||
log_error_errno(r, "mDNS maintenance query complete failed: %m");
|
||||
|
||||
mdns_find_service_from_query(&service, sb, query);
|
||||
if (service)
|
||||
service->query = NULL;
|
||||
}
|
||||
|
||||
static int mdns_maintenance_query(sd_event_source *s, uint64_t usec, void *userdata) {
|
||||
DnsService *service = NULL;
|
||||
_cleanup_(dns_query_freep) DnsQuery *q = NULL;
|
||||
int r;
|
||||
|
||||
assert(userdata);
|
||||
|
||||
service = userdata;
|
||||
|
||||
if (service->rr_ttl_state++ == MDNS_TTL_100_PERCENT)
|
||||
return mdns_browser_lookup_cache(service->sb, service->family);
|
||||
|
||||
r = dns_query_new(service->sb->m, &q, service->sb->question_utf8, service->sb->question_idna, NULL, service->sb->ifindex, service->sb->flags);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
q->complete = mdns_maintenance_query_complete;
|
||||
q->varlink_request = sd_varlink_ref(service->sb->link);
|
||||
service->query = TAKE_PTR(q);
|
||||
|
||||
usec_t next_time = mdns_maintenance_next_time(service->until, service->rr->ttl, service->rr_ttl_state);
|
||||
|
||||
/* Schedule next maintenance query for service */
|
||||
r = event_reset_time(
|
||||
service->sb->m->event, &service->schedule_event,
|
||||
CLOCK_BOOTTIME, next_time, 0, mdns_maintenance_query,
|
||||
service, 0, "mdns-next-query-schedule", true);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = dns_query_go(service->query);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
return 0;
|
||||
|
||||
finish:
|
||||
dns_query_free(service->query);
|
||||
return log_error_errno(r, "Failed mdns maintenance query: %m");
|
||||
}
|
||||
|
||||
int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family) {
|
||||
_cleanup_(dns_service_unrefp) DnsService *s = NULL;
|
||||
int r;
|
||||
|
||||
assert(sb);
|
||||
assert(rr);
|
||||
|
||||
s = new(DnsService, 1);
|
||||
if (!s)
|
||||
return log_oom();
|
||||
|
||||
usec_t usec = now(CLOCK_BOOTTIME);
|
||||
|
||||
*s = (DnsService) {
|
||||
.n_ref = 1,
|
||||
.sb = dns_service_browser_ref(sb),
|
||||
.rr = dns_resource_record_copy(rr),
|
||||
.family = owner_family,
|
||||
.until = rr->until,
|
||||
.query = NULL,
|
||||
.rr_ttl_state = MDNS_TTL_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 <= MDNS_TTL_100_PERCENT) {
|
||||
next_time = mdns_maintenance_next_time(rr->until, 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 = MDNS_TTL_100_PERCENT;
|
||||
}
|
||||
|
||||
usec_t jitter = mdns_maintenance_jitter(rr->ttl);
|
||||
|
||||
r = sd_event_add_time(
|
||||
sb->m->event,
|
||||
&s->schedule_event,
|
||||
CLOCK_BOOTTIME,
|
||||
usec_add(next_time, jitter),
|
||||
0,
|
||||
mdns_maintenance_query,
|
||||
s);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
TAKE_PTR(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dns_remove_service(DnsServiceBrowser *sb, DnsService *service) {
|
||||
assert(sb);
|
||||
assert(service);
|
||||
|
||||
LIST_REMOVE(dns_services, sb->dns_services, service);
|
||||
dns_service_free(service);
|
||||
}
|
||||
|
||||
DnsService *dns_service_free(DnsService *service) {
|
||||
if (!service)
|
||||
return NULL;
|
||||
|
||||
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);
|
||||
|
||||
dns_service_browser_unref(service->sb);
|
||||
|
||||
dns_resource_record_unref(service->rr);
|
||||
|
||||
return mfree(service);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsService, dns_service, dns_service_free);
|
||||
|
||||
int mdns_service_update(DnsService *service, DnsResourceRecord *rr, usec_t t) {
|
||||
service->until = rr->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 */
|
||||
usec_t next_time = mdns_maintenance_next_time(service->until, service->rr->ttl, MDNS_TTL_80_PERCENT);
|
||||
usec_t jitter = mdns_maintenance_jitter(service->rr->ttl);
|
||||
|
||||
if (service->schedule_event)
|
||||
return sd_event_source_set_time(service->schedule_event, usec_add(next_time, jitter));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool dns_service_contains(DnsService *services, DnsResourceRecord *rr, int owner_family) {
|
||||
usec_t t = now(CLOCK_BOOTTIME);
|
||||
|
||||
LIST_FOREACH(dns_services, service, services)
|
||||
if (dns_resource_record_equal(rr, service->rr) > 0 && service->family == owner_family) {
|
||||
if (rr->ttl <= 1)
|
||||
return true;
|
||||
|
||||
if (rr->until > service->until)
|
||||
mdns_service_update(service, rr, t);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void dns_browse_services_purge(Manager *m, int family) {
|
||||
int r = 0;
|
||||
|
||||
/* Called after caches are flused.
|
||||
* Clear local service records and notify varlink client. */
|
||||
if (!(m && m->dns_service_browsers))
|
||||
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)
|
||||
goto finish;
|
||||
|
||||
if (family == AF_UNSPEC) {
|
||||
r = mdns_browser_lookup_cache(sb, AF_INET);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
r = mdns_browser_lookup_cache(sb, AF_INET6);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
return;
|
||||
}
|
||||
|
||||
r = mdns_browser_lookup_cache(sb, family);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
finish:
|
||||
if (r < 0)
|
||||
log_error_errno(r, "mdns browse services purge failed: %m");
|
||||
return;
|
||||
}
|
||||
|
||||
int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int owner_family) {
|
||||
DnsResourceRecord *i;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
|
||||
int r;
|
||||
|
||||
assert(sb);
|
||||
|
||||
/* Check for new service added */
|
||||
DNS_ANSWER_FOREACH(i, answer) {
|
||||
_cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *entry = NULL;
|
||||
|
||||
if (dns_service_contains(sb->dns_services, i, owner_family))
|
||||
continue;
|
||||
|
||||
r = dns_service_split(i->ptr.name, &name, &type, &domain);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
if (!name) {
|
||||
type = mfree(type);
|
||||
domain = mfree(domain);
|
||||
r = dns_service_split(dns_resource_key_name(i->key), &name, &type, &domain);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (!type)
|
||||
continue;
|
||||
|
||||
r = dns_add_new_service(sb, i, owner_family);
|
||||
if (r < 0)
|
||||
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("add_flag", SD_JSON_BUILD_BOOLEAN(true)),
|
||||
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("interface", SD_JSON_BUILD_INTEGER(sb->ifindex)));
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = sd_json_variant_append_array(&array, entry);
|
||||
if (r < 0)
|
||||
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)
|
||||
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)
|
||||
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("add_flag", SD_JSON_BUILD_BOOLEAN(false)),
|
||||
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("interface", SD_JSON_BUILD_INTEGER(sb->ifindex)));
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = sd_json_variant_append_array(&array, entry);
|
||||
if (r < 0)
|
||||
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("browser_service_data", SD_JSON_BUILD_VARIANT(array)));
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
r = sd_varlink_notify(sb->link, vm);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
finish:
|
||||
log_error_errno(r, "Failed to process received services: %m");
|
||||
return sd_varlink_error_errno(sb->link, r);
|
||||
}
|
||||
|
||||
int mdns_browser_lookup_cache(DnsServiceBrowser *sb, int owner_family) {
|
||||
_cleanup_(dns_answer_unrefp) DnsAnswer *lookup_ret_answer = NULL;
|
||||
DnsScope *scope;
|
||||
int r;
|
||||
|
||||
assert(sb);
|
||||
assert(sb->m);
|
||||
|
||||
scope = manager_find_scope_from_protocol(sb->m, 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 r;
|
||||
|
||||
return mdns_manage_services_answer(sb, lookup_ret_answer, owner_family);
|
||||
}
|
||||
|
||||
int mdns_notify_browsers_goodbye(DnsScope *scope) {
|
||||
DnsServiceBrowser *sb = NULL;
|
||||
int r;
|
||||
|
||||
if (!scope)
|
||||
return 0;
|
||||
|
||||
HASHMAP_FOREACH(sb, scope->manager->dns_service_browsers) {
|
||||
r = mdns_browser_lookup_cache(sb, scope->family);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
finish:
|
||||
return r;
|
||||
}
|
||||
|
||||
int mdns_notify_browsers_unsolicited_updates(Manager *m, DnsAnswer *answer, int owner_family) {
|
||||
DnsServiceBrowser *sb = NULL;
|
||||
int r;
|
||||
|
||||
assert(m);
|
||||
|
||||
if (!answer)
|
||||
return 0;
|
||||
|
||||
if (!m->dns_service_browsers)
|
||||
return 0;
|
||||
|
||||
HASHMAP_FOREACH(sb, m->dns_service_browsers) {
|
||||
r = dns_answer_match_key(answer, sb->key, NULL);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
else if (r == 0)
|
||||
continue;
|
||||
|
||||
r = mdns_browser_lookup_cache(sb, owner_family);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
finish:
|
||||
return log_error_errno(r, "Failed to notify mDNS service subscribers, %m");
|
||||
}
|
||||
|
||||
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(hashmap_get(query->manager->dns_service_browsers, query->varlink_request));
|
||||
if (!sb)
|
||||
return;
|
||||
|
||||
r = dns_answer_match_key(query->answer, sb->key, NULL);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
else if (r == 0)
|
||||
return;
|
||||
|
||||
r = mdns_browser_lookup_cache(sb, query->answer_family);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
/* 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_lookup_cache(sb, query->answer_family == AF_INET? AF_INET6 : AF_INET);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
return;
|
||||
finish:
|
||||
log_error_errno(r, "mDNS browse query complete failed, %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);
|
||||
|
||||
sb = dns_service_browser_ref(userdata);
|
||||
|
||||
r = dns_query_new(sb->m, &q, sb->question_utf8, sb->question_idna, NULL, sb->ifindex, sb->flags);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
q->complete = mdns_browse_service_query_complete;
|
||||
q->varlink_request = sd_varlink_ref(sb->link);
|
||||
sd_varlink_set_userdata(sb->link, q);
|
||||
|
||||
r = dns_query_go(q);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
/* 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 */
|
||||
if (sb->delay == 0) {
|
||||
sb->delay++;
|
||||
/* First query is sent wihtout SD_RESOLVED_NO_CACHE to fetch answers already in cache.
|
||||
* Set SD_RESOLVED_NO_CACHE to make all subsequent queries go to the network. */
|
||||
sb->flags |= SD_RESOLVED_NO_CACHE;
|
||||
}
|
||||
else
|
||||
sb->delay = sb->delay < 2048 ? sb->delay * 2 : 3600;
|
||||
|
||||
r = event_reset_time_relative(
|
||||
sb->m->event, &sb->schedule_event,
|
||||
CLOCK_BOOTTIME, (sb->delay * USEC_PER_SEC),
|
||||
0, mdns_next_query_schedule,
|
||||
sb, 0, "mdns-next-query-schedule", true);
|
||||
if (r < 0)
|
||||
goto finish;
|
||||
|
||||
TAKE_PTR(q);
|
||||
|
||||
return 0;
|
||||
|
||||
finish:
|
||||
return log_error_errno(r, "Failed to schedule mDNS query, %m");
|
||||
}
|
||||
|
||||
void dns_service_browser_reset(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->m->event, &sb->schedule_event,
|
||||
CLOCK_BOOTTIME, (sb->delay * USEC_PER_SEC),
|
||||
0, mdns_next_query_schedule,
|
||||
sb, 0, "mdns-next-query-schedule", true);
|
||||
if (r < 0)
|
||||
log_error_errno(r, "Failed to reset mdns service subscriber, %m");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int dns_subscribe_browse_service(
|
||||
Manager *m,
|
||||
sd_varlink *link,
|
||||
const char *domain,
|
||||
const char *name,
|
||||
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(link, JSON_VARIANT_STRING_CONST("ifindex"));
|
||||
|
||||
if (isempty(name))
|
||||
name = NULL;
|
||||
else if (!dns_service_name_is_valid(name))
|
||||
return sd_varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("name"));
|
||||
|
||||
if (isempty(type))
|
||||
type = NULL;
|
||||
else if (!dnssd_srv_type_is_valid(type))
|
||||
return sd_varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("type"));
|
||||
|
||||
r = dns_name_is_valid(domain);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return sd_varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("domain"));
|
||||
|
||||
r = dns_question_new_service_type(&question_utf8, name, type, domain, false, DNS_TYPE_PTR);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = dns_question_new_service_type(&question_idna, name, type, domain, true, DNS_TYPE_PTR);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
sb = new(DnsServiceBrowser, 1);
|
||||
if (!sb)
|
||||
return log_oom();
|
||||
|
||||
*sb = (DnsServiceBrowser) {
|
||||
.n_ref = 1,
|
||||
.m = 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,
|
||||
};
|
||||
|
||||
/* Only mDNS continuous querying is currently supported. See RFC 6762 */
|
||||
switch (flags & SD_RESOLVED_PROTOCOLS_ALL) {
|
||||
case SD_RESOLVED_MDNS:
|
||||
r = sd_event_add_time(m->event,
|
||||
&sb->schedule_event,
|
||||
CLOCK_BOOTTIME,
|
||||
usec_add(now(CLOCK_BOOTTIME), (sb->delay * USEC_PER_SEC)),
|
||||
0,
|
||||
mdns_next_query_schedule,
|
||||
sb);
|
||||
if (r < 0)
|
||||
return r;
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
r = hashmap_ensure_put(&m->dns_service_browsers, NULL, link, sb);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
TAKE_PTR(sb);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
DnsServiceBrowser *dns_service_browser_free(DnsServiceBrowser *sb) {
|
||||
DnsQuery *q;
|
||||
|
||||
if (!sb)
|
||||
return NULL;
|
||||
|
||||
LIST_FOREACH(dns_services, service, sb->dns_services)
|
||||
dns_remove_service(sb, service);
|
||||
|
||||
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);
|
||||
|
||||
dns_question_unref(sb->question_idna);
|
||||
dns_question_unref(sb->question_utf8);
|
||||
|
||||
sd_varlink_unref(sb->link);
|
||||
|
||||
return mfree(sb);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsServiceBrowser, dns_service_browser, dns_service_browser_free);
|
|
@ -0,0 +1,79 @@
|
|||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
typedef struct DnsServiceBrowser DnsServiceBrowser;
|
||||
|
||||
#include "resolved-dns-query.h"
|
||||
#include "resolved-manager.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
typedef struct DnsService DnsService;
|
||||
|
||||
typedef enum DnsRecordTTLState DnsRecordTTLState;
|
||||
|
||||
enum DnsRecordTTLState {
|
||||
MDNS_TTL_80_PERCENT,
|
||||
MDNS_TTL_85_PERCENT,
|
||||
MDNS_TTL_90_PERCENT,
|
||||
MDNS_TTL_95_PERCENT,
|
||||
MDNS_TTL_100_PERCENT
|
||||
};
|
||||
|
||||
struct DnsService {
|
||||
unsigned n_ref;
|
||||
DnsServiceBrowser *sb;
|
||||
sd_event_source *schedule_event;
|
||||
DnsResourceRecord *rr;
|
||||
int family;
|
||||
usec_t until;
|
||||
DnsRecordTTLState rr_ttl_state;
|
||||
DnsQuery *query;
|
||||
LIST_FIELDS(DnsService, dns_services);
|
||||
};
|
||||
|
||||
struct DnsServiceBrowser {
|
||||
unsigned n_ref;
|
||||
Manager *m;
|
||||
sd_varlink *link;
|
||||
DnsQuestion *question_idna;
|
||||
DnsQuestion *question_utf8;
|
||||
uint64_t flags;
|
||||
sd_event_source *schedule_event;
|
||||
usec_t delay;
|
||||
DnsResourceKey *key;
|
||||
int ifindex;
|
||||
uint64_t token;
|
||||
LIST_HEAD(DnsService, dns_services);
|
||||
};
|
||||
|
||||
DnsServiceBrowser *dns_service_browser_free(DnsServiceBrowser *sb);
|
||||
void dns_remove_service(DnsServiceBrowser *sb, DnsService *service);
|
||||
DnsService *dns_service_free(DnsService *service);
|
||||
|
||||
DnsServiceBrowser* dns_service_browser_ref(DnsServiceBrowser *sb);
|
||||
DnsServiceBrowser* dns_service_browser_unref(DnsServiceBrowser *sb);
|
||||
|
||||
DnsService* dns_service_ref(DnsService *service);
|
||||
DnsService* dns_service_unref(DnsService *service);
|
||||
|
||||
void dns_browse_services_purge(Manager *m, int family);
|
||||
void dns_service_browser_reset(Manager *m);
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(DnsServiceBrowser*, dns_service_browser_unref);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(DnsService*, dns_service_unref);
|
||||
|
||||
bool dns_service_contains(DnsService *services, DnsResourceRecord *rr, int owner_family);
|
||||
int mdns_manage_services_answer(DnsServiceBrowser *sb, DnsAnswer *answer, int owner_family);
|
||||
int dns_add_new_service(DnsServiceBrowser *sb, DnsResourceRecord *rr, int owner_family);
|
||||
int mdns_service_update(DnsService *service, DnsResourceRecord *rr, usec_t t);
|
||||
int mdns_browser_lookup_cache(DnsServiceBrowser *sb, int owner_family);
|
||||
int dns_subscribe_browse_service(Manager *m,
|
||||
sd_varlink *link,
|
||||
const char *domain,
|
||||
const char * name,
|
||||
const char * type,
|
||||
int ifindex,
|
||||
uint64_t flags);
|
||||
int mdns_notify_browsers_unsolicited_updates(Manager *m, DnsAnswer *answer, int owner_family);
|
||||
int mdns_notify_browsers_goodbye(DnsScope *scope);
|
|
@ -1011,6 +1011,7 @@ static int answer_add_clamp_ttl(
|
|||
}
|
||||
}
|
||||
|
||||
rr->until = until;
|
||||
r = dns_answer_add_extend(answer, rr, ifindex, answer_flags, rrsig);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
|
|
@ -416,6 +416,66 @@ int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_
|
|||
return 0;
|
||||
}
|
||||
|
||||
int dns_question_new_service_type(
|
||||
DnsQuestion **ret,
|
||||
const char *service,
|
||||
const char *type,
|
||||
const char *domain,
|
||||
bool convert_idna,
|
||||
uint16_t record_type) {
|
||||
|
||||
_cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
|
||||
_cleanup_(dns_question_unrefp) DnsQuestion *q = NULL;
|
||||
_cleanup_free_ char *buf = NULL, *joined = NULL;
|
||||
const char *name;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
if (record_type == DNS_TYPE_SRV)
|
||||
return -EINVAL;
|
||||
|
||||
if (!domain)
|
||||
return -EINVAL;
|
||||
|
||||
if (type) {
|
||||
if (convert_idna) {
|
||||
r = dns_name_apply_idna(domain, &buf);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
domain = buf;
|
||||
}
|
||||
|
||||
r = dns_service_join(service, type, domain, &joined);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
name = joined;
|
||||
} else {
|
||||
if (service)
|
||||
return -EINVAL;
|
||||
|
||||
name = domain;
|
||||
}
|
||||
|
||||
q = dns_question_new(1);
|
||||
if (!q)
|
||||
return -ENOMEM;
|
||||
|
||||
key = dns_resource_key_new(DNS_CLASS_IN, record_type, name);
|
||||
if (!key)
|
||||
return -ENOMEM;
|
||||
|
||||
r = dns_question_add(q, key, 0);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_PTR(q);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int dns_question_new_service(
|
||||
DnsQuestion **ret,
|
||||
const char *service,
|
||||
|
|
|
@ -31,6 +31,7 @@ DnsQuestion *dns_question_unref(DnsQuestion *q);
|
|||
int dns_question_new_address(DnsQuestion **ret, int family, const char *name, bool convert_idna);
|
||||
int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_union *a);
|
||||
int dns_question_new_service(DnsQuestion **ret, const char *service, const char *type, const char *domain, bool with_txt, bool convert_idna);
|
||||
int dns_question_new_service_type(DnsQuestion **ret, const char *service, const char *type, const char *domain, bool convert_idna, uint16_t record_type);
|
||||
|
||||
int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags);
|
||||
int dns_question_add(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags);
|
||||
|
|
|
@ -398,6 +398,7 @@ DnsResourceRecord* dns_resource_record_new(DnsResourceKey *key) {
|
|||
.n_ref = 1,
|
||||
.key = dns_resource_key_ref(key),
|
||||
.expiry = USEC_INFINITY,
|
||||
.until = USEC_INFINITY,
|
||||
.n_skip_labels_signer = UINT8_MAX,
|
||||
.n_skip_labels_source = UINT8_MAX,
|
||||
};
|
||||
|
@ -1704,6 +1705,7 @@ DnsResourceRecord *dns_resource_record_copy(DnsResourceRecord *rr) {
|
|||
|
||||
copy->ttl = rr->ttl;
|
||||
copy->expiry = rr->expiry;
|
||||
copy->until = rr->until;
|
||||
copy->n_skip_labels_signer = rr->n_skip_labels_signer;
|
||||
copy->n_skip_labels_source = rr->n_skip_labels_source;
|
||||
copy->unparsable = rr->unparsable;
|
||||
|
|
|
@ -107,6 +107,8 @@ struct DnsResourceRecord {
|
|||
unsigned n_ref;
|
||||
uint32_t ttl;
|
||||
usec_t expiry; /* RRSIG signature expiry */
|
||||
usec_t until; /* Used to pass until of a record when doing a dns_cache_lookup().
|
||||
* Needed to schedule cache maintenance queries when browsing for services. */
|
||||
|
||||
DnsResourceKey *key;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "hostname-util.h"
|
||||
#include "missing_network.h"
|
||||
#include "random-util.h"
|
||||
#include "resolved-dns-browse-services.h"
|
||||
#include "resolved-dnssd.h"
|
||||
#include "resolved-dns-scope.h"
|
||||
#include "resolved-dns-synthesize.h"
|
||||
|
@ -121,6 +122,9 @@ DnsScope* dns_scope_free(DnsScope *s) {
|
|||
dns_cache_flush(&s->cache);
|
||||
dns_zone_flush(&s->zone);
|
||||
|
||||
/* Clear records of mDNS service browse subscriber, since cache bas been flushed */
|
||||
dns_browse_services_purge(s->manager, s->family);
|
||||
|
||||
LIST_REMOVE(scopes, s->manager->dns_scopes, s);
|
||||
return mfree(s);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#include "mkdir.h"
|
||||
#include "netif-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "resolved-dns-browse-services.h"
|
||||
#include "resolved-link.h"
|
||||
#include "resolved-llmnr.h"
|
||||
#include "resolved-mdns.h"
|
||||
|
@ -166,6 +167,7 @@ void link_allocate_scopes(Link *l) {
|
|||
r = dns_scope_new(l->manager, &l->mdns_ipv4_scope, l, DNS_PROTOCOL_MDNS, AF_INET);
|
||||
if (r < 0)
|
||||
log_link_warning_errno(l, r, "Failed to allocate mDNS IPv4 scope, ignoring: %m");
|
||||
dns_service_browser_reset(l->manager);
|
||||
}
|
||||
} else
|
||||
l->mdns_ipv4_scope = dns_scope_free(l->mdns_ipv4_scope);
|
||||
|
@ -176,6 +178,7 @@ void link_allocate_scopes(Link *l) {
|
|||
r = dns_scope_new(l->manager, &l->mdns_ipv6_scope, l, DNS_PROTOCOL_MDNS, AF_INET6);
|
||||
if (r < 0)
|
||||
log_link_warning_errno(l, r, "Failed to allocate mDNS IPv6 scope, ignoring: %m");
|
||||
dns_service_browser_reset(l->manager);
|
||||
}
|
||||
} else
|
||||
l->mdns_ipv6_scope = dns_scope_free(l->mdns_ipv6_scope);
|
||||
|
|
|
@ -769,6 +769,7 @@ int manager_start(Manager *m) {
|
|||
Manager *manager_free(Manager *m) {
|
||||
Link *l;
|
||||
DnssdService *s;
|
||||
DnsServiceBrowser *sb;
|
||||
|
||||
if (!m)
|
||||
return NULL;
|
||||
|
@ -840,6 +841,10 @@ Manager *manager_free(Manager *m) {
|
|||
dns_trust_anchor_flush(&m->trust_anchor);
|
||||
manager_etc_hosts_flush(m);
|
||||
|
||||
while ((sb = hashmap_first(m->dns_service_browsers)))
|
||||
dns_service_browser_free(sb);
|
||||
hashmap_free(m->dns_service_browsers);
|
||||
|
||||
return mfree(m);
|
||||
}
|
||||
|
||||
|
@ -1474,29 +1479,28 @@ bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p) {
|
|||
return t->sent && dns_packet_equal(t->sent, p);
|
||||
}
|
||||
|
||||
DnsScope* manager_find_scope(Manager *m, DnsPacket *p) {
|
||||
DnsScope* manager_find_scope_from_protocol(Manager *m, int ifindex, DnsProtocol protocol, int family) {
|
||||
Link *l;
|
||||
|
||||
assert(m);
|
||||
assert(p);
|
||||
|
||||
l = hashmap_get(m->links, INT_TO_PTR(p->ifindex));
|
||||
l = hashmap_get(m->links, INT_TO_PTR(ifindex));
|
||||
if (!l)
|
||||
return NULL;
|
||||
|
||||
switch (p->protocol) {
|
||||
switch (protocol) {
|
||||
case DNS_PROTOCOL_LLMNR:
|
||||
if (p->family == AF_INET)
|
||||
if (family == AF_INET)
|
||||
return l->llmnr_ipv4_scope;
|
||||
else if (p->family == AF_INET6)
|
||||
else if (family == AF_INET6)
|
||||
return l->llmnr_ipv6_scope;
|
||||
|
||||
break;
|
||||
|
||||
case DNS_PROTOCOL_MDNS:
|
||||
if (p->family == AF_INET)
|
||||
if (family == AF_INET)
|
||||
return l->mdns_ipv4_scope;
|
||||
else if (p->family == AF_INET6)
|
||||
else if (family == AF_INET6)
|
||||
return l->mdns_ipv6_scope;
|
||||
|
||||
break;
|
||||
|
@ -1706,6 +1710,9 @@ void manager_flush_caches(Manager *m, int log_level) {
|
|||
LIST_FOREACH(scopes, scope, m->dns_scopes)
|
||||
dns_cache_flush(&scope->cache);
|
||||
|
||||
dns_browse_services_purge(m, AF_UNSPEC); /* Clear records of DNS service browse subscriber, since caches are flushed */
|
||||
dns_service_browser_reset(m);
|
||||
|
||||
log_full(log_level, "Flushed all caches.");
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
typedef struct Manager Manager;
|
||||
|
||||
#include "resolved-dns-browse-services.h"
|
||||
#include "resolved-dns-query.h"
|
||||
#include "resolved-dns-search-domain.h"
|
||||
#include "resolved-dns-stream.h"
|
||||
|
@ -161,6 +162,9 @@ struct Manager {
|
|||
size_t n_socket_graveyard;
|
||||
|
||||
struct sigrtmin18_info sigrtmin18_info;
|
||||
|
||||
/* Map varlink links to DnsServiceBrowser instances. */
|
||||
Hashmap *dns_service_browsers;
|
||||
};
|
||||
|
||||
/* Manager */
|
||||
|
@ -188,7 +192,13 @@ int manager_next_hostname(Manager *m);
|
|||
bool manager_packet_from_local_address(Manager *m, DnsPacket *p);
|
||||
bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p);
|
||||
|
||||
DnsScope* manager_find_scope(Manager *m, DnsPacket *p);
|
||||
DnsScope* manager_find_scope_from_protocol(Manager *m, int ifindex, DnsProtocol protocol, int family);
|
||||
|
||||
static inline DnsScope* manager_find_scope(Manager *m, DnsPacket *p) {
|
||||
assert(m);
|
||||
assert(p);
|
||||
return manager_find_scope_from_protocol(m, p->ifindex, p->protocol, p->family);
|
||||
}
|
||||
|
||||
void manager_verify_all(Manager *m);
|
||||
|
||||
|
|
|
@ -359,6 +359,10 @@ static int mdns_goodbye_callback(sd_event_source *s, uint64_t usec, void *userda
|
|||
|
||||
dns_cache_prune(&scope->cache);
|
||||
|
||||
r = mdns_notify_browsers_goodbye(scope);
|
||||
if (r < 0)
|
||||
log_error_errno(r, "mDNS: Failed to notify service subscribers of goodbyes, %m");
|
||||
|
||||
if (dns_cache_expiry_in_one_second(&scope->cache, usec)) {
|
||||
r = sd_event_add_time_relative(
|
||||
scope->manager->event,
|
||||
|
@ -380,6 +384,7 @@ static int on_mdns_packet(sd_event_source *s, int fd, uint32_t revents, void *us
|
|||
Manager *m = userdata;
|
||||
DnsScope *scope;
|
||||
int r;
|
||||
bool unsolicited_packet = true;
|
||||
|
||||
r = manager_recv(m, fd, DNS_PROTOCOL_MDNS, &p);
|
||||
if (r <= 0)
|
||||
|
@ -455,30 +460,6 @@ static int on_mdns_packet(sd_event_source *s, int fd, uint32_t revents, void *us
|
|||
}
|
||||
}
|
||||
|
||||
for (bool match = true; match;) {
|
||||
match = false;
|
||||
LIST_FOREACH(transactions_by_scope, t, scope->transactions) {
|
||||
if (t->state != DNS_TRANSACTION_PENDING)
|
||||
continue;
|
||||
|
||||
r = dns_answer_match_key(p->answer, dns_transaction_key(t), NULL);
|
||||
if (r <= 0) {
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to match resource key, ignoring: %m");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* This packet matches the transaction, let's pass it on as reply */
|
||||
dns_transaction_process_reply(t, p, false);
|
||||
|
||||
/* The dns_transaction_process_reply() -> dns_transaction_complete() ->
|
||||
* dns_query_candidate_stop() may free multiple transactions. Hence, restart
|
||||
* the loop. */
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dns_cache_put(
|
||||
&scope->cache,
|
||||
scope->manager->enable_cache,
|
||||
|
@ -494,6 +475,35 @@ static int on_mdns_packet(sd_event_source *s, int fd, uint32_t revents, void *us
|
|||
&p->sender,
|
||||
scope->manager->stale_retention_usec);
|
||||
|
||||
for (bool match = true; match;) {
|
||||
match = false;
|
||||
LIST_FOREACH(transactions_by_scope, t, scope->transactions) {
|
||||
if (t->state != DNS_TRANSACTION_PENDING)
|
||||
continue;
|
||||
|
||||
r = dns_answer_match_key(p->answer, dns_transaction_key(t), NULL);
|
||||
if (r <= 0) {
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to match resource key, ignoring: %m");
|
||||
continue;
|
||||
}
|
||||
|
||||
unsolicited_packet = false;
|
||||
/* This packet matches the transaction, let's pass it on as reply */
|
||||
dns_transaction_process_reply(t, p, false);
|
||||
|
||||
/* The dns_transaction_process_reply() -> dns_transaction_complete() ->
|
||||
* dns_query_candidate_stop() may free multiple transactions. Hence, restart
|
||||
* the loop. */
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check incoming packet key matches with active clients if yes update the same */
|
||||
if (unsolicited_packet)
|
||||
mdns_notify_browsers_unsolicited_updates(m, p->answer, p->family);
|
||||
|
||||
} else if (dns_packet_validate_query(p) > 0) {
|
||||
log_debug("Got mDNS query packet for id %u", DNS_PACKET_ID(p));
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "glyph-util.h"
|
||||
#include "in-addr-util.h"
|
||||
#include "json-util.h"
|
||||
#include "resolved-dns-browse-services.h"
|
||||
#include "resolved-dns-synthesize.h"
|
||||
#include "resolved-varlink.h"
|
||||
#include "socket-netlink.h"
|
||||
|
@ -30,11 +31,26 @@ typedef struct LookupParametersResolveService {
|
|||
uint64_t flags;
|
||||
} LookupParametersResolveService;
|
||||
|
||||
typedef struct LookupParametersMdnsBrowse {
|
||||
char *domainName;
|
||||
char *name;
|
||||
char *type;
|
||||
int ifindex;
|
||||
uint64_t flags;
|
||||
} LookupParametersMdnsBrowse;
|
||||
|
||||
static void lookup_parameters_destroy(LookupParameters *p) {
|
||||
assert(p);
|
||||
free(p->name);
|
||||
}
|
||||
|
||||
static void lookup_parameters_mdns_destroy(LookupParametersMdnsBrowse *p) {
|
||||
assert(p);
|
||||
free(p->domainName);
|
||||
free(p->name);
|
||||
free(p->type);
|
||||
}
|
||||
|
||||
static int reply_query_state(DnsQuery *q) {
|
||||
|
||||
assert(q);
|
||||
|
@ -108,10 +124,18 @@ static int reply_query_state(DnsQuery *q) {
|
|||
|
||||
static void vl_on_disconnect(sd_varlink_server *s, sd_varlink *link, void *userdata) {
|
||||
DnsQuery *q;
|
||||
Manager *m;
|
||||
|
||||
assert(s);
|
||||
assert(link);
|
||||
|
||||
m = sd_varlink_server_get_userdata(s);
|
||||
if (!m)
|
||||
return;
|
||||
|
||||
DnsServiceBrowser *sb = hashmap_remove(m->dns_service_browsers, link);
|
||||
dns_service_browser_free(sb);
|
||||
|
||||
q = sd_varlink_get_userdata(link);
|
||||
if (!q)
|
||||
return;
|
||||
|
@ -1209,6 +1233,43 @@ static int verify_polkit(sd_varlink *link, sd_json_variant *parameters, const ch
|
|||
&m->polkit_registry);
|
||||
}
|
||||
|
||||
static int vl_method_start_browse(sd_varlink* link, sd_json_variant* parameters, sd_varlink_method_flags_t flags, void* userdata) {
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "domainName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(LookupParametersMdnsBrowse, domainName), SD_JSON_MANDATORY },
|
||||
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(LookupParametersMdnsBrowse, name), 0 },
|
||||
{ "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(LookupParametersMdnsBrowse, type), 0 },
|
||||
{ "ifindex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int, offsetof(LookupParametersMdnsBrowse, ifindex), SD_JSON_MANDATORY },
|
||||
{ "flags", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(LookupParametersMdnsBrowse, flags), SD_JSON_MANDATORY },
|
||||
{}
|
||||
};
|
||||
|
||||
_cleanup_(lookup_parameters_mdns_destroy) LookupParametersMdnsBrowse p = {};
|
||||
Manager *m;
|
||||
int r = 0;
|
||||
|
||||
assert(link);
|
||||
|
||||
/* if the client didn't set the more flag, it is using us incorrectly */
|
||||
if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
|
||||
return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL);
|
||||
|
||||
m = sd_varlink_server_get_userdata(sd_varlink_get_server(link));
|
||||
assert(m);
|
||||
|
||||
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "vl_method_start_browse json_dispatch fail: %m");
|
||||
|
||||
if (!validate_and_mangle_flags(NULL, &p.flags, 0))
|
||||
return sd_varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("flags"));
|
||||
|
||||
r = dns_subscribe_browse_service(m, link, p.domainName, p.name, p.type, p.ifindex, p.flags);
|
||||
if (r < 0)
|
||||
return sd_varlink_error_errno(link, r);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int vl_method_subscribe_query_results(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
Manager *m = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link)));
|
||||
int r;
|
||||
|
@ -1422,7 +1483,8 @@ static int varlink_main_server_init(Manager *m) {
|
|||
"io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname,
|
||||
"io.systemd.Resolve.ResolveAddress", vl_method_resolve_address,
|
||||
"io.systemd.Resolve.ResolveService", vl_method_resolve_service,
|
||||
"io.systemd.Resolve.ResolveRecord", vl_method_resolve_record);
|
||||
"io.systemd.Resolve.ResolveRecord", vl_method_resolve_record,
|
||||
"io.systemd.Resolve.StartBrowse", vl_method_start_browse);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to register varlink methods: %m");
|
||||
|
||||
|
|
|
@ -102,6 +102,15 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE(
|
|||
SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0));
|
||||
|
||||
static SD_VARLINK_DEFINE_STRUCT_TYPE(
|
||||
ServiceData,
|
||||
SD_VARLINK_DEFINE_FIELD(add_flag, SD_VARLINK_BOOL, 0),
|
||||
SD_VARLINK_DEFINE_FIELD(family, SD_VARLINK_INT, 0),
|
||||
SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_DEFINE_FIELD(type, SD_VARLINK_STRING, 0),
|
||||
SD_VARLINK_DEFINE_FIELD(domain, SD_VARLINK_STRING, 0),
|
||||
SD_VARLINK_DEFINE_FIELD(interface, 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."),
|
||||
|
@ -159,6 +168,15 @@ static SD_VARLINK_DEFINE_METHOD(
|
|||
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(
|
||||
StartBrowse,
|
||||
SD_VARLINK_DEFINE_INPUT(domainName, SD_VARLINK_STRING, 0),
|
||||
SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_DEFINE_INPUT(type, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_DEFINE_INPUT(ifindex, SD_VARLINK_INT, 0),
|
||||
SD_VARLINK_DEFINE_INPUT(flags, SD_VARLINK_INT, 0),
|
||||
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(browser_service_data, ServiceData, SD_VARLINK_ARRAY));
|
||||
|
||||
static SD_VARLINK_DEFINE_ERROR(NoNameServers);
|
||||
static SD_VARLINK_DEFINE_ERROR(NoSuchResourceRecord);
|
||||
static SD_VARLINK_DEFINE_ERROR(QueryTimedOut);
|
||||
|
@ -198,6 +216,8 @@ SD_VARLINK_DEFINE_INTERFACE(
|
|||
&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 mDNS services of specified type."),
|
||||
&vl_method_StartBrowse,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a resolved address."),
|
||||
&vl_type_ResolvedAddress,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a resolved host name."),
|
||||
|
@ -212,6 +232,7 @@ SD_VARLINK_DEFINE_INTERFACE(
|
|||
&vl_type_ResourceRecord,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates information about a resolved DNS resource record "),
|
||||
&vl_type_ResolvedRecord,
|
||||
&vl_type_ServiceData,
|
||||
&vl_error_NoNameServers,
|
||||
&vl_error_NoSuchResourceRecord,
|
||||
&vl_error_QueryTimedOut,
|
||||
|
|
|
@ -522,6 +522,7 @@ static int reply_callback(
|
|||
if (!arg_quiet)
|
||||
sd_json_variant_dump(parameters, arg_json_format_flags, stdout, NULL);
|
||||
|
||||
fflush(stdout);
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
all setup run clean clean-again:
|
||||
@TEST_BASE_DIR=../ ./test.sh --$@
|
||||
|
||||
.PHONY: all setup run clean clean-again
|
|
@ -0,0 +1,8 @@
|
|||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
integration_tests += [
|
||||
integration_test_template + {
|
||||
'name' : fs.name(meson.current_source_dir()),
|
||||
'vm' : true,
|
||||
},
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
set -e
|
||||
|
||||
TEST_DESCRIPTION="Test for systemd-resolved's mDNS functionality"
|
||||
IMAGE_NAME="resolved-mdns"
|
||||
TEST_NO_NSPAWN=1
|
||||
|
||||
# shellcheck source=test/test-functions
|
||||
. "${TEST_BASE_DIR:?}/test-functions"
|
||||
|
||||
do_test "$@"
|
|
@ -377,6 +377,7 @@ foreach dirname : [
|
|||
'TEST-84-STORAGETM',
|
||||
'TEST-85-NETWORK',
|
||||
'TEST-86-MULTI-PROFILE-UKI',
|
||||
'TEST-87-RESOLVED-MDNS',
|
||||
]
|
||||
subdir(dirname)
|
||||
endforeach
|
||||
|
|
|
@ -80,6 +80,7 @@ show_journal = True # When true, show journal on stopping networkd.
|
|||
|
||||
active_units = []
|
||||
protected_links = {
|
||||
'br0',
|
||||
'erspan0',
|
||||
'gre0',
|
||||
'gretap0',
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
[Unit]
|
||||
Description=TEST-87-RESOLVED-MDNS
|
||||
|
||||
[Service]
|
||||
ExecStartPre=rm -f /failed /testok
|
||||
ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
|
||||
Type=oneshot
|
|
@ -0,0 +1,255 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
set -eux
|
||||
set -o pipefail
|
||||
|
||||
# shellcheck source=test/units/test-control.sh
|
||||
. "$(dirname "$0")"/test-control.sh
|
||||
|
||||
SERVICE_TYPE_COUNT=10
|
||||
SERVICE_COUNT=20
|
||||
CONTAINER_ZONE="test-$RANDOM"
|
||||
CONTAINER_1="test-mdns-1"
|
||||
CONTAINER_2="test-mdns-2"
|
||||
|
||||
# Prepare containers
|
||||
create_container() {
|
||||
local container="${1:?}"
|
||||
local stype sid svc
|
||||
|
||||
# Prepare container's /etc
|
||||
#
|
||||
# Since we also need the various test suite related dropins from the host's /etc,
|
||||
# we'll overlay our customizations on top of that
|
||||
mkdir -p "/var/lib/machines/$container/etc/systemd/dnssd"
|
||||
# Create 20 test services for each service type (_testServiceX._udp) and number them sequentially,
|
||||
# i.e. create services 0-19 for _testService0._udp, services 20-39 for _testService1._udp, and so on
|
||||
for stype in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
for sid in $(seq 0 $((SERVICE_COUNT - 1))); do
|
||||
svc=$((stype * SERVICE_COUNT + sid))
|
||||
|
||||
cat >"/var/lib/machines/$container/etc/systemd/dnssd/test-service-$container-$svc.dnssd" <<EOF
|
||||
[Service]
|
||||
Name=Test Service $svc on %H
|
||||
Type=_testService$stype._udp
|
||||
Port=24002
|
||||
TxtText=DC=Monitor PN=867313 SN=XZ051Z0051
|
||||
EOF
|
||||
done
|
||||
done
|
||||
|
||||
# To make things fast, spawn the container with a transient version of what's currently the host's
|
||||
# rootfs, sans a couple of tweaks to make the container unique enough
|
||||
mkdir -p "/run/systemd/system/systemd-nspawn@$container.service.d"
|
||||
cat >"/run/systemd/system/systemd-nspawn@$container.service.d/override.conf" <<EOF
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=systemd-nspawn --quiet --link-journal=try-guest --keep-unit --machine=%i --boot \
|
||||
--volatile=yes --directory=/ \
|
||||
--inaccessible=/etc/machine-id \
|
||||
--inaccessible=/etc/hostname \
|
||||
--resolv-conf=replace-stub \
|
||||
--network-zone=$CONTAINER_ZONE \
|
||||
--overlay=/etc:/var/lib/machines/$container/etc::/etc \
|
||||
--hostname=$container
|
||||
EOF
|
||||
}
|
||||
|
||||
check_both() {
|
||||
local service_id="${1:?}"
|
||||
local result_file="${2:?}"
|
||||
|
||||
# We should get 20 services per container, 40 total
|
||||
if [[ "$(wc -l <"$result_file")" -ge 40 ]]; then
|
||||
# Check if the services we got are the correct ones
|
||||
for i in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
svc=$((service_id * SERVICE_COUNT + i))
|
||||
if ! grep "Test Service $svc on $CONTAINER_1" "$result_file" || \
|
||||
! grep "Test Service $svc on $CONTAINER_2" "$result_file"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# We got all records and all of them are what we expect
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
check_first() {
|
||||
local service_id="${1:?}"
|
||||
local result_file="${2:?}"
|
||||
|
||||
# We should get 20 services per container
|
||||
if [[ "$(wc -l <"$result_file")" -ge 20 ]]; then
|
||||
# Check if the services we got are the correct ones
|
||||
for i in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
svc=$((service_id * SERVICE_COUNT + i))
|
||||
if ! grep "Test Service $svc on $CONTAINER_1" "$result_file"; then
|
||||
return 1
|
||||
fi
|
||||
# This check assumes the second container is unreachable, so this shouldn't happen
|
||||
if grep "Test Service $svc on $CONTAINER_2" "$result_file"; then
|
||||
echo >&2 "Found a record from an unreachable container"
|
||||
cat "$result_file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# We got all records and all of them are what we expect
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
run_and_check_services() {
|
||||
local service_id="${1:?}"
|
||||
local check_func="${2:?}"
|
||||
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="{ \"domainName\": \"$service_type.local\", \"name\": \"\", \"type\": \"\", \"ifindex\": ${BRIDGE_INDEX:?}, \"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.StartBrowse "$parameters"
|
||||
|
||||
# shellcheck disable=SC2064
|
||||
# Note: unregister the trap once it's fired, otherwise it'll get propagated to functions that call this
|
||||
# one, *sigh*
|
||||
|
||||
trap "trap - RETURN; systemctl stop $unit_name" RETURN
|
||||
|
||||
for _ in {0..14}; do
|
||||
# The response format, for reference (it's JSON-SEQ):
|
||||
#
|
||||
# {
|
||||
# "browser_service_data": [
|
||||
# {
|
||||
# "add_flag": true,
|
||||
# "family": 10,
|
||||
# "name": "Test Service 13 on test-mdns-1",
|
||||
# "type": "_testService0._udp",
|
||||
# "domain": "local",
|
||||
# "interface": 3
|
||||
# },
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
if [[ -s "$out_file" ]]; then
|
||||
# Extract the service name from each valid record...
|
||||
# jq --slurp --raw-output \
|
||||
# ".[].browser_service_data[] | select(.add_flag == true and .type == \"$service_type\" and .family == 10).name" "$out_file" | sort | tee "$tmp_file"
|
||||
grep -o '"name":"[^"]*"' "$out_file" | sed 's/"name":"//;s/"//g' | sort | tee "$tmp_file"
|
||||
# ...and compare them with what we expect
|
||||
if "$check_func" "$service_id" "$tmp_file"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
|
||||
cat "$out_file"
|
||||
cat "$error_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
testcase_all_sequential() {
|
||||
: "Test each service type (sequentially)"
|
||||
resolvectl flush-caches
|
||||
for id in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
run_and_check_services "$id" check_both
|
||||
done
|
||||
|
||||
echo testcase_end
|
||||
}
|
||||
|
||||
testcase_all_parallel() {
|
||||
: "Test each service type (in parallel)"
|
||||
resolvectl flush-caches
|
||||
for id in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
run_and_check_services "$id" check_both &
|
||||
done
|
||||
wait
|
||||
}
|
||||
|
||||
testcase_single_service_multiple_times() {
|
||||
: "Test one service type multiple times"
|
||||
resolvectl flush-caches
|
||||
for _ in {0..4}; do
|
||||
run_and_check_services 4 check_both
|
||||
done
|
||||
}
|
||||
|
||||
testcase_second_unreachable() {
|
||||
: "Test each service type while the second container is unreachable"
|
||||
resolvectl flush-caches
|
||||
systemd-run -M "$CONTAINER_2" --wait --pipe -- networkctl down host0
|
||||
for id in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
run_and_check_services "$id" check_first
|
||||
done
|
||||
|
||||
|
||||
: "Test each service type after bringing the second container back up again"
|
||||
systemd-run -M "$CONTAINER_2" --wait --pipe -- networkctl up host0
|
||||
systemd-run -M "$CONTAINER_2" --wait --pipe -- \
|
||||
/usr/lib/systemd/systemd-networkd-wait-online --ipv4 --ipv6 --interface=host0 --operational-state=degraded --timeout=30
|
||||
for id in $(seq 0 $((SERVICE_TYPE_COUNT - 1))); do
|
||||
run_and_check_services "$id" check_both
|
||||
done
|
||||
}
|
||||
|
||||
: "Setup host & containers"
|
||||
# Note: create the drop-in intentionally under /run/ and copy it manually into the containers
|
||||
mkdir -p /run/systemd/resolved.conf.d/
|
||||
cat >/run/systemd/resolved.conf.d/99-mdns-llmnr.conf <<EOF
|
||||
[Resolve]
|
||||
MulticastDNS=yes
|
||||
LLMNR=yes
|
||||
EOF
|
||||
|
||||
systemctl unmask systemd-resolved.service systemd-networkd.{service,socket} systemd-machined.service
|
||||
systemctl enable --now systemd-resolved.service systemd-networkd.{socket,service} systemd-machined.service
|
||||
ln -svrf /run/systemd/resolved.conf.d/99-mdns-llmnr.conf /etc/resolv.conf
|
||||
systemctl reload systemd-resolved.service systemd-networkd.service
|
||||
|
||||
for container in "$CONTAINER_1" "$CONTAINER_2"; do
|
||||
create_container "$container"
|
||||
mkdir -p "/var/lib/machines/$container/etc/systemd/resolved.conf.d/"
|
||||
cp /run/systemd/resolved.conf.d/99-mdns-llmnr.conf "/var/lib/machines/$container/etc/systemd/resolved.conf.d/"
|
||||
touch "/var/lib/machines/$container/etc/hostname"
|
||||
systemctl daemon-reload
|
||||
machinectl start "$container"
|
||||
# Wait for the system bus to start...
|
||||
timeout 30s bash -xec "while ! systemd-run -M '$container' --wait --pipe true; do sleep 1; done"
|
||||
# ...and from there wait for the machine bootup to finish. We don't really care if the container
|
||||
# boots up in a degraded state, hence the `:`
|
||||
timeout 30s systemd-run -M "$container" --wait --pipe -- systemctl --wait is-system-running || :
|
||||
# Wait until the veth interface is configured and turn on mDNS and LLMNR
|
||||
systemd-run -M "$container" --wait --pipe -- \
|
||||
/usr/lib/systemd/systemd-networkd-wait-online --ipv4 --ipv6 --interface=host0 --operational-state=degraded --timeout=30
|
||||
systemd-run -M "$container" --wait --pipe -- resolvectl mdns host0 yes
|
||||
systemd-run -M "$container" --wait --pipe -- resolvectl llmnr host0 yes
|
||||
systemd-run -M "$container" --wait --pipe -- networkctl status --no-pager
|
||||
systemd-run -M "$container" --wait --pipe -- resolvectl status --no-pager
|
||||
[[ "$(systemd-run -M "$container" --wait --pipe -- resolvectl mdns host0)" =~ :\ yes$ ]]
|
||||
[[ "$(systemd-run -M "$container" --wait --pipe -- resolvectl llmnr host0)" =~ :\ yes$ ]]
|
||||
done
|
||||
|
||||
BRIDGE_INDEX="$(<"/sys/class/net/vz-$CONTAINER_ZONE/ifindex")"
|
||||
machinectl list
|
||||
resolvectl mdns "vz-$CONTAINER_ZONE" on
|
||||
resolvectl llmnr "vz-$CONTAINER_ZONE" on
|
||||
networkctl status
|
||||
resolvectl status
|
||||
|
||||
# Run the actual test cases (functions prefixed by testcase_)
|
||||
run_testcases
|
||||
|
||||
touch /testok
|
Loading…
Reference in New Issue