From 8209f4adcde08d225f56269e608ccd5f6704cd70 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 9 Oct 2025 11:04:58 +0200 Subject: [PATCH 01/11] resolved: add hook api This introduces /run/systemd/resolve.hook/ as a new directory that local (privileged) programs can bind a Varlink socket into. If they do they'll get a method call for each attempted resolved lookup, which they can then either process themselves (and generate new records for, or return errors to block stuff) or let pass so that the regular resolution is done. Usecase for this is primarily two things: 1. in machined we can add local resolution of machine names to their IP addresses, similar in fashion to nss-mymachines, but working also if the non-NSS interfaces to name resolution are used, i.e. the local DNS responder. In fact, I think we should eventually remove nss-mymachines from our tree, as soon as this code in resolved is setlled. 2. in networkd we can add local resolution of names specified in DHCP leases we hand out. But beyond that there should be many other uses, for example people could write "dns firewalls" with this if they like where they dynamically block certain names from resolution. Fixes: #8518 --- man/org.freedesktop.resolve1.xml | 1 + src/resolve/meson.build | 1 + src/resolve/resolvectl.c | 3 +- src/resolve/resolved-bus.c | 8 +- src/resolve/resolved-dns-query.c | 91 +- src/resolve/resolved-dns-query.h | 3 + src/resolve/resolved-forward.h | 1 + src/resolve/resolved-hook.c | 881 +++++++++++++++++++ src/resolve/resolved-hook.h | 10 + src/resolve/resolved-manager.c | 2 + src/resolve/resolved-manager.h | 3 + src/resolve/resolved-varlink.c | 9 +- src/resolve/test-dns-query.c | 10 +- src/shared/meson.build | 1 + src/shared/resolved-def.h | 5 +- src/shared/varlink-io.systemd.Resolve.Hook.c | 81 ++ src/shared/varlink-io.systemd.Resolve.Hook.h | 6 + src/test/test-varlink-idl.c | 2 + 18 files changed, 1100 insertions(+), 18 deletions(-) create mode 100644 src/resolve/resolved-hook.c create mode 100644 src/resolve/resolved-hook.h create mode 100644 src/shared/varlink-io.systemd.Resolve.Hook.c create mode 100644 src/shared/varlink-io.systemd.Resolve.Hook.h diff --git a/man/org.freedesktop.resolve1.xml b/man/org.freedesktop.resolve1.xml index 7d5e997b83..16133e1beb 100644 --- a/man/org.freedesktop.resolve1.xml +++ b/man/org.freedesktop.resolve1.xml @@ -497,6 +497,7 @@ node /org/freedesktop/resolve1 { #define SD_RESOLVED_FROM_ZONE (UINT64_C(1) << 21) #define SD_RESOLVED_FROM_TRUST_ANCHOR (UINT64_C(1) << 22) #define SD_RESOLVED_FROM_NETWORK (UINT64_C(1) << 23) +#define SD_RESOLVED_FROM_HOOK (UINT64_C(1) << 27) On input, the first five flags control the protocols to use for the look-up. They refer to diff --git a/src/resolve/meson.build b/src/resolve/meson.build index 6944f6eb7f..568a7c3c1d 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -28,6 +28,7 @@ systemd_resolved_extract_sources = files( 'resolved-dnssd-bus.c', 'resolved-dnssd.c', 'resolved-etc-hosts.c', + 'resolved-hook.c', 'resolved-link-bus.c', 'resolved-link.c', 'resolved-llmnr.c', diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c index 13f70e3b1c..125d89f41a 100644 --- a/src/resolve/resolvectl.c +++ b/src/resolve/resolvectl.c @@ -253,13 +253,14 @@ static void print_source(uint64_t flags, usec_t rtt) { ansi_normal()); if ((flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC)) != 0) - printf("%s-- Data from:%s%s%s%s%s%s\n", + printf("%s-- Data from:%s%s%s%s%s%s%s\n", ansi_grey(), FLAGS_SET(flags, SD_RESOLVED_SYNTHETIC) ? " synthetic" : "", FLAGS_SET(flags, SD_RESOLVED_FROM_CACHE) ? " cache" : "", FLAGS_SET(flags, SD_RESOLVED_FROM_ZONE) ? " zone" : "", FLAGS_SET(flags, SD_RESOLVED_FROM_TRUST_ANCHOR) ? " trust-anchor" : "", FLAGS_SET(flags, SD_RESOLVED_FROM_NETWORK) ? " network" : "", + FLAGS_SET(flags, SD_RESOLVED_FROM_HOOK) ? " hook" : "", ansi_normal()); } diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c index 53d2de274f..d03002f42c 100644 --- a/src/resolve/resolved-bus.c +++ b/src/resolve/resolved-bus.c @@ -1017,7 +1017,7 @@ static void resolve_service_all_complete(DnsQuery *query) { assert(q); - if (q->block_all_complete > 0) { + if (q->hook_query || q->block_all_complete > 0) { TAKE_PTR(q); return; } @@ -1028,6 +1028,12 @@ static void resolve_service_all_complete(DnsQuery *query) { LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) { + if (aux->hook_query) { + /* If an auxiliary query's hook is still pending, let's wait */ + TAKE_PTR(q); + return; + } + switch (aux->state) { case DNS_TRANSACTION_PENDING: diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c index 79a008bfb7..cb1026aadc 100644 --- a/src/resolve/resolved-dns-query.c +++ b/src/resolve/resolved-dns-query.c @@ -19,6 +19,7 @@ #include "resolved-dns-synthesize.h" #include "resolved-dns-transaction.h" #include "resolved-etc-hosts.h" +#include "resolved-hook.h" #include "resolved-manager.h" #include "resolved-timeouts.h" #include "set.h" @@ -524,6 +525,8 @@ DnsQuery *dns_query_free(DnsQuery *q) { dns_service_browser_unref(q->service_browser_request); + hook_query_free(q->hook_query); + if (q->manager) { LIST_REMOVE(queries, q->manager->dns_queries, q); q->manager->n_dns_queries--; @@ -907,24 +910,17 @@ static int dns_query_try_etc_hosts(DnsQuery *q) { return 1; } -int dns_query_go(DnsQuery *q) { - DnsScopeMatch found = DNS_SCOPE_NO; - DnsScope *first = NULL; +static int dns_query_go_scopes(DnsQuery *q) { int r; assert(q); + assert(!q->hook_query); + assert(q->state == DNS_TRANSACTION_NULL); - if (q->state != DNS_TRANSACTION_NULL) - return 0; - - r = dns_query_try_etc_hosts(q); - if (r < 0) - return r; - if (r > 0) { - dns_query_complete(q, DNS_TRANSACTION_SUCCESS); - return 1; - } + /* Start the lookup via the scopes */ + DnsScopeMatch found = DNS_SCOPE_NO; + DnsScope *first = NULL; LIST_FOREACH(scopes, s, q->manager->dns_scopes) { DnsScopeMatch match; @@ -999,6 +995,72 @@ fail: return r; } +static void on_hook_complete(HookQuery *hq, int rcode, DnsAnswer *answer, void *userdata) { + DnsQuery *q = ASSERT_PTR(userdata); + int r; + + assert(hq); + assert(q->hook_query == hq); + assert(q->state == DNS_TRANSACTION_NULL); + + q->hook_query = hook_query_free(q->hook_query); + TAKE_PTR(hq); + + if (rcode < 0) { + log_debug("Hook yielded no results, proceeding."); + r = dns_query_go_scopes(q); + if (r < 0) { + dns_query_reset_answer(q); + q->answer_errno = r; + dns_query_complete(q, DNS_TRANSACTION_ERRNO); + } + + return; + } + + dns_query_reset_answer(q); + + q->answer = dns_answer_ref(answer); + q->answer_rcode = rcode; + q->answer_protocol = dns_synthesize_protocol(q->flags); + q->answer_family = dns_synthesize_family(q->flags); + q->answer_query_flags = SD_RESOLVED_FROM_HOOK; + dns_query_complete(q, rcode == DNS_RCODE_SUCCESS ? DNS_TRANSACTION_SUCCESS : DNS_TRANSACTION_RCODE_FAILURE); +} + +int dns_query_go(DnsQuery *q) { + int r; + + assert(q); + + /* Already ongoing? Then suppress */ + if (q->hook_query || + q->state != DNS_TRANSACTION_NULL) + return 0; + + r = dns_query_try_etc_hosts(q); + if (r < 0) + return r; + if (r > 0) { + dns_query_complete(q, DNS_TRANSACTION_SUCCESS); + return 1; + } + + r = manager_hook_query( + q->manager, + q->question_bypass ? q->question_bypass->question : q->question_idna, + q->question_bypass ? q->question_bypass->question : q->question_utf8, + on_hook_complete, + q, + &q->hook_query); + if (r < 0) + return r; + if (r > 0) /* hook calls are pending */ + return 0; + + return dns_query_go_scopes(q); +} + static void dns_query_accept(DnsQuery *q, DnsQueryCandidate *c) { DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS; bool has_authenticated = false, has_non_authenticated = false, has_confidential = false, has_non_confidential = false; @@ -1137,6 +1199,9 @@ void dns_query_ready(DnsQuery *q) { * after calling this function, unless the block_ready * counter was explicitly bumped before doing so. */ + if (q->hook_query) + return; + if (q->block_ready > 0) return; diff --git a/src/resolve/resolved-dns-query.h b/src/resolve/resolved-dns-query.h index d3bed93084..b8b7d40525 100644 --- a/src/resolve/resolved-dns-query.h +++ b/src/resolve/resolved-dns-query.h @@ -110,6 +110,9 @@ typedef struct DnsQuery { DnssdDiscoveredService *dnsservice_request; DnsServiceBrowser *service_browser_request; + /* Pending query to any installed hooks */ + HookQuery *hook_query; + /* Completion callback */ void (*complete)(DnsQuery* q); diff --git a/src/resolve/resolved-forward.h b/src/resolve/resolved-forward.h index 8f1fb02539..16c5380d9b 100644 --- a/src/resolve/resolved-forward.h +++ b/src/resolve/resolved-forward.h @@ -34,6 +34,7 @@ typedef struct DnsSvcParam DnsSvcParam; typedef struct DnsTransaction DnsTransaction; typedef struct DnsTxtItem DnsTxtItem; typedef struct DnsZoneItem DnsZoneItem; +typedef struct HookQuery HookQuery; typedef struct Link Link; typedef struct LinkAddress LinkAddress; typedef struct Manager Manager; diff --git a/src/resolve/resolved-hook.c b/src/resolve/resolved-hook.c new file mode 100644 index 0000000000..4a688a8f31 --- /dev/null +++ b/src/resolve/resolved-hook.c @@ -0,0 +1,881 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" +#include "sd-varlink.h" + +#include "dirent-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "hash-funcs.h" +#include "iovec-util.h" +#include "json-util.h" +#include "ratelimit.h" +#include "resolved-hook.h" +#include "resolved-manager.h" +#include "set.h" +#include "stat-util.h" +#include "varlink-util.h" + +/* Controls how many idle connections to keep around at max. This is purely an optimization: an established + * socket that has gone through connect()/accept() already is just quicker to use. Since we might get a flood + * of resolution requests we keep multiple connections open thus, but not too many. */ +#define HOOK_IDLE_CONNECTIONS_MAX 4U + +/* Encapsulates a specific hook, i.e. bound socket in in the /run/systemd/resolve.hook/ directory */ +typedef struct Hook { + unsigned n_ref; + + Manager *manager; + char *socket_path; + + sd_varlink *filter_link; + Set *idle_links; /* we retry to recycle varlink connections */ + + /* This hook only shall be applied to names matching the following filter parameters */ + Set *filter_domains; /* if NULL → no filtering; if empty → do not accept anything */ + unsigned filter_labels_min; /* minimum number of labels */ + unsigned filter_labels_max; /* maximum number of labels (this is useful to hook only into single-label lookups á la LLMNR) */ + + /* timestamp we last saw this in CLOCK_MONOTONIC, for GC handling */ + uint64_t seen_usec; + + /* When a hook never responds correctly, we'll eventually give up trying */ + RateLimit reconnect_ratelimit; +} Hook; + +static Hook* hook_free(Hook *h) { + if (!h) + return NULL; + + mfree(h->socket_path); + sd_varlink_unref(h->filter_link); + set_free(h->idle_links); + + set_free(h->filter_domains); + + return mfree(h); +} + +DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(Hook, hook, hook_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(Hook*, hook_unref); + +static Hook *hook_unlink(Hook *h) { + if (!h) + return NULL; + + if (!h->manager) + return NULL; + + if (h->socket_path) + hashmap_remove(h->manager->hooks, h->socket_path); + h->manager = NULL; + + return hook_unref(h); +} + +static int dispatch_filter_domains(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + Hook *h = ASSERT_PTR(userdata); + int r; + + if (!sd_json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + /* Let's explicitly allocate the set here, since we want that a NULL set means: let everything + * through; but an empty set shall mean: let nothing through */ + r = set_ensure_allocated(&h->filter_domains, &dns_name_hash_ops_free); + if (r < 0) + return json_log_oom(variant, flags); + + sd_json_variant *i; + JSON_VARIANT_ARRAY_FOREACH(i, variant) { + if (!sd_json_variant_is_string(i)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), + "Element of JSON field '%s' is not a string.", strna(name)); + + r = set_put_strdup_full(&h->filter_domains, &dns_name_hash_ops_free, sd_json_variant_string(i)); + if (r < 0 && r != -EEXIST) + return json_log_oom(variant, flags); + } + + return 0; +} + +static void hook_reset_filter(Hook *h) { + assert(h); + + h->filter_domains = set_free(h->filter_domains); + h->filter_labels_min = UINT_MAX; + h->filter_labels_max = UINT_MAX; +} + +static int hook_acquire_filter(Hook *h); + +static int on_filter_reply( + sd_varlink *link, + sd_json_variant *parameters, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + Hook *h = ASSERT_PTR(userdata); + int r; + + if (error_id) { + if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) { + /* When we are are disconnected, that's fine, maybe the other side wants to clean up + * open connections every now and then, or is being restarted and thus a moment + * offline. Try to reconnect immediately to recover. However, a service that + * continously fails should not be able to get us into a busy loop, hence we apply a + * ratelimit, and when it is hit we stop reconnecting. */ + if (ratelimit_below(&h->reconnect_ratelimit)) { + log_debug("Connection terminated while querying filter of hook '%s', trying to reconnect.", h->socket_path); + + h->filter_link = sd_varlink_unref(h->filter_link); + + r = hook_acquire_filter(h); + if (r < 0) + goto terminate; + } else + log_warning("Connection terminated while querying filter of hook '%s', and reconnection attempts failed too quickly, giving up.", h->socket_path); + + goto terminate; + } + + if (streq(error_id, SD_VARLINK_ERROR_METHOD_NOT_FOUND)) { + log_debug("Hook '%s' does not implement querying filter.", h->socket_path); + goto terminate; + } + + log_warning("Received error while requesting query filter: %s", error_id); + goto terminate; + } + + if (!FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES)) { + log_debug("Final message received while querying filter, terminating connection."); + goto terminate; + } + + hook_reset_filter(h); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "filterDomains", SD_JSON_VARIANT_ARRAY, dispatch_filter_domains, 0, SD_JSON_NULLABLE }, + { "filterLabelsMin", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, offsetof(Hook, filter_labels_min), 0 }, + { "filterLabelsMax", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, offsetof(Hook, filter_labels_max), 0 }, + {}, + }; + + r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, h); + if (r < 0) + goto terminate; + + return 1; + +terminate: + h->filter_link = sd_varlink_unref(h->filter_link); + hook_reset_filter(h); + return 1; +} + +static int hook_varlink_connect(Hook *h, int64_t priority, sd_varlink **ret) { + int r; + + assert(h); + assert(ret); + + _cleanup_(sd_varlink_unrefp) sd_varlink *v = NULL; + r = sd_varlink_connect_address(&v, h->socket_path); + if (ERRNO_IS_NEG_DISCONNECT(r) || r == -ENOENT) { + log_debug_errno(r, "Socket '%s' is not connectible, probably stale, ignoring: %m", h->socket_path); + *ret = NULL; + return 0; /* dead socket */ + } + if (r < 0) + return log_error_errno(r, "Failed to connect to '%s': %m", h->socket_path); + + _cleanup_free_ char *bn = NULL; + r = path_extract_filename(h->socket_path, &bn); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from path '%s': %m", h->socket_path); + + _cleanup_free_ char *j = strjoin("hook-", bn); + if (!j) + return log_oom(); + + (void) sd_varlink_set_description(v, j); + + r = sd_varlink_attach_event(v, h->manager->event, priority); + if (r < 0) + return log_error_errno(r, "Failed to attach Varlink connection to event loop: %m"); + + *ret = TAKE_PTR(v); + return 1; /* worked */ +} + +static int hook_acquire_filter(Hook *h) { + int r; + + assert(h); + assert(h->manager); + + if (h->filter_link) + return 0; + + _cleanup_(sd_varlink_unrefp) sd_varlink *v = NULL; + r = hook_varlink_connect(h, SD_EVENT_PRIORITY_NORMAL-10, &v); /* Give the querying of the filter a bit of priority */ + if (r <= 0) + return r; + + /* Turn off timeout, after all we want to continously monitor filter changes */ + r = sd_varlink_set_relative_timeout(v, UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to disable timeout on Varlink connection %m"); + + sd_varlink_set_userdata(v, h); + r = sd_varlink_bind_reply(v, on_filter_reply); + if (r < 0) + return log_error_errno(r, "Failed to set filter reply callback on Varlink connection: %m"); + + r = sd_varlink_observe( + v, + "io.systemd.Resolve.Hook.QueryFilter", + /* parameters= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue QueryFilter() varlink call: %m"); + + h->filter_link = TAKE_PTR(v); + return 0; +} + +static int hook_test_filter(Hook *h, DnsQuestion *question) { + int r; + + assert(h); + assert(question); + + const char *name = dns_question_first_name(question); + if (!name) + return -EINVAL; + + if (h->filter_labels_max != UINT_MAX || h->filter_labels_min != UINT_MAX) { + int n = dns_name_count_labels(name); + if (n < 0) + return n; + + if (h->filter_labels_max != UINT_MAX && (unsigned) n > h->filter_labels_max) + return false; + if (h->filter_labels_min != UINT_MAX && (unsigned) n < h->filter_labels_min) + return false; + } + + if (h->filter_domains) + for (const char *p = name;;) { + if (set_contains(h->filter_domains, p)) + break; + + r = dns_name_parent(&p); + if (r < 0) + return r; + if (r == 0) + return false; + } + + return true; +} + +static int hook_compare(const Hook *a, const Hook *b) { + assert(a); + + /* Hooks take preference based on the name of their socket */ + return path_compare(a->socket_path, b->socket_path); +} + +static void hook_recycle_varlink(Hook *h, sd_varlink *vl) { + int r; + + assert(h); + assert(vl); + + /* Disable any potential callbacks while we are recycling the thing */ + sd_varlink_set_userdata(vl, NULL); + sd_varlink_bind_reply(vl, NULL); + + if (set_size(h->idle_links) > HOOK_IDLE_CONNECTIONS_MAX) + return; + + /* If we are done with a lookup don't close the connection right-away, but keep it open so that we + * can possibly reuse it later, and can save a bit of time on future lookups. We only keep a few + * around however. */ + + r = set_ensure_put(&h->idle_links, &varlink_hash_ops, vl); + if (r < 0) + log_debug_errno(r, "Failed to add varlink connection to idle set, ignoring: %m"); + else + sd_varlink_ref(vl); +} + +static void manager_gc_hooks(Manager *m, usec_t seen_usec) { + assert(m); + + Hook *h; + HASHMAP_FOREACH(h, m->hooks) { + /* Keep hooks around that have been seen in this iteration */ + if (h->seen_usec == seen_usec) + continue; + + hook_unlink(h); + } +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + hook_hash_ops, + char, string_hash_func, string_compare_func, + Hook, hook_unlink); + +static int manager_hook_add(Manager *m, const char *p, usec_t seen_usec) { + int r; + + assert(m); + assert(p); + + Hook *found = hashmap_get(m->hooks, p); + if (found) { + found->seen_usec = seen_usec; + return 0; + } + + _cleanup_free_ char *s = strdup(p); + if (!s) + return log_oom(); + + _cleanup_(hook_unrefp) Hook *h = new(Hook, 1); + if (!h) + return log_oom(); + + *h = (Hook) { + .n_ref = 1, + .socket_path = TAKE_PTR(s), + .filter_labels_min = UINT_MAX, + .filter_labels_max = UINT_MAX, + .reconnect_ratelimit = { 1 * USEC_PER_SEC, 5 }, + .seen_usec = seen_usec, + }; + + if (hashmap_ensure_put(&m->hooks, &hook_hash_ops, h->socket_path, h) < 0) + return log_oom(); + + hook_ref(h); + h->manager = m; + + r = hook_acquire_filter(h); + if (r < 0) { + hook_unlink(h); + return r; + } + + return 0; +} + +static int manager_hook_discover(Manager *m) { + /* You might wonder, why is this /run/systemd/resolve.hook/ and not /run/systemd/resolve/hook/? + * That's because of permissions: resolved runs as "systemd-resolve" user and owns + * /run/systemd/resolve/, but the hook directory is where other privileged code shall bind a socket + * in (and where root ownership hence makes sense). Hence we do not nest the directories, but put + * them side by side, so that they can have different ownership. */ + static const char dp[] = "/run/systemd/resolve.hook"; + _cleanup_closedir_ DIR *d = NULL; + int r; + + assert(m); + + usec_t seen_usec = now(CLOCK_MONOTONIC); + + struct stat st; + if (stat(dp, &st) < 0) { + if (errno == ENOENT) + r = 0; + else + r = log_warning_errno(errno, "Failed to stat %s/: %m", dp); + + goto finish; + } + + if (stat_inode_unmodified(&st, &m->hook_stat)) + return 0; + + d = opendir(dp); + if (!d) { + if (errno == ENOENT) + r = 0; + else + r = log_warning_errno(errno, "Failed to enumerate %s/ contents: %m", dp); + + goto finish; + } + + for (;;) { + errno = 0; + struct dirent *de = readdir_no_dot(d); + if (!de) { + if (errno == 0) /* EOD */ + break; + + r = log_error_errno(errno, "Failed to enumerate %s/: %m", dp); + goto finish; + } + + if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN)) + continue; + + _cleanup_free_ char *p = path_join(dp, de->d_name); + if (!p) { + r = log_oom(); + goto finish; + } + + (void) manager_hook_add(m, p, seen_usec); + } + + m->hook_stat = st; + r = 0; + +finish: + manager_gc_hooks(m, seen_usec); + return r; +} + +typedef struct HookQuery HookQuery; +typedef struct HookQueryCandidate HookQueryCandidate; + +/* Encapsulates a query currently being processed by various hooks */ +struct HookQuery { + /* Question */ + DnsQuestion *question_idna; + DnsQuestion *question_utf8; + + /* Selected answer */ + DnsAnswer *answer; + int answer_rcode; + Hook *answer_hook; + + /* Candidates for a reply, i.e, one entry for each hook */ + LIST_HEAD(HookQueryCandidate, candidates); + + /* Completion callback to invoke */ + void (*complete)(HookQuery *q, int answer_rcode, DnsAnswer *answer, void *userdata); + void *userdata; +}; + +/* Encapsulates the state of a hook query to one specific hook */ +struct HookQueryCandidate { + HookQuery *query; + Hook *hook; + sd_varlink *link; + LIST_FIELDS(HookQueryCandidate, candidates); +}; + +static HookQueryCandidate* hook_query_candidate_free(HookQueryCandidate *c) { + if (!c) + return NULL; + + c->link = sd_varlink_unref(c->link); + + if (c->query) + LIST_REMOVE(candidates, c->query->candidates, c); + + hook_unref(c->hook); + return mfree(c); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(HookQueryCandidate*, hook_query_candidate_free); + +HookQuery* hook_query_free(HookQuery *hq) { + if (!hq) + return NULL; + + /* Free candidates as long as there are candidates */ + while (hq->candidates) + hook_query_candidate_free(hq->candidates); + + dns_question_unref(hq->question_utf8); + dns_question_unref(hq->question_idna); + dns_answer_unref(hq->answer); + hook_unref(hq->answer_hook); + + return mfree(hq); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(HookQuery*, hook_query_free); + +static void hook_query_ready(HookQuery *hq) { + assert(hq); + + bool done = true; + LIST_FOREACH(candidates, c, hq->candidates) + if (c->link) { /* ongoing connection? */ + done = false; + break; + } + + if (!done) + return; + + /* The complete() callback quite likely will destroy 'hq', which might be what keeps the answer + * object alive. Let's take an explicit ref here hence, so that it definitely remains alive for the + * whole callback lifetime */ + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = dns_answer_ref(hq->answer); + hq->complete(hq, hq->answer_rcode, answer, hq->userdata); +} + +static int dispatch_rcode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + int *u = ASSERT_PTR(userdata), r; + + assert(variant); + + int rcode; + r = sd_json_dispatch_int(name, variant, flags, &rcode); + if (r < 0) + return r; + + if (rcode < 0 || rcode >= _DNS_RCODE_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' contains an invalid DNS rcode.", strna(name)); + + *u = rcode; + return 0; +} + +static int dispatch_answer(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + DnsAnswer **a = ASSERT_PTR(userdata); + int r; + + assert(variant); + + if (sd_json_variant_is_null(variant)) { + *a = dns_answer_unref(*a); + return 0; + } + + if (!sd_json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + _cleanup_(dns_answer_unrefp) DnsAnswer *l = NULL; + sd_json_variant *e; + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + if (!sd_json_variant_is_object(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object"); + + _cleanup_(iovec_done) struct iovec iovec = {}; + static const sd_json_dispatch_field dispatch_table[] = { + { "raw", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, 0, SD_JSON_MANDATORY }, + { "rr", SD_JSON_VARIANT_OBJECT, NULL, 0, 0 }, + {} + }; + + r = sd_json_dispatch(e, dispatch_table, flags|SD_JSON_ALLOW_EXTENSIONS, &iovec); + if (r < 0) + return r; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + r = dns_resource_record_new_from_raw(&rr, iovec.iov_base, iovec.iov_len); + if (r < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), + "JSON field '%s' contains an invalid resource record.", strna(name)); + + if (dns_answer_add_extend(&l, rr, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL) < 0) + return json_log_oom(e, flags); + } + + dns_answer_unref(*a); + *a = TAKE_PTR(l); + + return 0; +} + +typedef struct QueryReplyParameters { + int rcode; + DnsAnswer *answer; +} QueryReplyParameters; + +static void query_reply_parameters_done(QueryReplyParameters *p) { + assert(p); + + p->answer = dns_answer_unref(p->answer); +} + +static int on_query_reply( + sd_varlink *link, + sd_json_variant *parameters, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + HookQueryCandidate *qc = ASSERT_PTR(userdata); + HookQuery *q = ASSERT_PTR(qc->query); /* save early in case we destroy 'qc' half-way through this function */ + int r; + + assert(link); + + _cleanup_(query_reply_parameters_done) QueryReplyParameters p = { + .rcode = -1, + }; + + if (error_id) { + log_notice("Query on hook '%s' failed with error '%s', ignoring.", qc->hook->socket_path, error_id); + r = -EBADR; + goto destroy; + } + + static const sd_json_dispatch_field dispatch_table[] = { + { "rcode", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_rcode, offsetof(QueryReplyParameters, rcode), 0 }, + { "answer", SD_JSON_VARIANT_ARRAY, dispatch_answer, offsetof(QueryReplyParameters, answer), 0 }, + {}, + }; + + r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + goto destroy; + + if (p.rcode < 0) { + /* If no rcode is specified, then this means "continue with regular DNS based resolving" to us */ + log_debug("Query on hook '%s' returned empty reply, skipping.", qc->hook->socket_path); + r = 0; + goto destroy; + } + + bool win = false; + if (p.rcode == DNS_RCODE_SUCCESS) + /* if this is a successful lookup, let it win if the so far best lookup was a failure or + * empty, or ordered later than us */ + win = q->answer_rcode != DNS_RCODE_SUCCESS || + dns_answer_isempty(q->answer) || + (!dns_answer_isempty(p.answer) && + hook_compare(qc->hook, q->answer_hook) < 0); + else + /* if this is a failure lookup, let it win if we so far haven't seen any reply at all, or the + * winner so far us ordered later than us. */ + win = q->answer_rcode < 0 || + hook_compare(qc->hook, q->answer_hook) < 0; + + if (win) { + /* This reply wins over whatever was stored before. Let's track that */ + dns_answer_unref(q->answer); + q->answer = TAKE_PTR(p.answer); + q->answer_rcode = p.rcode; + hook_unref(q->answer_hook); + q->answer_hook = hook_ref(qc->hook); + } + + hook_recycle_varlink(qc->hook, qc->link); + qc->link = sd_varlink_unref(qc->link); + + /* Check if we are ready now, and have processed all hooks on this query (this might destroy our + * candidate and our hook query!) */ + hook_query_ready(q); + return 0; + +destroy: + qc = hook_query_candidate_free(qc); + hook_query_ready(q); + return r; +} + +static int dns_questions_to_json(DnsQuestion *a, DnsQuestion *b, sd_json_variant **ret) { + int r; + + assert(ret); + + /* Takes both questions and turns them into a JSON array of objects with the key. Note this takes two + * questions, one in IDNA and one in UTF-8 encoding, and merges them, removing duplicates. */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *l = NULL; + + DnsResourceKey *key; + DNS_QUESTION_FOREACH(key, a) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = dns_resource_key_to_json(key, &v); + if (r < 0) + return r; + + r = sd_json_variant_append_arraybo(&l, SD_JSON_BUILD_PAIR_VARIANT("key", v)); + if (r < 0) + return r; + } + + if (a != b) { + DNS_QUESTION_FOREACH(key, b) { + if (dns_question_contains_key(a, key)) + continue; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = dns_resource_key_to_json(key, &v); + if (r < 0) + return r; + + r = sd_json_variant_append_arraybo(&l, SD_JSON_BUILD_PAIR_VARIANT("key", v)); + if (r < 0) + return r; + } + } + + *ret = TAKE_PTR(l); + return 0; +} + +static int hook_query_add_candidate(HookQuery *hq, Hook *h) { + int r; + + assert(hq); + assert(h); + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + for (;;) { + /* Before we create a new connection, let's see if there are still idle connections we can + * use. */ + vl = set_steal_first(h->idle_links); + if (!vl) { + /* Nope, there's nothing, let's create a new connection */ + r = hook_varlink_connect(h, SD_EVENT_PRIORITY_NORMAL, &vl); + if (r <= 0) + return r; + + break; + } + + r = sd_varlink_is_connected(vl); + if (r < 0) + return log_error_errno(r, "Failed to check if varlink connection is connected: %m"); + if (r > 0) + break; + + vl = sd_varlink_unref(vl); + } + + /* Set a short timeout for hooks. Hooks should not be able to cause the DNS part of the lookup to fail. */ + r = sd_varlink_set_relative_timeout(vl, SD_RESOLVED_QUERY_TIMEOUT_USEC/4); + if (r < 0) + return log_error_errno(r, "Failed to set Varlink connection timeout: %m"); + + r = sd_varlink_bind_reply(vl, on_query_reply); + if (r < 0) + return log_error_errno(r, "Failed to bind reply callback to Varlink connection: %m"); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *jq = NULL; + r = dns_questions_to_json(hq->question_idna, hq->question_utf8, &jq); + if (r < 0) + return log_error_errno(r, "Failed to convert question to JSON: %m"); + + r = sd_varlink_invokebo( + vl, + "io.systemd.Resolve.Hook.ResolveRecord", + SD_JSON_BUILD_PAIR_VARIANT("question", jq)); + if (r < 0) + return log_error_errno(r, "Failed to enqueue question onto Varlink connection: %m"); + + _cleanup_(hook_query_candidate_freep) HookQueryCandidate *qc = new(HookQueryCandidate, 1); + if (!qc) + return log_oom(); + + qc->query = hq; + qc->hook = hook_ref(h); + qc->link = TAKE_PTR(vl); + LIST_PREPEND(candidates, hq->candidates, qc); + + sd_varlink_set_userdata(qc->link, qc); + + TAKE_PTR(qc); + + return 0; +} + +static bool use_hooks(void) { + static int cache = -1; + int r; + + if (cache >= 0) + return cache; + + r = secure_getenv_bool("SYSTEMD_RESOLVED_HOOK"); + if (r < 0) { + if (r != -ENXIO) + log_debug_errno(r, "Failed to parse $SYSTEMD_RESOLVED_HOOK, ignoring: %m"); + + return (cache = true); + } + + return (cache = r); +} + +int manager_hook_query( + Manager *m, + DnsQuestion *question_idna, + DnsQuestion *question_utf8, + HookCompleteCallback complete_cb, + void *userdata, + HookQuery **ret) { + + int r; + + assert(m); + assert(ret); + + if (!use_hooks()) { + *ret = NULL; + return 0; /* no relevant hooks, continue immediately */ + } + + /* Let's bring our list of hooks up-to-date */ + (void) manager_hook_discover(m); + + _cleanup_(hook_query_freep) HookQuery *hq = NULL; + + Hook *h; + HASHMAP_FOREACH(h, m->hooks) { + r = hook_test_filter(h, question_idna); + if (r < 0) { + log_warning_errno( + r, "Failed to test if hook '%s' matches IDNA question (%s), assuming not.", + h->socket_path, dns_question_first_name(question_idna)); + continue; + } + if (r == 0) { + r = hook_test_filter(h, question_utf8); + if (r < 0) { + log_warning_errno( + r, "Failed to test if hook '%s' matches UTF-8 question (%s), assuming not.", + h->socket_path, dns_question_first_name(question_utf8)); + continue; + } + if (r == 0) { + log_debug("Hook %s does not match question, skipping.", h->socket_path); + continue; + } + } + + if (!hq) { + hq = new(HookQuery, 1); + if (!hq) + return log_oom(); + + *hq = (HookQuery) { + .question_idna = dns_question_ref(question_idna), + .question_utf8 = dns_question_ref(question_utf8), + .answer_rcode = -1, + .complete = complete_cb, + .userdata = userdata, + }; + } + + r = hook_query_add_candidate(hq, h); + if (r < 0) + return r; + } + + if (!hq || !hq->candidates) { + *ret = NULL; + return 0; /* no relevant hooks, continue immediately */ + } + + *ret = TAKE_PTR(hq); + return 1; /* please wait for the hooks to reply */ +} diff --git a/src/resolve/resolved-hook.h b/src/resolve/resolved-hook.h new file mode 100644 index 0000000000..1365455a23 --- /dev/null +++ b/src/resolve/resolved-hook.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-forward.h" + +typedef void (HookCompleteCallback)(HookQuery *q, int rcode, DnsAnswer *answer, void *userdata); + +int manager_hook_query(Manager *m, DnsQuestion *question_idna, DnsQuestion *question_utf8, HookCompleteCallback complete_cb, void *userdata, HookQuery **ret); + +HookQuery* hook_query_free(HookQuery *hq); diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index fe4807685a..d256804dbc 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -918,6 +918,8 @@ Manager* manager_free(Manager *m) { dns_service_browser_free(sb); hashmap_free(m->dns_service_browsers); + hashmap_free(m->hooks); + return mfree(m); } diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index 68e9a6e4ea..7f8a0e0ceb 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -159,6 +159,9 @@ typedef struct Manager { /* Map varlink links to DnsServiceBrowser instances. */ Hashmap *dns_service_browsers; + + Hashmap *hooks; + struct stat hook_stat; } Manager; /* Manager */ diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index 90f4a9fffa..89d84e8eb2 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -700,7 +700,7 @@ static void resolve_service_all_complete(DnsQuery *query) { assert(q); - if (q->block_all_complete > 0) { + if (q->hook_query || q->block_all_complete > 0) { TAKE_PTR(q); return; } @@ -710,6 +710,13 @@ static void resolve_service_all_complete(DnsQuery *query) { bool have_success = false; LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) { + + if (aux->hook_query) { + /* If an auxiliary query's hook is still pending, let's wait */ + TAKE_PTR(q); + return; + } + switch (aux->state) { case DNS_TRANSACTION_PENDING: diff --git a/src/resolve/test-dns-query.c b/src/resolve/test-dns-query.c index 64114f5abc..de561aa8b7 100644 --- a/src/resolve/test-dns-query.c +++ b/src/resolve/test-dns-query.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "sd-event.h" #include "dns-answer.h" @@ -890,4 +892,10 @@ TEST(dns_query_go) { exercise_dns_query_go(&cfg, NULL); } -DEFINE_TEST_MAIN(LOG_DEBUG); +static int intro(void) { + /* Disable hooks in order to make test cases hermetic */ + ASSERT_OK_ERRNO(setenv("SYSTEMD_RESOLVED_HOOK", "0", /* overwrite= */ false)); + return EXIT_SUCCESS; +} + +DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro); diff --git a/src/shared/meson.build b/src/shared/meson.build index bc927e4cca..0cf0324f97 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -217,6 +217,7 @@ shared_sources = files( 'varlink-io.systemd.PCRLock.c', 'varlink-io.systemd.Repart.c', 'varlink-io.systemd.Resolve.c', + 'varlink-io.systemd.Resolve.Hook.c', 'varlink-io.systemd.Resolve.Monitor.c', 'varlink-io.systemd.Udev.c', 'varlink-io.systemd.Unit.c', diff --git a/src/shared/resolved-def.h b/src/shared/resolved-def.h index e00bd204d7..68a3d91179 100644 --- a/src/shared/resolved-def.h +++ b/src/shared/resolved-def.h @@ -80,10 +80,13 @@ #define SD_RESOLVED_QUERY_CONTINUOUS \ (UINT64_C(1) << 26) +/* Output: Result was answered by hook */ +#define SD_RESOLVED_FROM_HOOK (UINT64_C(1) << 27) + #define SD_RESOLVED_LLMNR (SD_RESOLVED_LLMNR_IPV4|SD_RESOLVED_LLMNR_IPV6) #define SD_RESOLVED_MDNS (SD_RESOLVED_MDNS_IPV4|SD_RESOLVED_MDNS_IPV6) #define SD_RESOLVED_PROTOCOLS_ALL (SD_RESOLVED_MDNS|SD_RESOLVED_LLMNR|SD_RESOLVED_DNS) -#define SD_RESOLVED_FROM_MASK (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK) +#define SD_RESOLVED_FROM_MASK (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK|SD_RESOLVED_FROM_HOOK) #define SD_RESOLVED_QUERY_TIMEOUT_USEC (120 * USEC_PER_SEC) diff --git a/src/shared/varlink-io.systemd.Resolve.Hook.c b/src/shared/varlink-io.systemd.Resolve.Hook.c new file mode 100644 index 0000000000..a172a95a67 --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Hook.c @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.Resolve.Hook.h" + +/* We want to reuse the ResourceKey structure from the io.systemd.Resolve interface, hence import it here */ +#include "varlink-io.systemd.Resolve.h" + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + Answer, + SD_VARLINK_FIELD_COMMENT("A resource record that shall be looked up. Note that this field is (currently) mostly " + "decoration, useful for debugging, and may be omitted. The data actually used is encoded in the " + "'raw' field."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(rr, ResourceRecord, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("A resource record encoded in DNS wire format, in turn encoded in Base64. This is the actual data " + "returned to the application, and should carry the same information as the 'rr' field, just in a " + "different encoding."), + SD_VARLINK_DEFINE_FIELD(raw, SD_VARLINK_STRING, 0)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + Question, + SD_VARLINK_FIELD_COMMENT("A resource record key that shall be looked up."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + QueryFilter, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("A list of domains this hook is interested in. Lookups for domains not listed here will not be " + "passed to the Hook via ResolveRecord(). If this field is not set, requests for all domains " + "will be passed to the hook. Note that this applies recursively, i.e. a domain of a lookup is " + "considered matching the listed domains both if it exactly matches it, and in case only a suffix " + "of it matches it. If this is set to an empty array the hook is disabled."), + SD_VARLINK_DEFINE_OUTPUT(filterDomains, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Require the specified number of labels or more in a domain for the hook to be considered."), + SD_VARLINK_DEFINE_OUTPUT(filterLabelsMin, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Require the specified number of labels or less in a domain for the hook to be considered."), + SD_VARLINK_DEFINE_OUTPUT(filterLabelsMax, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD( + ResolveRecord, + SD_VARLINK_FIELD_COMMENT("The question being looked up, i.e. a combination of resource record keys. Note that unlike DNS " + "queries on the wire these lookups can carry multiple key requests, albeit closely related ones. " + "Specifically, lookups for A+AAAA for the the same hostname are submitted as one question, as " + "are lookups for TXT+SRV when doing DNS-SD resolution. Moreover, when looking up resources with " + "non-ASCII characters, they are placed together in a single question, once with labels encoded in " + "UTF-8, and once in IDNA. Hook implementations must be able to deal with these and other similar " + "combinations of resource key requests, and reply with all matching answers at once, or fail them " + "as one. Partial success/failure combinations are not supported."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(question, Question, SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("A DNS response code. If a hook sets this return parameter further processing of the lookup via " + "regular proocols such as DNS, LLMNR, mDNS is skipped, and the return code returned immediately. " + "In other words, if a hook intends to let the request pass to normal resolution, it should not " + "set this return parameter."), + SD_VARLINK_DEFINE_OUTPUT(rcode, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("An answer for a lookup, i.e. a combination of resource records, matching the request. This " + "should only be set when the 'rcode' parameter is returned as 0 (SUCCESS)."), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(answer, Answer, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE)); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_Resolve_Hook, + "io.systemd.Resolve.Hook", + SD_VARLINK_INTERFACE_COMMENT("Generic interface for implementing a domain name resolution hook."), + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a positive lookup answer"), + &vl_type_Answer, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a lookup question"), + &vl_type_Question, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates the class/type/name part of a DNS resource record."), + &vl_type_ResourceKey, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a DNS resource record."), + &vl_type_ResourceRecord, + SD_VARLINK_SYMBOL_COMMENT("Returns filter parameters for this hook. A hook service can implement this to reduce lookup " + "requests, by enabling itself only for certain domains or certain numbers of labels in the name. " + "It's recommended to implement this to reduce the number of redundant calls to each hook. Note " + "that this is advisory only, and implementing services must be able to gracefully handle lookup " + "requests that do not match this filter. This call is usually made with the 'more' flag set, in " + "which case the connection is left open after the first reply, and the implementing hook " + "services can send updates to the filter at any time. Whenever a further reply is sent the " + "filter configured therein fully replaces any previously communicated filter."), + &vl_method_QueryFilter, + SD_VARLINK_SYMBOL_COMMENT("Sent whenever a resolution request is made. This typically takes the filter paramaters returned " + "by QueryFilter() into account, but this is not guaranteed."), + &vl_method_ResolveRecord); diff --git a/src/shared/varlink-io.systemd.Resolve.Hook.h b/src/shared/varlink-io.systemd.Resolve.Hook.h new file mode 100644 index 0000000000..0c371572d1 --- /dev/null +++ b/src/shared/varlink-io.systemd.Resolve.Hook.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_Resolve_Hook; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index cecc64b3fd..e3fe38f6ca 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -37,6 +37,7 @@ #include "varlink-io.systemd.PCRLock.h" #include "varlink-io.systemd.Repart.h" #include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.Resolve.Hook.h" #include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.Udev.h" #include "varlink-io.systemd.Unit.h" @@ -192,6 +193,7 @@ TEST(parse_format) { &vl_interface_io_systemd_PCRLock, &vl_interface_io_systemd_Repart, &vl_interface_io_systemd_Resolve, + &vl_interface_io_systemd_Resolve_Hook, &vl_interface_io_systemd_Resolve_Monitor, &vl_interface_io_systemd_Udev, &vl_interface_io_systemd_Unit, From 594352611b2903098f5f6fc88ca1173b188a33e8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 9 Oct 2025 22:19:37 +0200 Subject: [PATCH 02/11] machine: minor refactoring, making machine_send_signal() invocations more readable --- src/machine/machine-dbus.c | 9 ++++----- src/machine/machine-dbus.h | 2 +- src/machine/machine.c | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/machine/machine-dbus.c b/src/machine/machine-dbus.c index 69159f4eb0..6bee74fd30 100644 --- a/src/machine/machine-dbus.c +++ b/src/machine/machine-dbus.c @@ -870,12 +870,11 @@ const BusObjectImplementation machine_object = { .node_enumerator = machine_node_enumerator, }; -int machine_send_signal(Machine *m, bool new_machine) { - _cleanup_free_ char *p = NULL; - +int machine_send_signal(Machine *m, const char *signal_name) { assert(m); + assert(signal_name); - p = machine_bus_path(m); + _cleanup_free_ char *p = machine_bus_path(m); if (!p) return -ENOMEM; @@ -883,7 +882,7 @@ int machine_send_signal(Machine *m, bool new_machine) { m->manager->api_bus, "/org/freedesktop/machine1", "org.freedesktop.machine1.Manager", - new_machine ? "MachineNew" : "MachineRemoved", + signal_name, "so", m->name, p); } diff --git a/src/machine/machine-dbus.h b/src/machine/machine-dbus.h index 7fd025f7b3..38ea10b295 100644 --- a/src/machine/machine-dbus.h +++ b/src/machine/machine-dbus.h @@ -26,5 +26,5 @@ int bus_machine_method_copy(sd_bus_message *message, void *userdata, sd_bus_erro int bus_machine_method_open_root_directory(sd_bus_message *message, void *userdata, sd_bus_error *error); int bus_machine_method_get_uid_shift(sd_bus_message *message, void *userdata, sd_bus_error *error); -int machine_send_signal(Machine *m, bool new_machine); +int machine_send_signal(Machine *m, const char *signal_name); int machine_send_create_reply(Machine *m, sd_bus_error *error); diff --git a/src/machine/machine.c b/src/machine/machine.c index ff376c7047..0350c16df6 100644 --- a/src/machine/machine.c +++ b/src/machine/machine.c @@ -672,7 +672,7 @@ int machine_start(Machine *m, sd_bus_message *properties, sd_bus_error *error) { /* Save new machine data */ machine_save(m); - machine_send_signal(m, true); + machine_send_signal(m, "MachineNew"); return 0; } @@ -730,7 +730,7 @@ int machine_finalize(Machine *m) { machine_add_to_gc_queue(m); if (m->started) { - machine_send_signal(m, false); + machine_send_signal(m, "MachineRemoved"); m->started = false; } From 3cd929f837854e96cd0a856f9a96518701841782 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 9 Oct 2025 22:01:04 +0200 Subject: [PATCH 03/11] machined: implement resolve hook in machined This basically implements nss-myhostname, but natively in systemd-resolved, so that the logic becomes available also for clients using the local DNS stub for resolution or the D-Bus or Varlink APIs. --- src/machine/machine.c | 5 + src/machine/machined-resolve-hook.c | 200 ++++++++++++++++++++ src/machine/machined-resolve-hook.h | 9 + src/machine/machined-varlink.c | 59 ++++++ src/machine/machined.c | 3 + src/machine/machined.h | 2 + src/machine/meson.build | 1 + src/shared/dns-answer.c | 35 ++++ src/shared/dns-answer.h | 2 + src/shared/dns-question.c | 35 ++++ src/shared/dns-question.h | 4 + src/shared/dns-rr.c | 13 ++ src/shared/dns-rr.h | 2 + src/shared/meson.build | 1 + src/shared/resolve-hook-util.c | 15 ++ src/shared/resolve-hook-util.h | 14 ++ test/units/TEST-13-NSPAWN.nss-mymachines.sh | 8 + 17 files changed, 408 insertions(+) create mode 100644 src/machine/machined-resolve-hook.c create mode 100644 src/machine/machined-resolve-hook.h create mode 100644 src/shared/resolve-hook-util.c create mode 100644 src/shared/resolve-hook-util.h diff --git a/src/machine/machine.c b/src/machine/machine.c index 0350c16df6..f097bfe380 100644 --- a/src/machine/machine.c +++ b/src/machine/machine.c @@ -24,6 +24,7 @@ #include "log.h" #include "machine.h" #include "machine-dbus.h" +#include "machined-resolve-hook.h" #include "machined.h" #include "mkdir-label.h" #include "namespace-util.h" @@ -674,6 +675,8 @@ int machine_start(Machine *m, sd_bus_message *properties, sd_bus_error *error) { machine_send_signal(m, "MachineNew"); + (void) manager_notify_hook_filters(m->manager); + return 0; } @@ -732,6 +735,8 @@ int machine_finalize(Machine *m) { if (m->started) { machine_send_signal(m, "MachineRemoved"); m->started = false; + + (void) manager_notify_hook_filters(m->manager); } return 0; diff --git a/src/machine/machined-resolve-hook.c b/src/machine/machined-resolve-hook.c new file mode 100644 index 0000000000..c0bd5fda28 --- /dev/null +++ b/src/machine/machined-resolve-hook.c @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" + +#include "dns-answer.h" +#include "dns-domain.h" +#include "dns-packet.h" +#include "dns-question.h" +#include "dns-rr.h" +#include "hashmap.h" +#include "json-util.h" +#include "local-addresses.h" +#include "log.h" +#include "machine.h" +#include "machined.h" +#include "machined-resolve-hook.h" +#include "resolve-hook-util.h" +#include "set.h" +#include "varlink-util.h" + +static int manager_make_machine_array(Manager *m, sd_json_variant **ret) { + int r; + + assert(m); + assert(ret); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + Machine *machine; + HASHMAP_FOREACH(machine, m->machines) { + if (machine->class == MACHINE_HOST) + continue; + if (!machine->started) + continue; + + r = sd_json_variant_append_arrayb(&array, SD_JSON_BUILD_STRING(machine->name)); + if (r < 0) + return r; + } + + if (!array) + return sd_json_variant_new_array(ret, /* array= */ NULL, /* n= */ 0); + + *ret = TAKE_PTR(array); + return 0; +} + +int manager_notify_hook_filters(Manager *m) { + int r; + + assert(m); + + /* Called whenever a machine is added or dropped from the list */ + + if (set_isempty(m->query_filter_subscriptions)) + return 0; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + r = manager_make_machine_array(m, &array); + if (r < 0) + return log_error_errno(r, "Failed to generate JSON array with machine names: %m"); + + r = varlink_many_notifybo(m->query_filter_subscriptions, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + + return 0; +} + +int vl_method_query_filter( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(link); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + r = manager_make_machine_array(m, &array); + if (r < 0) + return r; + + if (flags & SD_VARLINK_METHOD_MORE) { + /* If 'more' is set, this is a subscription request, keep track of the link */ + + r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + + r = set_ensure_put(&m->query_filter_subscriptions, &varlink_hash_ops, link); + if (r < 0) + return r; + + sd_varlink_ref(link); + } else { + r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + } + + return 0; +} + +int vl_method_resolve_record( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(link); + + _cleanup_(resolve_record_parameters_done) ResolveRecordParameters p = {}; + r = sd_varlink_dispatch(link, parameters, resolve_record_parameters_dispatch_table, &p); + if (r != 0) + return r; + + if (dns_question_isempty(p.question)) + return sd_varlink_error_invalid_parameter_name(link, "question"); + + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + + _cleanup_free_ struct local_address *addresses = NULL; + bool found = false, nxdomain = false; + int n_addresses = -1; + + DnsResourceKey *key; + DNS_QUESTION_FOREACH(key, p.question) { + Machine *machine = hashmap_get(m->machines, dns_resource_key_name(key)); + if (machine) { + /* We found a perfect match, yay! */ + found = true; + + if (!dns_resource_key_is_address(key)) + continue; + + if (n_addresses < 0) { + n_addresses = machine_get_addresses(machine, &addresses); + if (n_addresses < 0) + return n_addresses; + } + + int family = dns_type_to_af(key->type); + FOREACH_ARRAY(address, addresses, n_addresses) { + if (address->family != family) + continue; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + r = dns_resource_record_new_address(&rr, address->family, &address->address, machine->name); + if (r < 0) + return r; + + r = dns_answer_add_extend( + &answer, + rr, + machine->n_netif == 1 ? machine->netif[0] : -1, + DNS_ANSWER_AUTHENTICATED, + /* rrsig= */ NULL); + if (r < 0) + return r; + } + } + + /* So this is not a direct match? Then check if we find a prefix match */ + const char *q = dns_resource_key_name(key); + while (!nxdomain) { + r = dns_name_parent(&q); + if (r < 0) + return r; + if (r == 0) + break; + + nxdomain = !!hashmap_get(m->machines, q); + } + } + + if (!found) { + /* If we found a prefix match we own the subtree, and thus return NXDOMAIN because we know + * that we only expose the machine A/AAAA records on the primary name, but nothing below. */ + if (nxdomain) + return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_NXDOMAIN)); + + /* Otherwise we return an empty response, which means: continue with the usual lookup */ + return sd_varlink_reply(link, /* parameters= */ NULL); + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *ja = NULL; + r = dns_answer_to_json(answer, &ja); + if (r < 0) + return r; + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_SUCCESS), + SD_JSON_BUILD_PAIR_VARIANT("answer", ja)); +} diff --git a/src/machine/machined-resolve-hook.h b/src/machine/machined-resolve-hook.h new file mode 100644 index 0000000000..df1056216d --- /dev/null +++ b/src/machine/machined-resolve-hook.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "machine-forward.h" + +int vl_method_query_filter(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); +int vl_method_resolve_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata); + +int manager_notify_hook_filters(Manager *m); diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c index 672bf919a0..8188aeb4b1 100644 --- a/src/machine/machined-varlink.c +++ b/src/machine/machined-varlink.c @@ -17,14 +17,17 @@ #include "machine.h" #include "machine-varlink.h" #include "machined.h" +#include "machined-resolve-hook.h" #include "machined-varlink.h" #include "path-lookup.h" +#include "set.h" #include "string-util.h" #include "strv.h" #include "user-util.h" #include "varlink-io.systemd.Machine.h" #include "varlink-io.systemd.MachineImage.h" #include "varlink-io.systemd.UserDatabase.h" +#include "varlink-io.systemd.Resolve.Hook.h" #include "varlink-io.systemd.service.h" #include "varlink-util.h" @@ -847,6 +850,57 @@ static int manager_varlink_init_machine(Manager *m) { return 0; } +static void on_resolve_hook_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + if (set_remove(m->query_filter_subscriptions, link)) + sd_varlink_unref(link); +} + +static int manager_varlink_init_resolve_hook(Manager *m) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL; + int r; + + assert(m); + + if (m->varlink_resolve_hook_server) + return 0; + if (m->runtime_scope != RUNTIME_SCOPE_SYSTEM) /* no resolved in per-user mode! */ + return 0; + + r = varlink_server_new(&s, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + (void) sd_varlink_server_set_description(s, "varlink-resolve-hook"); + + r = sd_varlink_server_add_interface(s, &vl_interface_io_systemd_Resolve_Hook); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve.Hook interface to varlink server: %m"); + + r = sd_varlink_server_bind_method_many( + s, + "io.systemd.Resolve.Hook.QueryFilter", vl_method_query_filter, + "io.systemd.Resolve.Hook.ResolveRecord", vl_method_resolve_record); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + r = sd_varlink_server_bind_disconnect(s, on_resolve_hook_disconnect); + if (r < 0) + return log_error_errno(r, "Failed to bind on resolve hook disconnection events: %m"); + + r = sd_varlink_server_listen_address(s, "/run/systemd/resolve.hook/io.systemd.Machine", 0666 | SD_VARLINK_SERVER_MODE_MKDIR_0755); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = sd_varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_resolve_hook_server = TAKE_PTR(s); + return 0; +} + int manager_varlink_init(Manager *m) { int r; @@ -858,6 +912,10 @@ int manager_varlink_init(Manager *m) { if (r < 0) return r; + r = manager_varlink_init_resolve_hook(m); + if (r < 0) + return r; + return 0; } @@ -866,4 +924,5 @@ void manager_varlink_done(Manager *m) { m->varlink_userdb_server = sd_varlink_server_unref(m->varlink_userdb_server); m->varlink_machine_server = sd_varlink_server_unref(m->varlink_machine_server); + m->varlink_resolve_hook_server = sd_varlink_server_unref(m->varlink_resolve_hook_server); } diff --git a/src/machine/machined.c b/src/machine/machined.c index c6b13e6cba..b972b182b6 100644 --- a/src/machine/machined.c +++ b/src/machine/machined.c @@ -29,6 +29,7 @@ #include "operation.h" #include "path-lookup.h" #include "service-util.h" +#include "set.h" #include "signal-util.h" #include "socket-util.h" #include "special.h" @@ -110,6 +111,8 @@ static Manager* manager_unref(Manager *m) { manager_varlink_done(m); + m->query_filter_subscriptions = set_free(m->query_filter_subscriptions); + sd_bus_flush_close_unref(m->api_bus); sd_bus_flush_close_unref(m->system_bus); sd_event_unref(m->event); diff --git a/src/machine/machined.h b/src/machine/machined.h index 483d41abed..912b9b0ea7 100644 --- a/src/machine/machined.h +++ b/src/machine/machined.h @@ -32,6 +32,8 @@ typedef struct Manager { sd_varlink_server *varlink_userdb_server; sd_varlink_server *varlink_machine_server; + sd_varlink_server *varlink_resolve_hook_server; + Set *query_filter_subscriptions; RuntimeScope runtime_scope; char *state_dir; diff --git a/src/machine/meson.build b/src/machine/meson.build index 8de7d7d2ae..e8b6c1611f 100644 --- a/src/machine/meson.build +++ b/src/machine/meson.build @@ -16,6 +16,7 @@ systemd_machined_extract_sources = files( 'machine.c', 'machined-core.c', 'machined-dbus.c', + 'machined-resolve-hook.c', 'machined-varlink.c', 'operation.c', ) diff --git a/src/shared/dns-answer.c b/src/shared/dns-answer.c index 7ebbf8795d..eaf2b19a18 100644 --- a/src/shared/dns-answer.c +++ b/src/shared/dns-answer.c @@ -2,6 +2,8 @@ #include +#include "sd-json.h" + #include "alloc-util.h" #include "dns-answer.h" #include "dns-domain.h" @@ -866,3 +868,36 @@ uint32_t dns_answer_min_ttl(DnsAnswer *a) { return ttl; } + +int dns_answer_to_json(DnsAnswer *answer, sd_json_variant **ret) { + int r; + + assert(ret); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *ja = NULL; + DnsResourceRecord *rr; + DNS_ANSWER_FOREACH(rr, answer) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + r = dns_resource_record_to_json(rr, &v); + if (r < 0) + return r; + + r = dns_resource_record_to_wire_format(rr, /* canonical= */ false); + if (r < 0) + return r; + + r = sd_json_variant_append_arraybo( + &ja, + SD_JSON_BUILD_PAIR_VARIANT("rr", v), + SD_JSON_BUILD_PAIR_BASE64("raw", rr->wire_format, rr->wire_format_size)); + if (r < 0) + return r; + } + + if (!ja) + return sd_json_variant_new_array(ret, /* array=*/ NULL, /* n= */ 0); + + *ret = TAKE_PTR(ja); + return 0; +} diff --git a/src/shared/dns-answer.h b/src/shared/dns-answer.h index 059f896b86..7b12e9be52 100644 --- a/src/shared/dns-answer.h +++ b/src/shared/dns-answer.h @@ -104,6 +104,8 @@ void dns_answer_randomize(DnsAnswer *a); uint32_t dns_answer_min_ttl(DnsAnswer *a); +int dns_answer_to_json(DnsAnswer *answer, sd_json_variant **ret); + DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref); typedef struct DnsAnswerIterator { diff --git a/src/shared/dns-question.c b/src/shared/dns-question.c index d6466e0793..ac4cc8e998 100644 --- a/src/shared/dns-question.c +++ b/src/shared/dns-question.c @@ -7,6 +7,7 @@ #include "dns-question.h" #include "dns-rr.h" #include "dns-type.h" +#include "json-util.h" #include "socket-util.h" #include "string-util.h" @@ -599,3 +600,37 @@ int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret) { *ret = TAKE_PTR(k); return 0; } + +int dns_json_dispatch_question(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + DnsQuestion **q = ASSERT_PTR(userdata); + int r; + + if (!sd_json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + _cleanup_(dns_question_unrefp) DnsQuestion *nq = NULL; + nq = dns_question_new(sd_json_variant_elements(variant)); + if (!nq) + return json_log_oom(variant, flags); + + sd_json_variant *i; + JSON_VARIANT_ARRAY_FOREACH(i, variant) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + static const sd_json_dispatch_field dispatch_table[] = { + { "key", SD_JSON_VARIANT_OBJECT, dns_json_dispatch_resource_key, 0, SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(i, dispatch_table, flags, &key); + if (r < 0) + return r; + + if (dns_question_add(nq, key, /* flags= */ 0) < 0) + return json_log_oom(variant, flags); + } + + dns_question_unref(*q); + *q = TAKE_PTR(nq); + return 0; +} diff --git a/src/shared/dns-question.h b/src/shared/dns-question.h index 4dd3b27713..3965010278 100644 --- a/src/shared/dns-question.h +++ b/src/shared/dns-question.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "sd-json.h" + #include "shared-forward.h" /* A simple array of resource keys */ @@ -58,6 +60,8 @@ static inline bool dns_question_isempty(DnsQuestion *q) { int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret); +int dns_json_dispatch_question(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); + DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref); #define _DNS_QUESTION_FOREACH(u, k, q) \ diff --git a/src/shared/dns-rr.c b/src/shared/dns-rr.c index 32c26a7612..d0ed39a08b 100644 --- a/src/shared/dns-rr.c +++ b/src/shared/dns-rr.c @@ -2558,3 +2558,16 @@ static const char* const sshfp_key_type_table[_SSHFP_KEY_TYPE_MAX_DEFINED] = { [SSHFP_KEY_TYPE_SHA256] = "SHA-256", /* RFC 4255 */ }; DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(sshfp_key_type, int, 255); + +int dns_json_dispatch_resource_key(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + DnsResourceKey **k = ASSERT_PTR(userdata); + int r; + + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *nk = NULL; + r = dns_resource_key_from_json(variant, &nk); + if (r < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid resource record key.", strna(name)); + + DNS_RESOURCE_KEY_REPLACE(*k, TAKE_PTR(nk)); + return 0; +} diff --git a/src/shared/dns-rr.h b/src/shared/dns-rr.h index 48f77d3326..23752946e7 100644 --- a/src/shared/dns-rr.h +++ b/src/shared/dns-rr.h @@ -444,3 +444,5 @@ int sshfp_algorithm_from_string(const char *s) _pure_; int sshfp_key_type_to_string_alloc(int i, char **ret); int sshfp_key_type_from_string(const char *s) _pure_; + +int dns_json_dispatch_resource_key(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); diff --git a/src/shared/meson.build b/src/shared/meson.build index 0cf0324f97..acc80d3e34 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -169,6 +169,7 @@ shared_sources = files( 'recovery-key.c', 'reread-partition-table.c', 'resize-fs.c', + 'resolve-hook-util.c', 'resolve-util.c', 'rm-rf.c', 'seccomp-util.c', diff --git a/src/shared/resolve-hook-util.c b/src/shared/resolve-hook-util.c new file mode 100644 index 0000000000..b1e948c5d3 --- /dev/null +++ b/src/shared/resolve-hook-util.c @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "resolve-hook-util.h" +#include "dns-question.h" + +void resolve_record_parameters_done(ResolveRecordParameters *p) { + assert(p); + + dns_question_unref(p->question); +} + +const sd_json_dispatch_field resolve_record_parameters_dispatch_table[] = { + { "question", SD_JSON_VARIANT_ARRAY, dns_json_dispatch_question, offsetof(ResolveRecordParameters, question), SD_JSON_MANDATORY }, + {} +}; diff --git a/src/shared/resolve-hook-util.h b/src/shared/resolve-hook-util.h new file mode 100644 index 0000000000..be41eb78aa --- /dev/null +++ b/src/shared/resolve-hook-util.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-json.h" + +#include "shared-forward.h" + +typedef struct ResolveRecordParameters { + DnsQuestion *question; +} ResolveRecordParameters; + +void resolve_record_parameters_done(ResolveRecordParameters *p); + +extern const sd_json_dispatch_field resolve_record_parameters_dispatch_table[]; diff --git a/test/units/TEST-13-NSPAWN.nss-mymachines.sh b/test/units/TEST-13-NSPAWN.nss-mymachines.sh index 2735cf61b4..5709ec50d7 100755 --- a/test/units/TEST-13-NSPAWN.nss-mymachines.sh +++ b/test/units/TEST-13-NSPAWN.nss-mymachines.sh @@ -136,4 +136,12 @@ done (! getent group -s mymachines foo 11) (! getent passwd -s mymachines foo 11) +# Now check the machined's hook for resolved too +run_and_grep "10\.1\.0\.2" resolvectl query nss-mymachines-singleip + +run_and_grep "fd00:dead:beef:cafe::2" resolvectl query nss-mymachines-manyips +for i in {100..120}; do + run_and_grep "10\.2\.0\.$i" resolvectl query nss-mymachines-manyips +done + machinectl stop nss-mymachines-{noip,singleip,manyips} From 4408db690886308d16059d3c8423546f4c29aa75 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Oct 2025 11:25:50 +0200 Subject: [PATCH 04/11] sd-dhcp-server: add api to get address from hostname, based on lease data --- src/libsystemd-network/sd-dhcp-server.c | 35 +++++++++++++++++++++++++ src/systemd/sd-dhcp-server.h | 2 ++ 2 files changed, 37 insertions(+) diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index 7503160643..c86e4e0e0c 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -1711,3 +1711,38 @@ int sd_dhcp_server_set_lease_file(sd_dhcp_server *server, int dir_fd, const char return 0; } + +static int find_lease_address(Hashmap *h, const char *name, struct in_addr *ret) { + int r; + + assert(name); + + sd_dhcp_server_lease *lease; + HASHMAP_FOREACH(lease, h) { + if (!lease->hostname) + continue; + + r = dns_name_equal(lease->hostname, name); + if (r <= 0) + continue; + + if (ret) + ret->s_addr = lease->address; + return 1; + } + + return -ENOENT; +} + +int sd_dhcp_server_get_lease_address_by_name(sd_dhcp_server *server, const char *name, struct in_addr *ret) { + int r; + + assert_return(server, -EINVAL); + assert_return(dns_name_is_valid(name), -EINVAL); + + r = find_lease_address(server->static_leases_by_address, name, ret); + if (r != -ENOENT) + return r; + + return find_lease_address(server->bound_leases_by_address, name, ret); +} diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h index 2069b9d287..787d2f4169 100644 --- a/src/systemd/sd-dhcp-server.h +++ b/src/systemd/sd-dhcp-server.h @@ -98,6 +98,8 @@ int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server); int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr* address); int sd_dhcp_server_set_relay_agent_information(sd_dhcp_server *server, const char* circuit_id, const char* remote_id); +int sd_dhcp_server_get_lease_address_by_name(sd_dhcp_server *server, const char *name, struct in_addr *ret); + _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dhcp_server, sd_dhcp_server_unref); _SD_END_DECLARATIONS; From f041d40dee1f99e271d61fbfd9e048d5ca229981 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Sat, 15 Nov 2025 00:37:58 +0000 Subject: [PATCH 05/11] test: always create networkd mock tmpfs for networkd-test.py Match the behaviour of the other test classes that use sd-run and always create the mock tmpfs runtime dirs. This will be needed as the new resolve.hook directory won't exist on boot but will be needed by the test case. --- test/networkd-test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/networkd-test.py b/test/networkd-test.py index c822511974..8ea6cd43a0 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -91,9 +91,8 @@ def setUpModule(): for d in ['/etc/systemd/network', '/run/systemd/network', '/run/systemd/netif', '/run/systemd/resolve']: - if os.path.isdir(d): - subprocess.check_call(["mount", "-t", "tmpfs", "none", d]) - tmpmounts.append(d) + subprocess.check_call(["mount", "-m", "-t", "tmpfs", "none", d]) + tmpmounts.append(d) if os.path.isdir('/run/systemd/resolve'): os.chmod('/run/systemd/resolve', 0o755) shutil.chown('/run/systemd/resolve', 'systemd-resolve', 'systemd-resolve') From a7fa29b1b52210e33f4e43efc1a2f06b7c7233c0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Oct 2025 09:57:48 +0200 Subject: [PATCH 06/11] networkd: add support for resolved hook for DHCP server Let's synthesize DNS RRs for leases handed out by our DHCP server. This way local VMs can have resolvable hostnames locally. This does not implement reverse look ups for now. We can add this later in similar fashion. --- man/systemd.network.xml | 17 ++ src/network/meson.build | 1 + src/network/networkd-dhcp-server.c | 3 + src/network/networkd-link.c | 22 ++ src/network/networkd-link.h | 2 + src/network/networkd-manager-varlink.c | 6 - src/network/networkd-manager-varlink.h | 1 - src/network/networkd-manager.c | 25 +- src/network/networkd-manager.h | 2 + src/network/networkd-network-gperf.gperf | 1 + src/network/networkd-network.c | 1 + src/network/networkd-network.h | 1 + src/network/networkd-resolve-hook.c | 261 ++++++++++++++++++ src/network/networkd-resolve-hook.h | 7 + test/networkd-test.py | 16 +- .../conf/25-dhcp-client-resolve-hook.network | 10 + .../conf/25-dhcp-server-resolve-hook.network | 13 + test/test-network/systemd-networkd-tests.py | 26 ++ units/meson.build | 4 + units/systemd-networkd-resolve-hook.socket | 26 ++ units/systemd-networkd.service.in | 4 +- 21 files changed, 434 insertions(+), 15 deletions(-) create mode 100644 src/network/networkd-resolve-hook.c create mode 100644 src/network/networkd-resolve-hook.h create mode 100644 test/test-network/conf/25-dhcp-client-resolve-hook.network create mode 100644 test/test-network/conf/25-dhcp-server-resolve-hook.network create mode 100644 units/systemd-networkd-resolve-hook.socket diff --git a/man/systemd.network.xml b/man/systemd.network.xml index a75f89de99..dda1e117ed 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -4201,6 +4201,23 @@ ServerAddress=192.168.0.1/24 + + LocalLeaseDomain= + + Takes a DNS domain name as argument. If specified, + systemd-networkd.service8 + will integrate with + systemd-resolved.service8 + and ensure that the hostnames associated with each handed out DHCP lease may be resolved to the IP + addresses of the lease. The hostnames are suffixed with the specified domain name. + + Note that this purely about hostname resolution on the local system, i.e. from programs with + access to the same systemd-resolved instances via D-Bus IPC, Varlink IPC, or + the local DNS stub. + + + + diff --git a/src/network/meson.build b/src/network/meson.build index 78a0e72b3a..2b66aa2743 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -74,6 +74,7 @@ systemd_networkd_extract_sources = files( 'networkd-ntp.c', 'networkd-queue.c', 'networkd-radv.c', + 'networkd-resolve-hook.c', 'networkd-route.c', 'networkd-route-metric.c', 'networkd-route-nexthop.c', diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c index 4b1dbe6eb9..75ecfdca91 100644 --- a/src/network/networkd-dhcp-server.c +++ b/src/network/networkd-dhcp-server.c @@ -25,6 +25,7 @@ #include "networkd-network.h" #include "networkd-ntp.h" #include "networkd-queue.h" +#include "networkd-resolve-hook.h" #include "networkd-route-util.h" #include "path-util.h" #include "set.h" @@ -747,6 +748,8 @@ static int dhcp4_server_configure(Link *link) { if (r < 0) return log_link_error_errno(link, r, "Could not start DHCPv4 server instance: %m"); + manager_notify_hook_filters(link->manager); + return 0; } diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index 9c3de19766..458ddff27f 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -23,6 +23,7 @@ #include "arphrd-util.h" #include "bitfield.h" #include "device-util.h" +#include "dns-domain.h" #include "errno-util.h" #include "ethtool-util.h" #include "event-util.h" @@ -53,6 +54,7 @@ #include "networkd-nexthop.h" #include "networkd-queue.h" #include "networkd-radv.h" +#include "networkd-resolve-hook.h" #include "networkd-route.h" #include "networkd-route-util.h" #include "networkd-routing-policy-rule.h" @@ -1070,6 +1072,8 @@ static Link *link_drop(Link *link) { assert(link->manager); + bool notify = link_has_local_lease_domain(link); + link_set_state(link, LINK_STATE_LINGER); /* Drop all references from other links and manager. Note that async netlink calls may have @@ -1098,6 +1102,10 @@ static Link *link_drop(Link *link) { /* The following must be called at last. */ assert_se(hashmap_remove(link->manager->links_by_index, INT_TO_PTR(link->ifindex)) == link); + + if (notify) + manager_notify_hook_filters(link->manager); + return link_unref(link); } @@ -1351,6 +1359,8 @@ static void link_enter_unmanaged(Link *link) { if (link->state == LINK_STATE_UNMANAGED) return; + bool notify = link_has_local_lease_domain(link); + log_link_full(link, link->state == LINK_STATE_INITIALIZED ? LOG_DEBUG : LOG_INFO, "Unmanaging interface."); @@ -1367,6 +1377,9 @@ static void link_enter_unmanaged(Link *link) { link->network = network_unref(link->network); link_set_state(link, LINK_STATE_UNMANAGED); + + if (notify) + manager_notify_hook_filters(link->manager); } static int link_managed_by_us(Link *link) { @@ -3061,3 +3074,12 @@ static const char * const kernel_operstate_table[] = { }; DEFINE_STRING_TABLE_LOOKUP_TO_STRING(kernel_operstate, int); + +bool link_has_local_lease_domain(Link *link) { + assert(link); + + return link->dhcp_server && + link->network && + link->network->dhcp_server_local_lease_domain && + !dns_name_is_root(link->network->dhcp_server_local_lease_domain); +} diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index 6e936e3bb1..8c5648d8d0 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -257,3 +257,5 @@ const char* kernel_operstate_to_string(int t) _const_; void link_required_operstate_for_online(Link *link, LinkOperationalStateRange *ret); AddressFamily link_required_family_for_online(Link *link); + +bool link_has_local_lease_domain(Link *link); diff --git a/src/network/networkd-manager-varlink.c b/src/network/networkd-manager-varlink.c index 871e5a4636..cc46e60e40 100644 --- a/src/network/networkd-manager-varlink.c +++ b/src/network/networkd-manager-varlink.c @@ -335,9 +335,3 @@ int manager_connect_varlink(Manager *m, int fd) { m->varlink_server = TAKE_PTR(s); return 0; } - -void manager_varlink_done(Manager *m) { - assert(m); - - m->varlink_server = sd_varlink_server_unref(m->varlink_server); -} diff --git a/src/network/networkd-manager-varlink.h b/src/network/networkd-manager-varlink.h index bd5cede404..0933c2a056 100644 --- a/src/network/networkd-manager-varlink.h +++ b/src/network/networkd-manager-varlink.h @@ -4,4 +4,3 @@ #include "networkd-forward.h" int manager_connect_varlink(Manager *m, int fd); -void manager_varlink_done(Manager *m); diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c index 4004be81c0..762f1ca190 100644 --- a/src/network/networkd-manager.c +++ b/src/network/networkd-manager.c @@ -8,6 +8,7 @@ #include "sd-event.h" #include "sd-netlink.h" #include "sd-resolve.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "bus-error.h" @@ -37,6 +38,7 @@ #include "networkd-neighbor.h" #include "networkd-nexthop.h" #include "networkd-queue.h" +#include "networkd-resolve-hook.h" #include "networkd-route.h" #include "networkd-routing-policy-rule.h" #include "networkd-serialize.h" @@ -205,13 +207,14 @@ static int manager_connect_udev(Manager *m) { return 0; } -static int manager_listen_fds(Manager *m, int *ret_rtnl_fd, int *ret_varlink_fd) { +static int manager_listen_fds(Manager *m, int *ret_rtnl_fd, int *ret_varlink_fd, int *ret_resolve_hook_fd) { _cleanup_strv_free_ char **names = NULL; - int n, rtnl_fd = -EBADF, varlink_fd = -EBADF; + int n, rtnl_fd = -EBADF, varlink_fd = -EBADF, resolve_hook_fd = -EBADF; assert(m); assert(ret_rtnl_fd); assert(ret_varlink_fd); + assert(ret_resolve_hook_fd); n = sd_listen_fds_with_names(/* unset_environment = */ true, &names); if (n < 0) @@ -235,6 +238,11 @@ static int manager_listen_fds(Manager *m, int *ret_rtnl_fd, int *ret_varlink_fd) continue; } + if (streq(names[i], "resolve-hook")) { + resolve_hook_fd = fd; + continue; + } + if (manager_set_serialization_fd(m, fd, names[i]) >= 0) continue; @@ -250,6 +258,7 @@ static int manager_listen_fds(Manager *m, int *ret_rtnl_fd, int *ret_varlink_fd) *ret_rtnl_fd = rtnl_fd; *ret_varlink_fd = varlink_fd; + *ret_resolve_hook_fd = resolve_hook_fd; return 0; } @@ -543,7 +552,7 @@ static int manager_set_keep_configuration(Manager *m) { } int manager_setup(Manager *m) { - _cleanup_close_ int rtnl_fd = -EBADF, varlink_fd = -EBADF; + _cleanup_close_ int rtnl_fd = -EBADF, varlink_fd = -EBADF, resolve_hook_fd = -EBADF; int r; assert(m); @@ -567,7 +576,7 @@ int manager_setup(Manager *m) { if (r < 0) return r; - r = manager_listen_fds(m, &rtnl_fd, &varlink_fd); + r = manager_listen_fds(m, &rtnl_fd, &varlink_fd, &resolve_hook_fd); if (r < 0) return r; @@ -590,6 +599,10 @@ int manager_setup(Manager *m) { if (r < 0) return r; + r = manager_varlink_init_resolve_hook(m, TAKE_FD(resolve_hook_fd)); + if (r < 0) + return r; + r = manager_connect_bus(m); if (r < 0) return r; @@ -737,7 +750,9 @@ Manager* manager_free(Manager *m) { sd_device_monitor_unref(m->device_monitor); - manager_varlink_done(m); + m->varlink_server = sd_varlink_server_unref(m->varlink_server); + m->varlink_resolve_hook_server = sd_varlink_server_unref(m->varlink_resolve_hook_server); + m->query_filter_subscriptions = set_free(m->query_filter_subscriptions); hashmap_free(m->polkit_registry); sd_bus_flush_close_unref(m->bus); diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h index e350fe80c5..871898abe6 100644 --- a/src/network/networkd-manager.h +++ b/src/network/networkd-manager.h @@ -22,6 +22,8 @@ typedef struct Manager { sd_resolve *resolve; sd_bus *bus; sd_varlink_server *varlink_server; + sd_varlink_server *varlink_resolve_hook_server; + Set *query_filter_subscriptions; sd_device_monitor *device_monitor; Hashmap *polkit_registry; int ethtool_fd; diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 2d0b7d4064..7f3f4c5311 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -393,6 +393,7 @@ DHCPServer.BootServerName, config_parse_dns_name, DHCPServer.BootFilename, config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, offsetof(Network, dhcp_server_boot_filename) DHCPServer.RapidCommit, config_parse_bool, 0, offsetof(Network, dhcp_server_rapid_commit) DHCPServer.PersistLeases, config_parse_dhcp_server_persist_leases, 0, offsetof(Network, dhcp_server_persist_leases) +DHCPServer.LocalLeaseDomain, config_parse_dns_name, 0, offsetof(Network, dhcp_server_local_lease_domain) DHCPServerStaticLease.Address, config_parse_dhcp_static_lease_address, 0, 0 DHCPServerStaticLease.MACAddress, config_parse_dhcp_static_lease_hwaddr, 0, 0 DHCPServerStaticLease.Hostname, config_parse_dhcp_static_lease_hostname, 0, 0 diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 4e8566afa6..e1ecc15235 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -761,6 +761,7 @@ static Network *network_free(Network *network) { free(network->dhcp_server_emit[t].addresses); ordered_hashmap_free(network->dhcp_server_send_options); ordered_hashmap_free(network->dhcp_server_send_vendor_options); + free(network->dhcp_server_local_lease_domain); /* DHCP client */ free(network->dhcp_vendor_class_identifier); diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index edd2177dd3..4ec2783e99 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -233,6 +233,7 @@ typedef struct Network { usec_t dhcp_server_ipv6_only_preferred_usec; bool dhcp_server_rapid_commit; DHCPServerPersistLeases dhcp_server_persist_leases; + char *dhcp_server_local_lease_domain; /* link-local addressing support */ AddressFamily link_local; diff --git a/src/network/networkd-resolve-hook.c b/src/network/networkd-resolve-hook.c new file mode 100644 index 0000000000..6c437be9db --- /dev/null +++ b/src/network/networkd-resolve-hook.c @@ -0,0 +1,261 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-dhcp-server.h" +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "dns-answer.h" +#include "dns-domain.h" +#include "dns-packet.h" +#include "dns-question.h" +#include "dns-rr.h" +#include "env-util.h" +#include "fd-util.h" +#include "networkd-link.h" +#include "networkd-manager.h" +#include "networkd-resolve-hook.h" +#include "resolve-hook-util.h" +#include "set.h" +#include "varlink-io.systemd.Resolve.Hook.h" +#include "varlink-util.h" + +static int manager_make_domain_array(Manager *m, sd_json_variant **ret) { + int r; + + assert(m); + assert(ret); + + _cleanup_(set_freep) Set *domains = NULL; + Link *link; + HASHMAP_FOREACH(link, m->links_by_index) { + if (!link_has_local_lease_domain(link)) + continue; + + r = set_put_strdup_full(&domains, &dns_name_hash_ops_free, link->network->dhcp_server_local_lease_domain); + if (r < 0 && r != -EEXIST) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + char *s; + SET_FOREACH(s, domains) { + r = sd_json_variant_append_arrayb(&array, SD_JSON_BUILD_STRING(s)); + if (r < 0) + return r; + } + + if (!array) + return sd_json_variant_new_array(ret, /* array= */ NULL, /* n= */ 0); + + *ret = TAKE_PTR(array); + return 0; +} + +int manager_notify_hook_filters(Manager *m) { + int r; + + assert(m); + + /* Called whenever a machine is added or dropped from the list */ + + if (set_isempty(m->query_filter_subscriptions)) + return 0; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + r = manager_make_domain_array(m, &array); + if (r < 0) + return log_error_errno(r, "Failed to generate JSON array with machine names: %m"); + + r = varlink_many_notifybo(m->query_filter_subscriptions, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + + return 0; +} + +static int vl_method_query_filter( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(link); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + r = manager_make_domain_array(m, &array); + if (r < 0) + return r; + + if (flags & SD_VARLINK_METHOD_MORE) { + /* If 'more' is set, this is a subscription request, keep track of the link */ + + r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + + r = set_ensure_put(&m->query_filter_subscriptions, &varlink_hash_ops, link); + if (r < 0) + return r; + + sd_varlink_ref(link); + } else { + r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array)); + if (r < 0) + return log_error_errno(r, "Failed to notify filter subscribers: %m"); + } + + return 0; +} + +static int vl_method_resolve_record( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(link); + + _cleanup_(resolve_record_parameters_done) ResolveRecordParameters p = {}; + r = sd_varlink_dispatch(link, parameters, resolve_record_parameters_dispatch_table, &p); + if (r != 0) + return r; + + if (dns_question_isempty(p.question)) + return sd_varlink_error_invalid_parameter_name(link, "question"); + + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + bool found_address = false, found_domain = false; + DnsResourceKey *key; + DNS_QUESTION_FOREACH(key, p.question) { + const char *name = dns_resource_key_name(key); + + Link *l; + HASHMAP_FOREACH(l, m->links_by_index) { + + if (!link_has_local_lease_domain(l)) + continue; + + /* Try to strip the local lease domain suffix from name, so that we have the short hostname left. */ + _cleanup_free_ char *prefix = NULL; + r = dns_name_change_suffix(name, l->network->dhcp_server_local_lease_domain, /* new_suffix= */ NULL, &prefix); + if (r <= 0) /* no match? */ + continue; + + found_domain = true; + + struct in_addr address; + r = sd_dhcp_server_get_lease_address_by_name(l->dhcp_server, prefix, &address); + if (r <= 0) + continue; + + /* The domain exists, so we can give a positive reply. But only for A lookups we have addresses to return. */ + if (key->type != DNS_TYPE_A) + continue; + + found_address = true; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + r = dns_resource_record_new_address(&rr, AF_INET, (union in_addr_union*) &address, name); + if (r < 0) + return r; + + r = dns_answer_add_extend( + &answer, + rr, + l->ifindex, + DNS_ANSWER_AUTHENTICATED, + /* rrsig= */ NULL); + if (r < 0) + return r; + } + } + + if (!found_address) { + /* If this was a lookup in one of our domains, return NXDOMAIN, we are authoritative on that */ + if (found_domain) + return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_NXDOMAIN)); + + /* Otherwise we return an empty response, which means: continue with the usual lookup */ + return sd_varlink_reply(link, /* parameters= */ NULL); + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *ja = NULL; + r = dns_answer_to_json(answer, &ja); + if (r < 0) + return r; + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_SUCCESS), + SD_JSON_BUILD_PAIR_VARIANT("answer", ja)); +} + +static void on_resolve_hook_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + if (set_remove(m->query_filter_subscriptions, link)) + sd_varlink_unref(link); +} + +int manager_varlink_init_resolve_hook(Manager *m, int fd) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL; + _unused_ _cleanup_close_ int fd_close = fd; /* take possession */ + int r; + + assert(m); + + if (m->varlink_resolve_hook_server) + return 0; + + r = getenv_bool("SYSTEMD_NETWORK_RESOLVE_HOOK"); + if (r < 0 && r != -ENXIO) + log_warning_errno(r, "Failed to parse $SYSTEMD_NETWORK_RESOLVE_HOOK, ignoring: %m"); + if (r == 0) { + log_notice("Resolve hook disabled via $SYSTEMD_NETWORK_RESOLVE_HOOK."); + return 0; + } + + r = varlink_server_new(&s, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + (void) sd_varlink_server_set_description(s, "varlink-resolve-hook"); + + r = sd_varlink_server_add_interface(s, &vl_interface_io_systemd_Resolve_Hook); + if (r < 0) + return log_error_errno(r, "Failed to add Resolve.Hook interface to varlink server: %m"); + + r = sd_varlink_server_bind_method_many( + s, + "io.systemd.Resolve.Hook.QueryFilter", vl_method_query_filter, + "io.systemd.Resolve.Hook.ResolveRecord", vl_method_resolve_record); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + r = sd_varlink_server_bind_disconnect(s, on_resolve_hook_disconnect); + if (r < 0) + return log_error_errno(r, "Failed to bind on resolve hook disconnection events: %m"); + + if (fd < 0) + r = sd_varlink_server_listen_address(s, "/run/systemd/resolve.hook/io.systemd.Network", 0666 | SD_VARLINK_SERVER_MODE_MKDIR_0755); + else + r = sd_varlink_server_listen_fd(s, fd); + if (r < 0) + return log_error_errno(r, "Failed to bind to systemd-resolved hook Varlink socket: %m"); + + TAKE_FD(fd_close); + + r = sd_varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_resolve_hook_server = TAKE_PTR(s); + return 0; +} diff --git a/src/network/networkd-resolve-hook.h b/src/network/networkd-resolve-hook.h new file mode 100644 index 0000000000..3ad185be52 --- /dev/null +++ b/src/network/networkd-resolve-hook.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "networkd-forward.h" + +int manager_notify_hook_filters(Manager *m); +int manager_varlink_init_resolve_hook(Manager *m, int fd); diff --git a/test/networkd-test.py b/test/networkd-test.py index 8ea6cd43a0..691f58b2d3 100755 --- a/test/networkd-test.py +++ b/test/networkd-test.py @@ -65,6 +65,7 @@ def setUpModule(): for u in [ 'systemd-networkd.socket', 'systemd-networkd-varlink.socket', + 'systemd-networkd-resolve-hook.socket', 'systemd-networkd.service', 'systemd-resolved-varlink.socket', 'systemd-resolved-monitor.socket', @@ -90,12 +91,15 @@ def setUpModule(): subprocess.call(['useradd', '--system', '--no-create-home', 'systemd-network']) for d in ['/etc/systemd/network', '/run/systemd/network', - '/run/systemd/netif', '/run/systemd/resolve']: + '/run/systemd/netif', '/run/systemd/resolve', '/run/systemd/resolve.hook']: subprocess.check_call(["mount", "-m", "-t", "tmpfs", "none", d]) tmpmounts.append(d) if os.path.isdir('/run/systemd/resolve'): os.chmod('/run/systemd/resolve', 0o755) shutil.chown('/run/systemd/resolve', 'systemd-resolve', 'systemd-resolve') + if os.path.isdir('/run/systemd/resolve.hook'): + os.chmod('/run/systemd/resolve.hook', 0o755) + shutil.chown('/run/systemd/resolve.hook', 'systemd-network', 'systemd-network') if os.path.isdir('/run/systemd/netif'): os.chmod('/run/systemd/netif', 0o755) shutil.chown('/run/systemd/netif', 'systemd-network', 'systemd-network') @@ -278,6 +282,8 @@ Gateway=192.168.250.1 def tearDown(self): subprocess.check_call(['systemctl', 'stop', 'systemd-networkd.socket']) + subprocess.check_call(['systemctl', 'stop', 'systemd-networkd-varlink.socket']) + subprocess.check_call(['systemctl', 'stop', 'systemd-networkd-resolve-hook.socket']) subprocess.check_call(['systemctl', 'stop', 'systemd-networkd.service']) subprocess.check_call(['ip', 'link', 'del', 'mybridge']) subprocess.check_call(['ip', 'link', 'del', 'port1']) @@ -373,6 +379,8 @@ class ClientTestBase(NetworkdTestingUtilities): def tearDown(self): self.shutdown_iface() subprocess.call(['systemctl', 'stop', 'systemd-networkd.socket']) + subprocess.call(['systemctl', 'stop', 'systemd-networkd-varlink.socket']) + subprocess.call(['systemctl', 'stop', 'systemd-networkd-resolve-hook.socket']) subprocess.call(['systemctl', 'stop', 'systemd-networkd.service']) subprocess.call(['ip', 'link', 'del', 'dummy0'], stderr=subprocess.DEVNULL) @@ -930,9 +938,11 @@ class NetworkdClientTest(ClientTestBase, unittest.TestCase): set -eu mkdir -p /run/systemd/network mkdir -p /run/systemd/netif +mkdir -p /run/systemd/resolve.hook mkdir -p /var/lib/systemd/network mount -t tmpfs none /run/systemd/network mount -t tmpfs none /run/systemd/netif +mount -t tmpfs none /run/systemd/resolve.hook mount -t tmpfs none /var/lib/systemd/network [ ! -e /run/dbus ] || mount -t tmpfs none /run/dbus # create router/client veth pair @@ -966,6 +976,9 @@ EOF # Hence, 'networkctl persistent-storage yes' cannot be used. export SYSTEMD_NETWORK_PERSISTENT_STORAGE_READY=1 +# Don't try to register resolved hook for our testcase +export SYSTEMD_NETWORK_RESOLVE_HOOK=0 + # Generate debugging logs. export SYSTEMD_LOG_LEVEL=debug @@ -982,6 +995,7 @@ exec $(systemctl cat systemd-networkd.service | sed -n '/^ExecStart=/ {{ s/^.*=/ '-p', 'InaccessibleDirectories=-/etc/systemd/network', '-p', 'InaccessibleDirectories=-/run/systemd/network', '-p', 'InaccessibleDirectories=-/run/systemd/netif', + '-p', 'InaccessibleDirectories=-/run/systemd/resolve.hook', '-p', 'InaccessibleDirectories=-/var/lib/systemd/network', '--service-type=notify', script]) diff --git a/test/test-network/conf/25-dhcp-client-resolve-hook.network b/test/test-network/conf/25-dhcp-client-resolve-hook.network new file mode 100644 index 0000000000..5f4ed00d75 --- /dev/null +++ b/test/test-network/conf/25-dhcp-client-resolve-hook.network @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth99 + +[Network] +DHCP=yes + +[DHCPv4] +SendHostname=yes +Hostname=flummy diff --git a/test/test-network/conf/25-dhcp-server-resolve-hook.network b/test/test-network/conf/25-dhcp-server-resolve-hook.network new file mode 100644 index 0000000000..ec6ee8d895 --- /dev/null +++ b/test/test-network/conf/25-dhcp-server-resolve-hook.network @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth-peer + +[Network] +IPv6AcceptRA=false +DHCPServer=yes + +[DHCPServer] +ServerAddress=192.168.5.1/24 +PoolOffset=20 +PoolSize=10 +LocalLeaseDomain=_networkdtest diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 32c7b59044..2210a99c07 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -426,6 +426,7 @@ def save_active_units(): for u in [ 'systemd-networkd.socket', 'systemd-networkd-varlink.socket', + 'systemd-networkd-resolve-hook.socket', 'systemd-networkd.service', 'systemd-resolved-monitor.socket', 'systemd-resolved-varlink.socket', @@ -449,6 +450,10 @@ def restore_active_units(): call('systemctl stop systemd-networkd-varlink.socket') has_network_socket = True + if 'systemd-networkd-resolve-hook.socket' in active_units: + call('systemctl stop systemd-networkd-resolve-hook.socket') + has_network_socket = True + if 'systemd-resolved-monitor.socket' in active_units: call('systemctl stop systemd-resolved-monitor.socket') has_resolve_socket = True @@ -525,6 +530,7 @@ def setup_system_units(): 'systemd-networkd.service', 'systemd-networkd.socket', 'systemd-networkd-varlink.socket', + 'systemd-networkd-resolve-hook.socket', 'systemd-networkd-persistent-storage.service', 'systemd-resolved.service', 'systemd-timesyncd.service', @@ -572,6 +578,13 @@ def setup_system_units(): 'StartLimitIntervalSec=0', ] ) + create_unit_dropin( + 'systemd-networkd-resolve-hook.socket', + [ + '[Unit]', + 'StartLimitIntervalSec=0', + ] + ) create_unit_dropin( 'systemd-networkd-persistent-storage.service', [ @@ -604,6 +617,7 @@ def clear_system_units(): rm_unit('systemd-networkd.service') rm_unit('systemd-networkd.socket') rm_unit('systemd-networkd-varlink.socket') + rm_unit('systemd-networkd-resolve-hook.socket') rm_unit('systemd-networkd-persistent-storage.service') rm_unit('systemd-resolved.service') rm_unit('systemd-timesyncd.service') @@ -990,10 +1004,12 @@ def stop_networkd(show_logs=True, check_failed=True): if check_failed: check_output('systemctl stop systemd-networkd.socket') check_output('systemctl stop systemd-networkd-varlink.socket') + check_output('systemctl stop systemd-networkd-resolve-hook.socket') check_output('systemctl stop systemd-networkd.service') else: call('systemctl stop systemd-networkd.socket') call('systemctl stop systemd-networkd-varlink.socket') + call('systemctl stop systemd-networkd-resolve-hook.socket') call('systemctl stop systemd-networkd.service') if show_logs: @@ -7366,6 +7382,16 @@ class NetworkdDHCPServerTests(unittest.TestCase, Utilities): data = json.loads(output) self.assertEqual(data['DHCPv4Client']['Lease']['Hostname'], 'fqdn.example.com') + def test_dhcp_server_resolve_hook(self): + copy_network_unit('25-veth.netdev', '25-dhcp-client-resolve-hook.network', '25-dhcp-server-resolve-hook.network') + start_networkd() + self.wait_online('veth99:routable', 'veth-peer:routable') + + output = check_output('resolvectl query flummy._networkdtest') + print(output) + self.assertIn('192.168.5.2', output) + + class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities): def setUp(self): diff --git a/units/meson.build b/units/meson.build index 8e5b645f91..2e04c4aa2b 100644 --- a/units/meson.build +++ b/units/meson.build @@ -497,6 +497,10 @@ units = [ 'file' : 'systemd-networkd-varlink.socket', 'conditions' : ['ENABLE_NETWORKD'], }, + { + 'file' : 'systemd-networkd-resolve-hook.socket', + 'conditions' : ['ENABLE_NETWORKD'], + }, { 'file' : 'systemd-networkd-wait-online.service.in', 'conditions' : ['ENABLE_NETWORKD'], diff --git a/units/systemd-networkd-resolve-hook.socket b/units/systemd-networkd-resolve-hook.socket new file mode 100644 index 0000000000..3c11b8e8de --- /dev/null +++ b/units/systemd-networkd-resolve-hook.socket @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Network Management Resolve Hook Socket +Documentation=man:systemd-networkd.service(8) +ConditionCapability=CAP_NET_ADMIN +DefaultDependencies=no +Before=sockets.target shutdown.target +Conflicts=shutdown.target + +[Socket] +ListenStream=/run/systemd/resolve.hook/io.systemd.Network +FileDescriptorName=resolve-hook +SocketMode=0666 +Service=systemd-networkd.service +RemoveOnStop=yes + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-networkd.service.in b/units/systemd-networkd.service.in index 44103f7daa..589782effc 100644 --- a/units/systemd-networkd.service.in +++ b/units/systemd-networkd.service.in @@ -46,7 +46,7 @@ RestrictRealtime=yes RestrictSUIDSGID=yes RuntimeDirectory=systemd/netif RuntimeDirectoryPreserve=yes -Sockets=systemd-networkd.socket systemd-networkd-varlink.socket +Sockets=systemd-networkd.socket systemd-networkd-varlink.socket systemd-networkd-resolve-hook.socket SystemCallArchitectures=native SystemCallErrorNumber=EPERM SystemCallFilter=@system-service bpf @@ -56,7 +56,7 @@ User=systemd-network [Install] WantedBy=multi-user.target -Also=systemd-networkd.socket systemd-networkd-varlink.socket +Also=systemd-networkd.socket systemd-networkd-varlink.socket systemd-networkd-resolve-hook.socket Alias=dbus-org.freedesktop.network1.service # The output from this generator is used by udevd and networkd. Enable it by From 5615ac1f0d5aa3a0623433e57a072f0eb9bb8ded Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Oct 2025 12:35:35 +0200 Subject: [PATCH 07/11] =?UTF-8?q?networkd:=20rename=20manager=5Fconnect=5F?= =?UTF-8?q?varlink()=20=E2=86=92=20manager=5Fvarlink=5Finit()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function doesn't "connect" to Varlink (i.e. it isn't a client) but it binds a Varlink socket (i.e. it is server), hence let's remove the verb "connect" from its name. let's copy how machined/resolved name the counterpart for this function: manager_varlink_init() --- src/network/networkd-manager-varlink.c | 4 ++-- src/network/networkd-manager-varlink.h | 2 +- src/network/networkd-manager.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network/networkd-manager-varlink.c b/src/network/networkd-manager-varlink.c index cc46e60e40..a83665b982 100644 --- a/src/network/networkd-manager-varlink.c +++ b/src/network/networkd-manager-varlink.c @@ -279,9 +279,9 @@ static int vl_method_set_persistent_storage(sd_varlink *vlink, sd_json_variant * return sd_varlink_reply(vlink, NULL); } -int manager_connect_varlink(Manager *m, int fd) { +int manager_varlink_init(Manager *m, int fd) { _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL; - _unused_ _cleanup_close_ int fd_close = fd; + _unused_ _cleanup_close_ int fd_close = fd; /* take possession */ int r; assert(m); diff --git a/src/network/networkd-manager-varlink.h b/src/network/networkd-manager-varlink.h index 0933c2a056..edbc76859f 100644 --- a/src/network/networkd-manager-varlink.h +++ b/src/network/networkd-manager-varlink.h @@ -3,4 +3,4 @@ #include "networkd-forward.h" -int manager_connect_varlink(Manager *m, int fd); +int manager_varlink_init(Manager *m, int fd); diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c index 762f1ca190..fc0a435554 100644 --- a/src/network/networkd-manager.c +++ b/src/network/networkd-manager.c @@ -595,7 +595,7 @@ int manager_setup(Manager *m) { if (m->test_mode) return 0; - r = manager_connect_varlink(m, TAKE_FD(varlink_fd)); + r = manager_varlink_init(m, TAKE_FD(varlink_fd)); if (r < 0) return r; From ec9ec7fa3aa7dee60175eb5fc4d248c670d143ab Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Oct 2025 11:42:25 +0200 Subject: [PATCH 08/11] network: make use of LocalLeaseDomain= by default Let's expose local VMs/containers under ._dhcp by default. Let's also expose WIFI AP clients under .home.arpa (i.e. the RFC8375 domain for home networks). --- network/80-container-ve.network | 1 + network/80-container-vz.network | 1 + network/80-namespace-ns-tun.network | 1 + network/80-namespace-ns.network | 1 + network/80-vm-vt.network | 1 + network/80-wifi-ap.network.example | 3 +++ 6 files changed, 8 insertions(+) diff --git a/network/80-container-ve.network b/network/80-container-ve.network index a70a24cbcb..7d27b46920 100644 --- a/network/80-container-ve.network +++ b/network/80-container-ve.network @@ -30,3 +30,4 @@ IPv6SendRA=yes [DHCPServer] PersistLeases=runtime +LocalLeaseDomain=_dhcp diff --git a/network/80-container-vz.network b/network/80-container-vz.network index 09eadf48db..95c68943a1 100644 --- a/network/80-container-vz.network +++ b/network/80-container-vz.network @@ -29,3 +29,4 @@ IPv6SendRA=yes [DHCPServer] PersistLeases=runtime +LocalLeaseDomain=_dhcp diff --git a/network/80-namespace-ns-tun.network b/network/80-namespace-ns-tun.network index 658eac88d8..091c3861b1 100644 --- a/network/80-namespace-ns-tun.network +++ b/network/80-namespace-ns-tun.network @@ -30,3 +30,4 @@ IPv6SendRA=yes [DHCPServer] PersistLeases=runtime +LocalLeaseDomain=_dhcp diff --git a/network/80-namespace-ns.network b/network/80-namespace-ns.network index 2fb422a8bb..a8057be08f 100644 --- a/network/80-namespace-ns.network +++ b/network/80-namespace-ns.network @@ -30,3 +30,4 @@ IPv6SendRA=yes [DHCPServer] PersistLeases=runtime +LocalLeaseDomain=_dhcp diff --git a/network/80-vm-vt.network b/network/80-vm-vt.network index cb96806439..f0916ca798 100644 --- a/network/80-vm-vt.network +++ b/network/80-vm-vt.network @@ -29,3 +29,4 @@ IPv6SendRA=yes [DHCPServer] PersistLeases=runtime +LocalLeaseDomain=_dhcp diff --git a/network/80-wifi-ap.network.example b/network/80-wifi-ap.network.example index 09de9192d7..1fe4ea961c 100644 --- a/network/80-wifi-ap.network.example +++ b/network/80-wifi-ap.network.example @@ -19,3 +19,6 @@ DHCPServer=yes IPMasquerade=both IPv6AcceptRA=no IPv6SendRA=yes + +[DHCPServer] +LocalLeaseDomain=home.arpa From 5ba49cc834d4eb8bfe6a58ef7308e407fce36daf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 Oct 2025 15:56:25 +0200 Subject: [PATCH 09/11] units: let's set a socket name for networkd rtnl socket Let's make our networkd sockets recognizable purely by name. It hink already for debugging it's a good idea to always set socket names, in particular for services that have multiple sockets they listen on. This adds a name to the rtnl socket, which so far missed one. Note that the C code won't look for it, for compat with older versions, but at least things are a bit more debuggable. --- units/systemd-networkd.socket | 1 + 1 file changed, 1 insertion(+) diff --git a/units/systemd-networkd.socket b/units/systemd-networkd.socket index f594d77940..9d078f9382 100644 --- a/units/systemd-networkd.socket +++ b/units/systemd-networkd.socket @@ -18,6 +18,7 @@ Conflicts=shutdown.target [Socket] ReceiveBuffer=128M ListenNetlink=route 1361 +FileDescriptorName=rtnl PassPacketInfo=yes [Install] From 6850f7f5101aeecda3031cca9bb72bee5697f1d4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Oct 2025 13:07:15 +0200 Subject: [PATCH 10/11] update TODO --- TODO | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/TODO b/TODO index e1e5d54dce..bea5148b73 100644 --- a/TODO +++ b/TODO @@ -134,6 +134,14 @@ Deprecations and removals: Features: +* networkd/machined: implement reverse name lookups in the resolved hook + +* networkd's resolved hook: optionally map all lease IP addresses handed out to + the same hostname which is configured on the .network file. Optionally, even + derive this single name from the network interface name (i.e. probably + altname or so). This way, when spawning a VM the host could pick the hostname + for it and the client gets no say. + * systemd-repart: add --ghost, that creates file systems, updates the kernel's partition table but does *not* update partition table on disk. This way, we have disk backed file systems that go effectively disappear on reboot. This @@ -230,12 +238,6 @@ Features: so that we add correct ordering between the two. Use MSG_PEEK + SCM_TIMESTAMP for this. -* maybe replace nss-machines with logic in networkd that registers records with - systemd-resolved, based on DHCP leases, so that we gain compat with VMs. - Implementation idea: encode in an ifaltname the intended local name to expose this - under and then parse that out and map it to the combined A/AAAA of all handed - out leases. - * bsod: add target "bsod.target" or so, which invokes systemd-bsod.target and waits and then reboots. Then use OnFailure=bsod.target from various jobs that should result in system reboots, such as TPM tamper detection cases. @@ -2138,7 +2140,6 @@ Features: names, so that for the container case we can establish the same name (maybe "host") for referencing the server, everywhere. - allow clients to request DNSSEC for a single lookup even if DNSSEC is off (?) - - hook up resolved with machined-based address resolution * refcounting in sd-resolve is borked From a719802ca3842e65e8f8fa77ecaba13c4601faf2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Nov 2025 17:46:34 +0100 Subject: [PATCH 11/11] test: add workaround for networkd downgrade issue --- test/units/TEST-88-UPGRADE.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/units/TEST-88-UPGRADE.sh b/test/units/TEST-88-UPGRADE.sh index 603efea980..8bff38d247 100755 --- a/test/units/TEST-88-UPGRADE.sh +++ b/test/units/TEST-88-UPGRADE.sh @@ -83,6 +83,9 @@ timer2=$(systemctl show -P NextElapseUSecRealtime upgrade_timer_test.timer) dnf downgrade -y --allowerasing --disablerepo '*' "$pkgdir"/distro/*.rpm +# FIXME: See https://github.com/systemd/systemd/pull/39293 +systemctl stop systemd-networkd-resolve-hook.socket || true + # Some distros don't ship networkd, so the test will always fail if command -v networkctl >/dev/null; then networkd=1