1
0
mirror of https://github.com/systemd/systemd synced 2026-03-31 04:04:54 +02:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Lennart Poettering
c15bd80eb9
resolved: add ability for external programs to hook into name resolution (for machined+networkd to synthesize records) (#39293)
Fixes: #8518
2025-11-15 12:12:37 +01:00
Lennart Poettering
a719802ca3 test: add workaround for networkd downgrade issue 2025-11-15 07:52:42 +01:00
Lennart Poettering
6850f7f510 update TODO 2025-11-15 07:52:42 +01:00
Lennart Poettering
5ba49cc834 units: let's set a socket name for networkd rtnl socket
Let's make our networkd sockets recognizable purely by name. It hink
already for debugging it's a good idea to always set socket names, in
particular for services that have multiple sockets they listen on.

This adds a name to the rtnl socket, which so far missed one. Note that
the C code won't look for it, for compat with older versions, but at
least things are a bit more debuggable.
2025-11-15 07:52:42 +01:00
Lennart Poettering
ec9ec7fa3a network: make use of LocalLeaseDomain= by default
Let's expose local VMs/containers under ._dhcp by default. Let's also
expose WIFI AP clients under .home.arpa (i.e. the RFC8375 domain for
home networks).
2025-11-15 07:52:42 +01:00
Lennart Poettering
5615ac1f0d networkd: rename manager_connect_varlink() → manager_varlink_init()
This function doesn't "connect" to Varlink (i.e. it isn't a client) but
it binds a Varlink socket (i.e. it is server), hence let's remove the
verb "connect" from its name. let's copy how machined/resolved name the
counterpart for this function: manager_varlink_init()
2025-11-15 07:52:42 +01:00
Lennart Poettering
a7fa29b1b5 networkd: add support for resolved hook for DHCP server
Let's synthesize DNS RRs for leases handed out by our DHCP server. This
way local VMs can have resolvable hostnames locally.

This does not implement reverse look ups for now. We can add this
later in similar fashion.
2025-11-15 07:52:42 +01:00
Luca Boccassi
f041d40dee test: always create networkd mock tmpfs for networkd-test.py
Match the behaviour of the other test classes that use sd-run and
always create the mock tmpfs runtime dirs.
This will be needed as the new resolve.hook directory won't exist
on boot but will be needed by the test case.
2025-11-15 07:48:29 +01:00
Lennart Poettering
4408db6908 sd-dhcp-server: add api to get address from hostname, based on lease data 2025-11-15 07:44:24 +01:00
Lennart Poettering
3cd929f837 machined: implement resolve hook in machined
This basically implements nss-myhostname, but natively in
systemd-resolved, so that the logic becomes available also for clients
using the local DNS stub for resolution or the D-Bus or Varlink APIs.
2025-11-15 07:44:24 +01:00
Lennart Poettering
594352611b machine: minor refactoring, making machine_send_signal() invocations more readable 2025-11-15 07:44:24 +01:00
Lennart Poettering
8209f4adcd resolved: add hook api
This introduces /run/systemd/resolve.hook/ as a new directory that local
(privileged) programs can bind a Varlink socket into. If they do they'll
get a method call for each attempted resolved lookup, which they can
then either process themselves (and generate new records for, or return
errors to block stuff) or let pass so that the regular resolution is
done.

Usecase for this is primarily two things:

1. in machined we can add local resolution of machine names to their IP
   addresses, similar in fashion to nss-mymachines, but working also if
   the non-NSS interfaces to name resolution are used, i.e. the local
   DNS responder. In fact, I think we should eventually remove
   nss-mymachines from our tree, as soon as this code in resolved is
   setlled.

2. in networkd we can add local resolution of names specified in DHCP
   leases we hand out.

But beyond that there should be many other uses, for example people
could write "dns firewalls" with this if they like where they
dynamically block certain names from resolution.

Fixes: #8518
2025-11-15 07:44:24 +01:00
68 changed files with 2012 additions and 55 deletions

15
TODO
View File

@ -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

View File

@ -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)
</programlisting>
<para>On input, the first five flags control the protocols to use for the look-up. They refer to

View File

@ -4201,6 +4201,23 @@ ServerAddress=192.168.0.1/24</programlisting>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>LocalLeaseDomain=</varname></term>
<listitem>
<para>Takes a DNS domain name as argument. If specified,
<citerefentry><refentrytitle>systemd-networkd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
will integrate with
<citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
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.</para>
<para>Note that this purely about hostname resolution on the local system, i.e. from programs with
access to the same <filename>systemd-resolved</filename> instances via D-Bus IPC, Varlink IPC, or
the local DNS stub.</para>
<xi:include href="version-info.xml" xpointer="v259"/>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@ -30,3 +30,4 @@ IPv6SendRA=yes
[DHCPServer]
PersistLeases=runtime
LocalLeaseDomain=_dhcp

View File

@ -29,3 +29,4 @@ IPv6SendRA=yes
[DHCPServer]
PersistLeases=runtime
LocalLeaseDomain=_dhcp

View File

@ -30,3 +30,4 @@ IPv6SendRA=yes
[DHCPServer]
PersistLeases=runtime
LocalLeaseDomain=_dhcp

View File

@ -30,3 +30,4 @@ IPv6SendRA=yes
[DHCPServer]
PersistLeases=runtime
LocalLeaseDomain=_dhcp

View File

@ -29,3 +29,4 @@ IPv6SendRA=yes
[DHCPServer]
PersistLeases=runtime
LocalLeaseDomain=_dhcp

View File

@ -19,3 +19,6 @@ DHCPServer=yes
IPMasquerade=both
IPv6AcceptRA=no
IPv6SendRA=yes
[DHCPServer]
LocalLeaseDomain=home.arpa

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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));
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -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',
)

View File

@ -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',

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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);

View File

@ -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',

View File

@ -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());
}

View File

@ -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:

View File

@ -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;

View File

@ -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);

View File

@ -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;

881
src/resolve/resolved-hook.c Normal file
View File

@ -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 */
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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 */

View File

@ -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:

View File

@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <stdlib.h>
#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);

View File

@ -2,6 +2,8 @@
#include <stdio.h>
#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;
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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) \

View File

@ -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;
}

View File

@ -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);

View File

@ -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',

View File

@ -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 },
{}
};

View File

@ -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[];

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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])
'/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])

View File

@ -0,0 +1,10 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
[Match]
Name=veth99
[Network]
DHCP=yes
[DHCPv4]
SendHostname=yes
Hostname=flummy

View File

@ -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

View File

@ -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):

View File

@ -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}

View File

@ -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

View File

@ -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'],

View File

@ -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

View File

@ -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

View File

@ -18,6 +18,7 @@ Conflicts=shutdown.target
[Socket]
ReceiveBuffer=128M
ListenNetlink=route 1361
FileDescriptorName=rtnl
PassPacketInfo=yes
[Install]