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 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/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/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 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/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..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" @@ -672,7 +673,9 @@ 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"); + + (void) manager_notify_hook_filters(m->manager); return 0; } @@ -730,8 +733,10 @@ 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; + + (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/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..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); @@ -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..edbc76859f 100644 --- a/src/network/networkd-manager-varlink.h +++ b/src/network/networkd-manager-varlink.h @@ -3,5 +3,4 @@ #include "networkd-forward.h" -int manager_connect_varlink(Manager *m, int fd); -void manager_varlink_done(Manager *m); +int manager_varlink_init(Manager *m, int fd); diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c index 4004be81c0..fc0a435554 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; @@ -586,7 +595,11 @@ 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; + + r = manager_varlink_init_resolve_hook(m, TAKE_FD(resolve_hook_fd)); 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/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/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 bc927e4cca..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', @@ -217,6 +218,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/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/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/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; 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, diff --git a/test/networkd-test.py b/test/networkd-test.py index c822511974..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,13 +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']: - if os.path.isdir(d): - subprocess.check_call(["mount", "-t", "tmpfs", "none", d]) - tmpmounts.append(d) + '/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') @@ -279,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']) @@ -374,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) @@ -931,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 @@ -967,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 @@ -983,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/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} 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 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 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]