From: Nick Rosbrook Date: Thu, 19 Sep 2024 19:59:50 +0000 (-0400) Subject: wait-online: add support for waiting for DNS configuration X-Git-Tag: v258-rc1~1462^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=359db7e9b9908a89dd7d94170f0bafdc3fe11fc7;p=thirdparty%2Fsystemd.git wait-online: add support for waiting for DNS configuration Add a new flag to systemd-networkd-wait-online, --dns, to allow waiting for DNS to be configured. DNS is considered configured when at least one DNS server is accessible. If a link has the property DefaultRoute=yes (either by explicit configuration, or because there are no routing-only domains), or if the search domain '.' is configured, wait for link-specific DNS to be configured. Otherwise, global DNS servers may be considered. --- diff --git a/man/systemd-networkd-wait-online.service.xml b/man/systemd-networkd-wait-online.service.xml index e696a2ba9a1..2daeabfe7b1 100644 --- a/man/systemd-networkd-wait-online.service.xml +++ b/man/systemd-networkd-wait-online.service.xml @@ -144,6 +144,22 @@ + + + + Waiting for DNS servers to be accessible on each interface. If this + option is specified with , then + systemd-networkd-wait-online exits with success when at least one interface + becomes online and has an accessible DNS server. + If a link has the property DefaultRoute=yes (either because the + DNSDefaultRoute= network property is explicitly configured, or + because the link does not have any "routing-only" domains), or if the search domain "." is + configured, then wait for link-specific DNS servers to be accessible. Otherwise, allow global + DNS servers to satisfy the condition. + + + + diff --git a/src/network/meson.build b/src/network/meson.build index 295d015e7c4..5bcce39738c 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -114,6 +114,7 @@ sources = files( systemd_networkd_sources = files('networkd.c') systemd_networkd_wait_online_sources = files( + 'wait-online/dns-configuration.c', 'wait-online/link.c', 'wait-online/manager.c', 'wait-online/wait-online.c', diff --git a/src/network/wait-online/dns-configuration.c b/src/network/wait-online/dns-configuration.c new file mode 100644 index 00000000000..c369cc115a9 --- /dev/null +++ b/src/network/wait-online/dns-configuration.c @@ -0,0 +1,233 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-json.h" + +#include "af-list.h" +#include "alloc-util.h" +#include "dns-configuration.h" +#include "hash-funcs.h" +#include "in-addr-util.h" +#include "iovec-util.h" +#include "json-util.h" +#include "set.h" +#include "strv.h" + +DNSServer* dns_server_free(DNSServer *s) { + if (!s) + return NULL; + + free(s->server_name); + iovec_done(&s->addr); + + return mfree(s); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + dns_server_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + DNSServer, + dns_server_free); + +static int dispatch_dns_server(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dns_server_dispatch_table[] = { + { "address", SD_JSON_VARIANT_ARRAY, json_dispatch_byte_array_iovec, offsetof(DNSServer, addr), SD_JSON_MANDATORY }, + { "family", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(DNSServer, family), SD_JSON_MANDATORY }, + { "port", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint16, offsetof(DNSServer, port), 0 }, + { "ifindex", SD_JSON_VARIANT_UNSIGNED, json_dispatch_ifindex, offsetof(DNSServer, ifindex), SD_JSON_RELAX }, + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(DNSServer, server_name), 0 }, + { "accessible", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DNSServer, accessible), SD_JSON_MANDATORY }, + {}, + }; + DNSServer **ret = ASSERT_PTR(userdata); + _cleanup_(dns_server_freep) DNSServer *s = NULL; + int r; + + s = new0(DNSServer, 1); + if (!s) + return log_oom(); + + r = sd_json_dispatch(variant, dns_server_dispatch_table, flags, s); + if (r < 0) + return r; + + if (s->addr.iov_len != FAMILY_ADDRESS_SIZE_SAFE(s->family)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), + "Dispatched address size (%zu) is incompatible with the family (%s).", + s->addr.iov_len, af_to_ipv4_ipv6(s->family)); + + *ret = TAKE_PTR(s); + + return 0; +} + +static int dispatch_dns_server_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + Set **ret = ASSERT_PTR(userdata); + _cleanup_set_free_ Set *dns_servers = NULL; + sd_json_variant *v; + int r; + + JSON_VARIANT_ARRAY_FOREACH(v, variant) { + _cleanup_(dns_server_freep) DNSServer *s = NULL; + + r = dispatch_dns_server(name, v, flags, &s); + if (r < 0) + return json_log(v, flags, r, "JSON array element is not a valid DNSServer."); + + r = set_ensure_consume(&dns_servers, &dns_server_hash_ops, TAKE_PTR(s)); + if (r < 0) + return r; + } + + set_free_and_replace(*ret, dns_servers); + + return 0; +} + +SearchDomain* search_domain_free(SearchDomain *d) { + if (!d) + return NULL; + + free(d->name); + + return mfree(d); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + search_domain_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + SearchDomain, + search_domain_free); + +static int dispatch_search_domain(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + static const sd_json_dispatch_field search_domain_dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(SearchDomain, name), SD_JSON_MANDATORY }, + { "routeOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(SearchDomain, route_only), SD_JSON_MANDATORY }, + { "ifindex", SD_JSON_VARIANT_UNSIGNED, json_dispatch_ifindex, offsetof(SearchDomain, ifindex), SD_JSON_RELAX }, + {}, + }; + SearchDomain **ret = ASSERT_PTR(userdata); + _cleanup_(search_domain_freep) SearchDomain *d = NULL; + int r; + + d = new0(SearchDomain, 1); + if (!d) + return log_oom(); + + r = sd_json_dispatch(variant, search_domain_dispatch_table, flags, d); + if (r < 0) + return r; + + *ret = TAKE_PTR(d); + + return 0; +} + +static int dispatch_search_domain_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + Set **ret = ASSERT_PTR(userdata); + _cleanup_set_free_ Set *search_domains = NULL; + sd_json_variant *v; + int r; + + JSON_VARIANT_ARRAY_FOREACH(v, variant) { + _cleanup_(search_domain_freep) SearchDomain *d = NULL; + + r = dispatch_search_domain(name, v, flags, &d); + if (r < 0) + return json_log(v, flags, r, "JSON array element is not a valid SearchDomain."); + + r = set_ensure_consume(&search_domains, &search_domain_hash_ops, TAKE_PTR(d)); + if (r < 0) + return r; + } + + set_free_and_replace(*ret, search_domains); + + return 0; +} + +DNSConfiguration* dns_configuration_free(DNSConfiguration *c) { + if (!c) + return NULL; + + dns_server_free(c->current_dns_server); + set_free(c->dns_servers); + set_free(c->search_domains); + free(c->ifname); + + return mfree(c); +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + dns_configuration_hash_ops, + void, + trivial_hash_func, + trivial_compare_func, + DNSConfiguration, + dns_configuration_free); + +static int dispatch_dns_configuration(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dns_configuration_dispatch_table[] = { + { "ifname", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(DNSConfiguration, ifname), 0 }, + { "ifindex", SD_JSON_VARIANT_UNSIGNED, json_dispatch_ifindex, offsetof(DNSConfiguration, ifindex), SD_JSON_RELAX }, + { "defaultRoute", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(DNSConfiguration, default_route), 0 }, + { "currentServer", SD_JSON_VARIANT_OBJECT, dispatch_dns_server, offsetof(DNSConfiguration, current_dns_server), 0 }, + { "servers", SD_JSON_VARIANT_ARRAY, dispatch_dns_server_array, offsetof(DNSConfiguration, dns_servers), 0 }, + { "searchDomains", SD_JSON_VARIANT_ARRAY, dispatch_search_domain_array, offsetof(DNSConfiguration, search_domains), 0 }, + {}, + + }; + DNSConfiguration **ret = ASSERT_PTR(userdata); + _cleanup_(dns_configuration_freep) DNSConfiguration *c = NULL; + int r; + + c = new0(DNSConfiguration, 1); + if (!c) + return log_oom(); + + r = sd_json_dispatch(variant, dns_configuration_dispatch_table, flags, c); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + + return 0; +} + +int dns_configuration_from_json(sd_json_variant *variant, DNSConfiguration **ret) { + return dispatch_dns_configuration(NULL, variant, SD_JSON_LOG, ret); +} + +bool dns_is_accessible(DNSConfiguration *c) { + DNSServer *s = NULL; + + if (!c) + return false; + + if (c->current_dns_server && c->current_dns_server->accessible) + return true; + + SET_FOREACH(s, c->dns_servers) + if (s->accessible) + return true; + + return false; +} + +bool dns_configuration_contains_search_domain(DNSConfiguration *c, const char *domain) { + SearchDomain *d = NULL; + + assert(domain); + + if (!c) + return false; + + SET_FOREACH(d, c->search_domains) + if (streq(d->name, domain)) + return true; + + return false; +} diff --git a/src/network/wait-online/dns-configuration.h b/src/network/wait-online/dns-configuration.h new file mode 100644 index 00000000000..6e6890d9250 --- /dev/null +++ b/src/network/wait-online/dns-configuration.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-json.h" + +#include "hash-funcs.h" +#include "iovec-util.h" +#include "macro-fundamental.h" +#include "set.h" + +typedef struct DNSServer DNSServer; +typedef struct SearchDomain SearchDomain; +typedef struct DNSConfiguration DNSConfiguration; + +struct DNSServer { + struct iovec addr; + int family; + uint16_t port; + int ifindex; + char *server_name; + bool accessible; +}; + +DNSServer* dns_server_free(DNSServer *s); +DEFINE_TRIVIAL_CLEANUP_FUNC(DNSServer*, dns_server_free); + +struct SearchDomain { + char *name; + bool route_only; + int ifindex; +}; + +SearchDomain* search_domain_free(SearchDomain *d); +DEFINE_TRIVIAL_CLEANUP_FUNC(SearchDomain*, search_domain_free); + +struct DNSConfiguration { + char *ifname; + int ifindex; + bool default_route; + DNSServer *current_dns_server; + Set *dns_servers; + Set *search_domains; +}; + +DNSConfiguration* dns_configuration_free(DNSConfiguration *c); +DEFINE_TRIVIAL_CLEANUP_FUNC(DNSConfiguration*, dns_configuration_free); + +int dns_configuration_from_json(sd_json_variant *variant, DNSConfiguration **ret); +bool dns_is_accessible(DNSConfiguration *c); +bool dns_configuration_contains_search_domain(DNSConfiguration *c, const char *domain); + +extern const struct hash_ops dns_configuration_hash_ops; diff --git a/src/network/wait-online/link.c b/src/network/wait-online/link.c index 3097611d98b..a4a64462ffd 100644 --- a/src/network/wait-online/link.c +++ b/src/network/wait-online/link.c @@ -3,6 +3,7 @@ #include "sd-network.h" #include "alloc-util.h" +#include "dns-configuration.h" #include "format-ifname.h" #include "hashmap.h" #include "link.h" @@ -32,6 +33,7 @@ int link_new(Manager *m, Link **ret, int ifindex, const char *ifname) { .ifname = TAKE_PTR(n), .ifindex = ifindex, .required_operstate = LINK_OPERSTATE_RANGE_DEFAULT, + .dns_configuration = hashmap_remove(m->dns_configuration_by_link_index, INT_TO_PTR(ifindex)), }; r = hashmap_ensure_put(&m->links_by_index, NULL, INT_TO_PTR(ifindex), l); @@ -62,6 +64,8 @@ Link *link_free(Link *l) { hashmap_remove(l->manager->links_by_name, *n); } + dns_configuration_free(l->dns_configuration); + free(l->state); free(l->ifname); strv_free(l->altnames); diff --git a/src/network/wait-online/link.h b/src/network/wait-online/link.h index 5dc26d9c40a..ec352c41132 100644 --- a/src/network/wait-online/link.h +++ b/src/network/wait-online/link.h @@ -3,6 +3,7 @@ #include "sd-netlink.h" +#include "dns-configuration.h" #include "log-link.h" #include "network-util.h" @@ -24,6 +25,7 @@ struct Link { LinkAddressState ipv4_address_state; LinkAddressState ipv6_address_state; char *state; + DNSConfiguration *dns_configuration; }; int link_new(Manager *m, Link **ret, int ifindex, const char *ifname); diff --git a/src/network/wait-online/manager.c b/src/network/wait-online/manager.c index 47f5df6a5f6..4bad0f97e2c 100644 --- a/src/network/wait-online/manager.c +++ b/src/network/wait-online/manager.c @@ -4,7 +4,13 @@ #include #include +#include "sd-event.h" +#include "sd-json.h" +#include "sd-varlink.h" + #include "alloc-util.h" +#include "dns-configuration.h" +#include "json-util.h" #include "link.h" #include "manager.h" #include "netlink-util.h" @@ -133,6 +139,26 @@ static int manager_link_is_online(Manager *m, Link *l, const LinkOperationalStat "No routable IPv6 address is configured."); } + if (m->requires_dns) { + if (!l->dns_configuration) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No DNS configuration yet"); + + /* If a link is configured with DNSDefaultRoute=yes, or is configured with the + * search domain '.', then require link-specific DNS servers to be available. + * Otherwise, we check the global DNS configuration. */ + if (l->dns_configuration->default_route || + dns_configuration_contains_search_domain(l->dns_configuration, ".")) { + + if (!dns_is_accessible(l->dns_configuration)) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No link-specific DNS server is accessible."); + + } else if (!dns_is_accessible(m->dns_configuration)) + return log_link_debug_errno(l, SYNTHETIC_ERRNO(EADDRNOTAVAIL), + "No DNS server is accessible."); + } + log_link_debug(l, "link is configured by networkd and online."); return true; } @@ -381,13 +407,117 @@ static int manager_network_monitor_listen(Manager *m) { return 0; } +static int on_dns_configuration_event( + sd_varlink *link, + sd_json_variant *parameters, + const char *error_id, + sd_varlink_reply_flags_t flags, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + sd_json_variant *configurations = NULL, *v = NULL; + int r; + + assert(link); + + if (error_id) { + log_warning("DNS configuration event error, ignoring: %s", error_id); + return 0; + } + + configurations = sd_json_variant_by_key(parameters, "configuration"); + if (!sd_json_variant_is_array(configurations)) { + log_warning("DNS configuration JSON data does not have configuration key, ignoring."); + return 0; + } + + /* Clear any existing link DNS configuration saved by the manager. */ + hashmap_clear(m->dns_configuration_by_link_index); + + JSON_VARIANT_ARRAY_FOREACH(v, configurations) { + _cleanup_(dns_configuration_freep) DNSConfiguration *c = NULL; + + r = dns_configuration_from_json(v, &c); + if (r < 0) { + log_warning_errno(r, "Failed to get DNS configuration JSON, ignoring: %m"); + continue; + } + + if (c->ifindex > 0) { + Link *l = hashmap_get(m->links_by_index, INT_TO_PTR(c->ifindex)); + if (l) + free_and_replace_full(l->dns_configuration, c, dns_configuration_free); + else { + r = hashmap_ensure_put( + &m->dns_configuration_by_link_index, + &dns_configuration_hash_ops, + INT_TO_PTR(c->ifindex), + c); + if (r < 0) { + log_warning_errno(r, "Failed to save DNS configuration for link %i, ignoring: %m", c->ifindex); + continue; + } + TAKE_PTR(c); + } + } else + /* Global DNS configuration */ + free_and_replace_full(m->dns_configuration, c, dns_configuration_free); + } + + if (manager_configured(m)) + sd_event_exit(m->event, 0); + + return 0; +} + +static int manager_dns_configuration_listen(Manager *m) { + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + int r; + + assert(m); + assert(m->event); + + if (!m->requires_dns) + return 0; + + r = sd_varlink_connect_address(&vl, "/run/systemd/resolve/io.systemd.Resolve.Monitor"); + if (r < 0) + return log_error_errno(r, "Failed to connect to io.systemd.Resolve.Monitor: %m"); + + r = sd_varlink_set_relative_timeout(vl, USEC_INFINITY); + if (r < 0) + return log_error_errno(r, "Failed to set varlink timeout: %m"); + + r = sd_varlink_attach_event(vl, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + (void) sd_varlink_set_userdata(vl, m); + + r = sd_varlink_bind_reply(vl, on_dns_configuration_event); + if (r < 0) + return log_error_errno(r, "Failed to bind varlink reply callback: %m"); + + r = sd_varlink_observebo( + vl, + "io.systemd.Resolve.Monitor.SubscribeDNSConfiguration", + SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", false)); + if (r < 0) + return log_error_errno(r, "Failed to issue SubscribeDNSConfiguration: %m"); + + m->varlink_client = TAKE_PTR(vl); + + return 0; +} + int manager_new(Manager **ret, Hashmap *command_line_interfaces_by_name, char **ignored_interfaces, LinkOperationalStateRange required_operstate, AddressFamily required_family, bool any, - usec_t timeout) { + usec_t timeout, + bool requires_dns) { _cleanup_(manager_freep) Manager *m = NULL; int r; @@ -404,6 +534,7 @@ int manager_new(Manager **ret, .required_operstate = required_operstate, .required_family = required_family, .any = any, + .requires_dns = requires_dns, }; r = sd_event_default(&m->event); @@ -428,6 +559,10 @@ int manager_new(Manager **ret, if (r < 0) return r; + r = manager_dns_configuration_listen(m); + if (r < 0) + return r; + *ret = TAKE_PTR(m); return 0; @@ -445,6 +580,10 @@ Manager* manager_free(Manager *m) { sd_event_source_unref(m->rtnl_event_source); sd_netlink_unref(m->rtnl); sd_event_unref(m->event); + sd_varlink_unref(m->varlink_client); + + dns_configuration_free(m->dns_configuration); + hashmap_free(m->dns_configuration_by_link_index); return mfree(m); } diff --git a/src/network/wait-online/manager.h b/src/network/wait-online/manager.h index 01ad18f8f62..e5ff8c21ba8 100644 --- a/src/network/wait-online/manager.h +++ b/src/network/wait-online/manager.h @@ -4,7 +4,9 @@ #include "sd-event.h" #include "sd-netlink.h" #include "sd-network.h" +#include "sd-varlink.h" +#include "dns-configuration.h" #include "hashmap.h" #include "network-util.h" #include "time-util.h" @@ -23,6 +25,7 @@ struct Manager { LinkOperationalStateRange required_operstate; AddressFamily required_family; bool any; + bool requires_dns; sd_netlink *rtnl; sd_event_source *rtnl_event_source; @@ -31,13 +34,17 @@ struct Manager { sd_event_source *network_monitor_event_source; sd_event *event; + + sd_varlink *varlink_client; + DNSConfiguration *dns_configuration; + Hashmap *dns_configuration_by_link_index; }; Manager* manager_free(Manager *m); int manager_new(Manager **ret, Hashmap *command_line_interfaces_by_name, char **ignored_interfaces, LinkOperationalStateRange required_operstate, AddressFamily required_family, - bool any, usec_t timeout); + bool any, usec_t timeout, bool requires_dns); DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); diff --git a/src/network/wait-online/wait-online.c b/src/network/wait-online/wait-online.c index 48c4d1c0ee0..467d2e7da75 100644 --- a/src/network/wait-online/wait-online.c +++ b/src/network/wait-online/wait-online.c @@ -10,6 +10,7 @@ #include "daemon-util.h" #include "main-func.h" #include "manager.h" +#include "parse-argument.h" #include "pretty-print.h" #include "signal-util.h" #include "socket-util.h" @@ -22,6 +23,7 @@ static char **arg_ignore = NULL; static LinkOperationalStateRange arg_required_operstate = LINK_OPERSTATE_RANGE_INVALID; static AddressFamily arg_required_family = ADDRESS_FAMILY_NO; static bool arg_any = false; +static bool arg_requires_dns = false; STATIC_DESTRUCTOR_REGISTER(arg_interfaces, hashmap_freep); STATIC_DESTRUCTOR_REGISTER(arg_ignore, strv_freep); @@ -48,6 +50,7 @@ static int help(void) { " -6 --ipv6 Requires at least one IPv6 address\n" " --any Wait until at least one of the interfaces is online\n" " --timeout=SECS Maximum time to wait for network connectivity\n" + " --dns Requires at least one DNS server to be accessible\n" "\nSee the %s for details.\n", program_invocation_short_name, link); @@ -106,6 +109,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_IGNORE, ARG_ANY, ARG_TIMEOUT, + ARG_DNS, }; static const struct option options[] = { @@ -119,6 +123,7 @@ static int parse_argv(int argc, char *argv[]) { { "ipv6", no_argument, NULL, '6' }, { "any", no_argument, NULL, ARG_ANY }, { "timeout", required_argument, NULL, ARG_TIMEOUT }, + { "dns", optional_argument, NULL, ARG_DNS }, {} }; @@ -178,6 +183,12 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_DNS: + r = parse_boolean_argument("--dns", optarg, &arg_requires_dns); + if (r < 0) + return r; + break; + case '?': return -EINVAL; @@ -204,7 +215,14 @@ static int run(int argc, char *argv[]) { if (arg_quiet) log_set_max_level(LOG_ERR); - r = manager_new(&m, arg_interfaces, arg_ignore, arg_required_operstate, arg_required_family, arg_any, arg_timeout); + r = manager_new(&m, + arg_interfaces, + arg_ignore, + arg_required_operstate, + arg_required_family, + arg_any, + arg_timeout, + arg_requires_dns); if (r < 0) return log_error_errno(r, "Could not create manager: %m");