From cf22b5c5ad6c879055ccbdc182897be1771cb676 Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Mon, 7 Oct 2024 11:39:57 -0400 Subject: [PATCH 1/9] resolve: rename varlink_subscription -> varlink_query_results_subscription No functional change. Make it more clear that these varlink connections are subscribed to query results. This prepares for adding SubscribeDNS to the varlink API. --- src/resolve/resolved-manager.c | 4 ++-- src/resolve/resolved-manager.h | 2 +- src/resolve/resolved-varlink.c | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index 5ec946717f..883bc09701 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -1190,7 +1190,7 @@ int manager_monitor_send(Manager *m, DnsQuery *q) { assert(m); - if (set_isempty(m->varlink_subscription)) + if (set_isempty(m->varlink_query_results_subscription)) return 0; /* Merge all questions into one */ @@ -1240,7 +1240,7 @@ int manager_monitor_send(Manager *m, DnsQuery *q) { } r = varlink_many_notifybo( - m->varlink_subscription, + m->varlink_query_results_subscription, SD_JSON_BUILD_PAIR("state", SD_JSON_BUILD_STRING(dns_transaction_state_to_string(q->state))), SD_JSON_BUILD_PAIR_CONDITION(q->state == DNS_TRANSACTION_DNSSEC_FAILED, "result", SD_JSON_BUILD_STRING(dnssec_result_to_string(q->answer_dnssec_result))), diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index 337aa2788c..5d870111d1 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -152,7 +152,7 @@ struct Manager { sd_varlink_server *varlink_server; sd_varlink_server *varlink_monitor_server; - Set *varlink_subscription; + Set *varlink_query_results_subscription; sd_event_source *clock_change_event_source; diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index c30a2a3fea..fd2fc58853 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -131,10 +131,10 @@ static void vl_on_notification_disconnect(sd_varlink_server *s, sd_varlink *link assert(s); assert(link); - sd_varlink *removed_link = set_remove(m->varlink_subscription, link); + sd_varlink *removed_link = set_remove(m->varlink_query_results_subscription, link); if (removed_link) { sd_varlink_unref(removed_link); - log_debug("%u monitor clients remain active", set_size(m->varlink_subscription)); + log_debug("%u query result monitor clients remain active", set_size(m->varlink_query_results_subscription)); } } @@ -1229,12 +1229,12 @@ static int vl_method_subscribe_query_results(sd_varlink *link, sd_json_variant * if (r < 0) return log_error_errno(r, "Failed to report monitor to be established: %m"); - r = set_ensure_put(&m->varlink_subscription, NULL, link); + r = set_ensure_put(&m->varlink_query_results_subscription, NULL, link); if (r < 0) return log_error_errno(r, "Failed to add subscription to set: %m"); sd_varlink_ref(link); - log_debug("%u clients now attached for varlink notifications", set_size(m->varlink_subscription)); + log_debug("%u clients now attached for query result varlink notifications", set_size(m->varlink_query_results_subscription)); return 1; } From 428c99997d19a132e231313ed09625ddcebbd38c Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Tue, 8 Oct 2024 10:19:42 -0400 Subject: [PATCH 2/9] varlink-util: add varlink_many_notify We already have varlink_many_notifyb. Just re-factor it slightly and add a plain varlink_many_notify. --- src/libsystemd/sd-varlink/varlink-util.c | 20 ++++++++++++++------ src/libsystemd/sd-varlink/varlink-util.h | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libsystemd/sd-varlink/varlink-util.c b/src/libsystemd/sd-varlink/varlink-util.c index 22f1a94f0b..899b8ea929 100644 --- a/src/libsystemd/sd-varlink/varlink-util.c +++ b/src/libsystemd/sd-varlink/varlink-util.c @@ -89,6 +89,19 @@ int varlink_callb_and_log( return varlink_call_and_log(v, method, parameters, ret_parameters); } +int varlink_many_notify(Set *s, sd_json_variant *parameters) { + sd_varlink *link; + int r = 1; + + if (set_isempty(s)) + return 0; + + SET_FOREACH(link, s) + RET_GATHER(r, sd_varlink_notify(link, parameters)); + + return r; +} + int varlink_many_notifyb(Set *s, ...) { int r; @@ -105,12 +118,7 @@ int varlink_many_notifyb(Set *s, ...) { if (r < 0) return r; - r = 1; - sd_varlink *link; - SET_FOREACH(link, s) - RET_GATHER(r, sd_varlink_notify(link, parameters)); - - return r; + return varlink_many_notify(s, parameters); } int varlink_many_reply(Set *s, sd_json_variant *parameters) { diff --git a/src/libsystemd/sd-varlink/varlink-util.h b/src/libsystemd/sd-varlink/varlink-util.h index 351f3951b0..124b16263e 100644 --- a/src/libsystemd/sd-varlink/varlink-util.h +++ b/src/libsystemd/sd-varlink/varlink-util.h @@ -13,6 +13,7 @@ int varlink_callb_and_log(sd_varlink *v, const char *method, sd_json_variant **r #define varlink_callbo_and_log(v, method, ret_parameters, ...) \ varlink_callb_and_log((v), (method), (ret_parameters), SD_JSON_BUILD_OBJECT(__VA_ARGS__)) +int varlink_many_notify(Set *s, sd_json_variant *parameters); int varlink_many_notifyb(Set *s, ...); #define varlink_many_notifybo(s, ...) \ varlink_many_notifyb((s), SD_JSON_BUILD_OBJECT(__VA_ARGS__)) From b9335a329db0ea7e2aa4aa9ca1853abb770cdb93 Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Tue, 15 Oct 2024 16:30:52 -0400 Subject: [PATCH 3/9] resolved: add a helper to check if DNS server is accessible We check this by opening a UDP socket and attempting to connect. We do not send any traffic on it, but this will tell us if there are routes to the DNS server. This will be used in a later commit. --- src/resolve/resolved-dns-server.c | 52 +++++++++++++++++++++++++++++++ src/resolve/resolved-dns-server.h | 9 ++++++ src/resolve/resolved-link.c | 3 ++ 3 files changed, 64 insertions(+) diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c index b48d8e1417..1702866a14 100644 --- a/src/resolve/resolved-dns-server.c +++ b/src/resolve/resolved-dns-server.c @@ -1,14 +1,19 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "sd-messages.h" #include "alloc-util.h" +#include "errno-util.h" +#include "fd-util.h" #include "resolved-bus.h" #include "resolved-dns-server.h" #include "resolved-dns-stub.h" #include "resolved-manager.h" #include "resolved-resolv-conf.h" #include "siphash24.h" +#include "socket-util.h" #include "string-table.h" #include "string-util.h" @@ -69,6 +74,7 @@ int dns_server_new( .ifindex = ifindex, .server_name = TAKE_PTR(name), .config_source = config_source, + .accessible = -1, }; dns_server_reset_features(s); @@ -1128,3 +1134,49 @@ int dns_server_dump_state_to_json(DnsServer *server, sd_json_variant **ret) { SD_JSON_BUILD_PAIR_BOOLEAN("PacketInvalid", server->packet_invalid), SD_JSON_BUILD_PAIR_BOOLEAN("PacketDoOff", server->packet_do_off)); } + +int dns_server_is_accessible(DnsServer *s) { + _cleanup_close_ int fd = -EBADF; + union sockaddr_union sa; + int r; + + assert(s); + + if (s->accessible >= 0) + return s->accessible; + + r = sockaddr_set_in_addr(&sa, s->family, &s->address, dns_server_port(s)); + if (r < 0) + return r; + + fd = socket(s->family, SOCK_DGRAM|SOCK_CLOEXEC, 0); + if (fd < 0) + return -errno; + + if (s->family == AF_INET6 && in_addr_is_link_local(AF_INET6, &s->address)) { + /* Connecting to ipv6 link-local requires binding to an interface. */ + r = socket_bind_to_ifindex(fd, dns_server_ifindex(s)); + if (r < 0) + return r; + } + + r = RET_NERRNO(connect(fd, &sa.sa, SOCKADDR_LEN(sa))); + if (!IN_SET(r, + 0, + -ENETUNREACH, + -EHOSTDOWN, + -EHOSTUNREACH, + -ENETDOWN, + -ENETRESET, + -ENONET)) + /* If we did not receive one of the expected return values, + * then leave the accessible flag untouched. */ + return r; + + return (s->accessible = r >= 0); +} + +void dns_server_reset_accessible_all(DnsServer *first) { + LIST_FOREACH(servers, s, first) + dns_server_reset_accessible(s); +} diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h index 7d7d294c82..8915b87e95 100644 --- a/src/resolve/resolved-dns-server.h +++ b/src/resolve/resolved-dns-server.h @@ -106,6 +106,9 @@ struct DnsServer { /* Servers registered via D-Bus are not removed on reload */ ResolveConfigSource config_source; + + /* Tri-state to indicate if the DNS server is accessible. */ + int accessible; }; int dns_server_new( @@ -187,3 +190,9 @@ static inline bool dns_server_is_fallback(DnsServer *s) { } int dns_server_dump_state_to_json(DnsServer *server, sd_json_variant **ret); + +int dns_server_is_accessible(DnsServer *s); +static inline void dns_server_reset_accessible(DnsServer *s) { + s->accessible = -1; +} +void dns_server_reset_accessible_all(DnsServer *first); diff --git a/src/resolve/resolved-link.c b/src/resolve/resolved-link.c index 928137b967..1c5ad0878c 100644 --- a/src/resolve/resolved-link.c +++ b/src/resolve/resolved-link.c @@ -299,6 +299,9 @@ static int link_update_dns_servers(Link *l) { } dns_server_unlink_marked(l->dns_servers); + + dns_server_reset_accessible_all(l->dns_servers); + return 0; clear: From f63acba4adad4e6f100a50ebc2873282ab9e8f4c Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Thu, 14 Nov 2024 14:31:07 -0500 Subject: [PATCH 4/9] resolved: add link_get_default_route helper The dbus property getter for DefaultRoute does not simply check link->default_route. Instead, if l->default_route is not explicitly configured, it checks dns_scope_is_default_route(l->unicast_scope). Add a link_get_default_route() helper with this logic so that it can be used for consistency. --- src/resolve/resolved-link-bus.c | 25 +------------------------ src/resolve/resolved-link.c | 15 +++++++++++++++ src/resolve/resolved-link.h | 2 ++ 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/resolve/resolved-link-bus.c b/src/resolve/resolved-link-bus.c index 1c26dcbc66..b0c4b833b5 100644 --- a/src/resolve/resolved-link-bus.c +++ b/src/resolve/resolved-link-bus.c @@ -26,6 +26,7 @@ static BUS_DEFINE_PROPERTY_GET(property_get_dnssec_supported, "b", Link, link_dn static BUS_DEFINE_PROPERTY_GET2(property_get_dnssec_mode, "s", Link, link_get_dnssec_mode, dnssec_mode_to_string); static BUS_DEFINE_PROPERTY_GET2(property_get_llmnr_support, "s", Link, link_get_llmnr_support, resolve_support_to_string); static BUS_DEFINE_PROPERTY_GET2(property_get_mdns_support, "s", Link, link_get_mdns_support, resolve_support_to_string); +static BUS_DEFINE_PROPERTY_GET(property_get_default_route, "b", Link, link_get_default_route); static int property_get_dns_over_tls_mode( sd_bus *bus, @@ -162,30 +163,6 @@ static int property_get_domains( return sd_bus_message_close_container(reply); } -static int property_get_default_route( - sd_bus *bus, - const char *path, - const char *interface, - const char *property, - sd_bus_message *reply, - void *userdata, - sd_bus_error *error) { - - Link *l = ASSERT_PTR(userdata); - - assert(reply); - - /* Return what is configured, if there's something configured */ - if (l->default_route >= 0) - return sd_bus_message_append(reply, "b", l->default_route); - - /* Otherwise report what is in effect */ - if (l->unicast_scope) - return sd_bus_message_append(reply, "b", dns_scope_is_default_route(l->unicast_scope)); - - return sd_bus_message_append(reply, "b", false); -} - static int property_get_scopes_mask( sd_bus *bus, const char *path, diff --git a/src/resolve/resolved-link.c b/src/resolve/resolved-link.c index 1c5ad0878c..df4b5993a3 100644 --- a/src/resolve/resolved-link.c +++ b/src/resolve/resolved-link.c @@ -14,6 +14,7 @@ #include "mkdir.h" #include "netif-util.h" #include "parse-util.h" +#include "resolved-dns-scope.h" #include "resolved-link.h" #include "resolved-llmnr.h" #include "resolved-mdns.h" @@ -834,6 +835,20 @@ ResolveSupport link_get_mdns_support(Link *link) { return MIN(link->mdns_support, link->manager->mdns_support); } +bool link_get_default_route(Link *l) { + assert(l); + + /* Return what is configured, if there's something configured */ + if (l->default_route >= 0) + return l->default_route; + + /* Otherwise report what is in effect */ + if (l->unicast_scope) + return dns_scope_is_default_route(l->unicast_scope); + + return false; +} + int link_address_new(Link *l, LinkAddress **ret, int family, diff --git a/src/resolve/resolved-link.h b/src/resolve/resolved-link.h index 482a4b4148..712a81d5ee 100644 --- a/src/resolve/resolved-link.h +++ b/src/resolve/resolved-link.h @@ -109,6 +109,8 @@ DnsOverTlsMode link_get_dns_over_tls_mode(Link *l); ResolveSupport link_get_llmnr_support(Link *link); ResolveSupport link_get_mdns_support(Link *link); +bool link_get_default_route(Link *l); + int link_save_user(Link *l); int link_load_user(Link *l); void link_remove_user(Link *l); From 54401c6fdf5385d158ea8b91949b8b5391e0da95 Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Fri, 11 Oct 2024 14:44:44 -0400 Subject: [PATCH 5/9] resolved: add SubscribeDNSConfiguration to varlink API Add a new method to io.systemd.Resolve.Monitor that allows subscribing to changes in the systemd-resolved DNS configuration. The new method emits the full DNS configuration (one entry for global configuration, and one entry for each interface), any time the configuration is updated. --- src/resolve/org.freedesktop.resolve1.policy | 11 + src/resolve/resolved-dns-search-domain.c | 20 ++ src/resolve/resolved-dns-search-domain.h | 4 + src/resolve/resolved-dns-server.c | 27 +++ src/resolve/resolved-dns-server.h | 1 + src/resolve/resolved-link-bus.c | 2 + src/resolve/resolved-manager.c | 223 ++++++++++++++++++ src/resolve/resolved-manager.h | 12 + src/resolve/resolved-varlink.c | 54 ++++- .../varlink-io.systemd.Resolve.Monitor.c | 56 ++++- 10 files changed, 407 insertions(+), 3 deletions(-) diff --git a/src/resolve/org.freedesktop.resolve1.policy b/src/resolve/org.freedesktop.resolve1.policy index b96c8c0d6a..097e78e73c 100644 --- a/src/resolve/org.freedesktop.resolve1.policy +++ b/src/resolve/org.freedesktop.resolve1.policy @@ -150,6 +150,17 @@ unix-user:systemd-resolve + + Subscribe to DNS configuration + Authentication is required to subscribe to DNS configuration. + + auth_admin + auth_admin + auth_admin_keep + + unix-user:systemd-resolve + + Dump cache Authentication is required to dump cache. diff --git a/src/resolve/resolved-dns-search-domain.c b/src/resolve/resolved-dns-search-domain.c index a11b21350a..642d52c6ea 100644 --- a/src/resolve/resolved-dns-search-domain.c +++ b/src/resolve/resolved-dns-search-domain.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-json.h" + #include "alloc-util.h" #include "dns-domain.h" #include "resolved-dns-search-domain.h" @@ -197,3 +199,21 @@ int dns_search_domain_find(DnsSearchDomain *first, const char *name, DnsSearchDo *ret = NULL; return 0; } + +int dns_search_domain_dump_to_json(DnsSearchDomain *domain, sd_json_variant **ret) { + int ifindex = 0; + + assert(domain); + assert(ret); + + if (domain->type == DNS_SEARCH_DOMAIN_LINK) { + assert(domain->link); + ifindex = domain->link->ifindex; + } + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("name", domain->name), + SD_JSON_BUILD_PAIR_BOOLEAN("routeOnly", domain->route_only), + SD_JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", SD_JSON_BUILD_UNSIGNED(ifindex))); +} diff --git a/src/resolve/resolved-dns-search-domain.h b/src/resolve/resolved-dns-search-domain.h index f0d96aca37..3e5229c12f 100644 --- a/src/resolve/resolved-dns-search-domain.h +++ b/src/resolve/resolved-dns-search-domain.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "sd-json.h" + #include "list.h" #include "macro.h" @@ -54,3 +56,5 @@ static inline const char* DNS_SEARCH_DOMAIN_NAME(DnsSearchDomain *d) { } DEFINE_TRIVIAL_CLEANUP_FUNC(DnsSearchDomain*, dns_search_domain_unref); + +int dns_search_domain_dump_to_json(DnsSearchDomain *domain, sd_json_variant **ret); diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c index 1702866a14..54d0d32ec5 100644 --- a/src/resolve/resolved-dns-server.c +++ b/src/resolve/resolved-dns-server.c @@ -7,6 +7,7 @@ #include "alloc-util.h" #include "errno-util.h" #include "fd-util.h" +#include "json-util.h" #include "resolved-bus.h" #include "resolved-dns-server.h" #include "resolved-dns-stub.h" @@ -881,6 +882,7 @@ DnsServer *manager_set_dns_server(Manager *m, DnsServer *s) { dns_cache_flush(&m->unicast_scope->cache); (void) manager_send_changed(m, "CurrentDNSServer"); + (void) manager_send_dns_configuration_changed(m, NULL, /* reset= */ false); return s; } @@ -1180,3 +1182,28 @@ void dns_server_reset_accessible_all(DnsServer *first) { LIST_FOREACH(servers, s, first) dns_server_reset_accessible(s); } + +int dns_server_dump_configuration_to_json(DnsServer *server, sd_json_variant **ret) { + bool accessible = false; + int ifindex, r; + + assert(server); + assert(ret); + + ifindex = dns_server_ifindex(server); + + r = dns_server_is_accessible(server); + if (r < 0) + log_debug_errno(r, "Failed to check if %s is accessible, assume not: %m", dns_server_string_full(server)); + else + accessible = r; + + return sd_json_buildo( + ret, + JSON_BUILD_PAIR_IN_ADDR("address", &server->address, server->family), + SD_JSON_BUILD_PAIR_INTEGER("family", server->family), + SD_JSON_BUILD_PAIR_UNSIGNED("port", dns_server_port(server)), + SD_JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", SD_JSON_BUILD_UNSIGNED(ifindex)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("name", server->server_name), + SD_JSON_BUILD_PAIR_BOOLEAN("accessible", accessible)); +} diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h index 8915b87e95..0f066b15b9 100644 --- a/src/resolve/resolved-dns-server.h +++ b/src/resolve/resolved-dns-server.h @@ -190,6 +190,7 @@ static inline bool dns_server_is_fallback(DnsServer *s) { } int dns_server_dump_state_to_json(DnsServer *server, sd_json_variant **ret); +int dns_server_dump_configuration_to_json(DnsServer *server, sd_json_variant **ret); int dns_server_is_accessible(DnsServer *s); static inline void dns_server_reset_accessible(DnsServer *s) { diff --git a/src/resolve/resolved-link-bus.c b/src/resolve/resolved-link-bus.c index b0c4b833b5..77d2663e4f 100644 --- a/src/resolve/resolved-link-bus.c +++ b/src/resolve/resolved-link-bus.c @@ -272,6 +272,7 @@ static int bus_link_method_set_dns_servers_internal(sd_bus_message *message, voi (void) link_save_user(l); (void) manager_write_resolv_conf(l->manager); (void) manager_send_changed(l->manager, "DNS"); + (void) manager_send_dns_configuration_changed(l->manager, l, /* reset= */ true); if (j) log_link_info(l, "Bus client set DNS server list to: %s", j); @@ -751,6 +752,7 @@ int bus_link_method_revert(sd_bus_message *message, void *userdata, sd_bus_error (void) link_save_user(l); (void) manager_write_resolv_conf(l->manager); (void) manager_send_changed(l->manager, "DNS"); + (void) manager_send_dns_configuration_changed(l->manager, l, /* reset= */ true); manager_llmnr_maybe_stop(l->manager); manager_mdns_maybe_stop(l->manager); diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index 883bc09701..e374978691 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -22,6 +22,7 @@ #include "idn-util.h" #include "io-util.h" #include "iovec-util.h" +#include "json-util.h" #include "memstream-util.h" #include "missing_network.h" #include "missing_socket.h" @@ -108,6 +109,11 @@ static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void * /* Now check all the links, and if mDNS/llmr are disabled everywhere, stop them globally too. */ manager_llmnr_maybe_stop(m); manager_mdns_maybe_stop(m); + + /* The accessible flag on link DNS servers will have been reset by + * link_update(). Just reset the global DNS servers. */ + (void) manager_send_dns_configuration_changed(m, NULL, /* reset= */ true); + return 0; fail: @@ -192,6 +198,8 @@ static int manager_process_address(sd_netlink *rtnl, sd_netlink_message *mm, voi break; } + (void) manager_send_dns_configuration_changed(m, l, /* reset= */ true); + return 0; fail: @@ -199,6 +207,38 @@ fail: return 0; } +static int manager_process_route(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + Link *l = NULL; + uint16_t type; + uint32_t ifindex = 0; + int r; + + assert(rtnl); + assert(mm); + + r = sd_netlink_message_get_type(mm, &type); + if (r < 0) { + log_warning_errno(r, "Failed not get message type, ignoring: %m"); + return 0; + } + + if (!IN_SET(type, RTM_NEWROUTE, RTM_DELROUTE)) { + log_warning("Unexpected message type %u when processing route, ignoring.", type); + return 0; + } + + r = sd_netlink_message_read_u32(mm, RTA_OIF, &ifindex); + if (r < 0) + log_full_errno(r == -ENODATA ? LOG_DEBUG : LOG_WARNING, r, "Failed to get route ifindex, ignoring: %m"); + else + l = hashmap_get(m->links, INT_TO_PTR(ifindex)); + + (void) manager_send_dns_configuration_changed(m, l, /* reset= */ true); + + return 0; +} + static int manager_rtnl_listen(Manager *m) { _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; int r; @@ -289,6 +329,7 @@ static int on_network_event(sd_event_source *s, int fd, uint32_t revents, void * (void) manager_write_resolv_conf(m); (void) manager_send_changed(m, "DNS"); + (void) manager_send_dns_configuration_changed(m, NULL, /* reset= */ true); /* Now check all the links, and if mDNS/llmr are disabled everywhere, stop them globally too. */ manager_llmnr_maybe_stop(m); @@ -808,10 +849,14 @@ Manager *manager_free(Manager *m) { sd_event_source_unref(m->network_event_source); sd_network_monitor_unref(m->network_monitor); + sd_netlink_slot_unref(m->netlink_new_route_slot); + sd_netlink_slot_unref(m->netlink_del_route_slot); sd_netlink_unref(m->rtnl); sd_event_source_unref(m->rtnl_event_source); sd_event_source_unref(m->clock_change_event_source); + sd_json_variant_unref(m->dns_configuration_json); + manager_llmnr_stop(m); manager_mdns_stop(m); manager_dns_stub_stop(m); @@ -1938,3 +1983,181 @@ void dns_manager_reset_statistics(Manager *m) { m->n_failure_responses_served_stale_total = 0; zero(m->n_dnssec_verdict); } + +static int dns_configuration_json_append( + const char *ifname, + int ifindex, + int default_route, + DnsServer *current_dns_server, + DnsServer *dns_servers, + DnsSearchDomain *search_domains, + sd_json_variant **configuration) { + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *dns_servers_json = NULL, + *search_domains_json = NULL, + *current_dns_server_json = NULL; + int r; + + assert(configuration); + + if (dns_servers) { + r = sd_json_variant_new_array(&dns_servers_json, NULL, 0); + if (r < 0) + return r; + } + + if (search_domains) { + r = sd_json_variant_new_array(&search_domains_json, NULL, 0); + if (r < 0) + return r; + } + + if (current_dns_server) { + r = dns_server_dump_configuration_to_json(current_dns_server, ¤t_dns_server_json); + if (r < 0) + return r; + } + + LIST_FOREACH(servers, s, dns_servers) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + assert(dns_servers_json); + + r = dns_server_dump_configuration_to_json(s, &v); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&dns_servers_json, v); + if (r < 0) + return r; + } + + LIST_FOREACH(domains, d, search_domains) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + assert(search_domains_json); + + r = dns_search_domain_dump_to_json(d, &v); + if (r < 0) + return r; + + r = sd_json_variant_append_array(&search_domains_json, v); + if (r < 0) + return r; + } + + return sd_json_variant_append_arraybo( + configuration, + JSON_BUILD_PAIR_STRING_NON_EMPTY("ifname", ifname), + SD_JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", SD_JSON_BUILD_UNSIGNED(ifindex)), + SD_JSON_BUILD_PAIR_CONDITION(ifindex > 0, "defaultRoute", SD_JSON_BUILD_BOOLEAN(default_route > 0)), + JSON_BUILD_PAIR_VARIANT_NON_NULL("currentServer", current_dns_server_json), + JSON_BUILD_PAIR_VARIANT_NON_NULL("servers", dns_servers_json), + JSON_BUILD_PAIR_VARIANT_NON_NULL("searchDomains", search_domains_json)); +} + +int manager_dump_dns_configuration_json(Manager *m, sd_json_variant **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *configuration = NULL; + Link *l; + int r; + + assert(m); + assert(ret); + + /* Global DNS configuration */ + r = dns_configuration_json_append( + /* ifname = */ NULL, + /* ifindex = */ 0, + /* default_route = */ 0, + manager_get_dns_server(m), + m->dns_servers, + m->search_domains, + &configuration); + if (r < 0) + return r; + + /* Append configuration for each link */ + HASHMAP_FOREACH(l, m->links) { + r = dns_configuration_json_append( + l->ifname, + l->ifindex, + link_get_default_route(l), + link_get_dns_server(l), + l->dns_servers, + l->search_domains, + &configuration); + if (r < 0) + return r; + } + + return sd_json_buildo(ret, SD_JSON_BUILD_PAIR_VARIANT("configuration", configuration)); +} + +int manager_send_dns_configuration_changed(Manager *m, Link *l, bool reset) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *configuration = NULL; + int r; + + assert(m); + + if (set_isempty(m->varlink_dns_configuration_subscription)) + return 0; + + if (reset) { + dns_server_reset_accessible_all(m->dns_servers); + + if (l) + dns_server_reset_accessible_all(l->dns_servers); + } + + r = manager_dump_dns_configuration_json(m, &configuration); + if (r < 0) + return log_warning_errno(r, "Failed to dump DNS configuration json: %m"); + + if (sd_json_variant_equal(configuration, m->dns_configuration_json)) + return 0; + + JSON_VARIANT_REPLACE(m->dns_configuration_json, TAKE_PTR(configuration)); + + r = varlink_many_notify(m->varlink_dns_configuration_subscription, m->dns_configuration_json); + if (r < 0) + return log_warning_errno(r, "Failed to send DNS configuration event: %m"); + + return 0; +} + +int manager_start_dns_configuration_monitor(Manager *m) { + Link *l; + int r; + + assert(m); + assert(!m->dns_configuration_json); + assert(!m->netlink_new_route_slot); + assert(!m->netlink_del_route_slot); + + dns_server_reset_accessible_all(m->dns_servers); + + HASHMAP_FOREACH(l, m->links) + dns_server_reset_accessible_all(l->dns_servers); + + r = manager_dump_dns_configuration_json(m, &m->dns_configuration_json); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, &m->netlink_new_route_slot, RTM_NEWROUTE, manager_process_route, NULL, m, "resolve-NEWROUTE"); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, &m->netlink_del_route_slot, RTM_DELROUTE, manager_process_route, NULL, m, "resolve-DELROUTE"); + if (r < 0) + return r; + + return 0; +} + +void manager_stop_dns_configuration_monitor(Manager *m) { + assert(m); + + m->dns_configuration_json = sd_json_variant_unref(m->dns_configuration_json); + m->netlink_new_route_slot = sd_netlink_slot_unref(m->netlink_new_route_slot); + m->netlink_del_route_slot = sd_netlink_slot_unref(m->netlink_del_route_slot); +} diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index 5d870111d1..3481a8836b 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -153,6 +153,12 @@ struct Manager { sd_varlink_server *varlink_monitor_server; Set *varlink_query_results_subscription; + Set *varlink_dns_configuration_subscription; + + sd_json_variant *dns_configuration_json; + + sd_netlink_slot *netlink_new_route_slot; + sd_netlink_slot *netlink_del_route_slot; sd_event_source *clock_change_event_source; @@ -225,3 +231,9 @@ int socket_disable_pmtud(int fd, int af); int dns_manager_dump_statistics_json(Manager *m, sd_json_variant **ret); void dns_manager_reset_statistics(Manager *m); + +int manager_dump_dns_configuration_json(Manager *m, sd_json_variant **ret); +int manager_send_dns_configuration_changed(Manager *m, Link *l, bool reset); + +int manager_start_dns_configuration_monitor(Manager *m); +void manager_stop_dns_configuration_monitor(Manager *m); diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index fd2fc58853..02e00eef7d 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -127,15 +127,25 @@ static void vl_on_disconnect(sd_varlink_server *s, sd_varlink *link, void *userd static void vl_on_notification_disconnect(sd_varlink_server *s, sd_varlink *link, void *userdata) { Manager *m = ASSERT_PTR(userdata); + sd_varlink *removed_link = NULL; assert(s); assert(link); - sd_varlink *removed_link = set_remove(m->varlink_query_results_subscription, link); + removed_link = set_remove(m->varlink_query_results_subscription, link); if (removed_link) { sd_varlink_unref(removed_link); log_debug("%u query result monitor clients remain active", set_size(m->varlink_query_results_subscription)); } + + removed_link = set_remove(m->varlink_dns_configuration_subscription, link); + if (removed_link) { + sd_varlink_unref(removed_link); + log_debug("%u DNS monitor clients remain active", set_size(m->varlink_dns_configuration_subscription)); + + if (set_isempty(m->varlink_dns_configuration_subscription)) + manager_stop_dns_configuration_monitor(m); + } } static bool validate_and_mangle_flags( @@ -1354,6 +1364,45 @@ static int vl_method_reset_statistics(sd_varlink *link, sd_json_variant *paramet return sd_varlink_replyb(link, SD_JSON_BUILD_EMPTY_OBJECT); } +static int vl_method_subscribe_dns_configuration(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link))); + int r; + + /* if the client didn't set the more flag, it is using us incorrectly */ + if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE)) + return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL); + + r = verify_polkit(link, parameters, "org.freedesktop.resolve1.subscribe-dns-configuration"); + if (r <= 0) + return r; + + if (set_isempty(m->varlink_dns_configuration_subscription)) { + r = manager_start_dns_configuration_monitor(m); + if (r < 0) + return log_error_errno(r, "Failed to start DNS configuration monitor: %m"); + } + + r = sd_varlink_notify(link, m->dns_configuration_json); + if (r < 0) + goto fail; + + r = set_ensure_put(&m->varlink_dns_configuration_subscription, NULL, link); + if (r < 0) + goto fail; + sd_varlink_ref(link); + + log_debug("%u clients now attached for link configuration varlink notifications", + set_size(m->varlink_dns_configuration_subscription)); + + return 1; +fail: + if (set_isempty(m->varlink_dns_configuration_subscription)) + manager_stop_dns_configuration_monitor(m); + + + return log_debug_errno(r, "Failed to subscribe client to DNS configuration monitor: %m"); +} + static int varlink_monitor_server_init(Manager *m) { _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *server = NULL; int r; @@ -1377,7 +1426,8 @@ static int varlink_monitor_server_init(Manager *m) { "io.systemd.Resolve.Monitor.DumpCache", vl_method_dump_cache, "io.systemd.Resolve.Monitor.DumpServerState", vl_method_dump_server_state, "io.systemd.Resolve.Monitor.DumpStatistics", vl_method_dump_statistics, - "io.systemd.Resolve.Monitor.ResetStatistics", vl_method_reset_statistics); + "io.systemd.Resolve.Monitor.ResetStatistics", vl_method_reset_statistics, + "io.systemd.Resolve.Monitor.SubscribeDNSConfiguration", vl_method_subscribe_dns_configuration); if (r < 0) return log_error_errno(r, "Failed to register varlink methods: %m"); diff --git a/src/shared/varlink-io.systemd.Resolve.Monitor.c b/src/shared/varlink-io.systemd.Resolve.Monitor.c index bc8907ddbe..18d4eafefa 100644 --- a/src/shared/varlink-io.systemd.Resolve.Monitor.c +++ b/src/shared/varlink-io.systemd.Resolve.Monitor.c @@ -112,6 +112,52 @@ static SD_VARLINK_DEFINE_METHOD( ResetStatistics, VARLINK_DEFINE_POLKIT_INPUT); +static SD_VARLINK_DEFINE_STRUCT_TYPE( + DNSServer, + SD_VARLINK_FIELD_COMMENT("IPv4 or IPv6 address of the server."), + SD_VARLINK_DEFINE_FIELD(address, SD_VARLINK_INT, SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Address family of the server, one of AF_INET or AF_INET6."), + SD_VARLINK_DEFINE_FIELD(family, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("Port number of the server."), + SD_VARLINK_DEFINE_FIELD(port, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("Interface index for which this server is configured."), + SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Server Name Indication (SNI) of the server."), + SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Indicates if the DNS server is accessible or not."), + SD_VARLINK_DEFINE_FIELD(accessible, SD_VARLINK_BOOL, 0)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SearchDomain, + SD_VARLINK_FIELD_COMMENT("Domain name."), + SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Indicates whether or not this is a routing-only domain."), + SD_VARLINK_DEFINE_FIELD(routeOnly, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Interface index for which this search domain is configured."), + SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + DNSConfiguration, + SD_VARLINK_FIELD_COMMENT("Interface name, if any, associated with this configuration. Empty for global configuration."), + SD_VARLINK_DEFINE_FIELD(ifname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Interface index, if any, associated with this configuration. Empty for global configuration."), + SD_VARLINK_DEFINE_FIELD(ifindex, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Indicates whether or not this link's DNS servers will be used for resolving domain names that do not match any link's configured domains."), + SD_VARLINK_DEFINE_FIELD(defaultRoute, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("DNS server currently selected to use for lookups."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(currentServer, DNSServer, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Array of configured DNS servers."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(servers, DNSServer, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Array of configured search domains."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(searchDomains, SearchDomain, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + SubscribeDNSConfiguration, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The current global and per-interface DNS configurations"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(configuration, DNSConfiguration, SD_VARLINK_ARRAY)); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_Resolve_Monitor, "io.systemd.Resolve.Monitor", @@ -129,4 +175,12 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_type_TransactionStatistics, &vl_type_CacheStatistics, &vl_type_DnssecStatistics, - &vl_type_ServerState); + &vl_type_ServerState, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a DNS server address specification."), + &vl_type_DNSServer, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a search domain specification."), + &vl_type_SearchDomain, + SD_VARLINK_SYMBOL_COMMENT("Encapsulates a global or per-link DNS configuration, including configured DNS servers, search domains, and more."), + &vl_type_DNSConfiguration, + SD_VARLINK_SYMBOL_COMMENT("Sends the complete global and per-link DNS configurations when any changes are made to them. The current configurations are given immediately when this method is invoked."), + &vl_method_SubscribeDNSConfiguration); From 359db7e9b9908a89dd7d94170f0bafdc3fe11fc7 Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Thu, 19 Sep 2024 15:59:50 -0400 Subject: [PATCH 6/9] 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. --- man/systemd-networkd-wait-online.service.xml | 16 ++ src/network/meson.build | 1 + src/network/wait-online/dns-configuration.c | 233 +++++++++++++++++++ src/network/wait-online/dns-configuration.h | 52 +++++ src/network/wait-online/link.c | 4 + src/network/wait-online/link.h | 2 + src/network/wait-online/manager.c | 141 ++++++++++- src/network/wait-online/manager.h | 9 +- src/network/wait-online/wait-online.c | 20 +- 9 files changed, 475 insertions(+), 3 deletions(-) create mode 100644 src/network/wait-online/dns-configuration.c create mode 100644 src/network/wait-online/dns-configuration.h diff --git a/man/systemd-networkd-wait-online.service.xml b/man/systemd-networkd-wait-online.service.xml index e696a2ba9a..2daeabfe7b 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 295d015e7c..5bcce39738 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 0000000000..c369cc115a --- /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 0000000000..6e6890d925 --- /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 3097611d98..a4a64462ff 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 5dc26d9c40..ec352c4113 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 47f5df6a5f..4bad0f97e2 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 01ad18f8f6..e5ff8c21ba 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 48c4d1c0ee..467d2e7da7 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"); From 266cfdfc77a0eee6d4df60d9795f381ced8a1e65 Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Wed, 23 Oct 2024 16:31:00 -0400 Subject: [PATCH 7/9] test: cleanup after testcase_12_resolvectl2 --- test/units/TEST-75-RESOLVED.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh index fd07fd898a..25ab02a938 100755 --- a/test/units/TEST-75-RESOLVED.sh +++ b/test/units/TEST-75-RESOLVED.sh @@ -936,6 +936,15 @@ testcase_11_nft() { # Test resolvectl show-server-state testcase_12_resolvectl2() { + # Cleanup + # shellcheck disable=SC2317 + cleanup() { + rm -f /run/systemd/resolved.conf.d/reload.conf + systemctl reload systemd-resolved.service + } + + trap cleanup RETURN + run resolvectl show-server-state grep -qF "10.0.0.1" "$RUN_OUT" grep -qF "Interface" "$RUN_OUT" From 5ee20919a2f2b2168d2fcf92021afe92eb6d8d1d Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Tue, 22 Oct 2024 10:06:11 -0400 Subject: [PATCH 8/9] test: add test for resolved SubscribeDNSConfiguration API --- test/units/TEST-75-RESOLVED.sh | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh index 25ab02a938..431b37a5e5 100755 --- a/test/units/TEST-75-RESOLVED.sh +++ b/test/units/TEST-75-RESOLVED.sh @@ -1007,6 +1007,83 @@ testcase_12_resolvectl2() { restart_resolved } +# Test io.systemd.Resolve.Monitor.SubscribeDNSConfiguration +testcase_13_varlink_subscribe_dns_configuration() { + # Cleanup + # shellcheck disable=SC2317 + cleanup() { + echo "===== io.systemd.Resolve.Monitor.SubscribeDNSConfiguration output: =====" + cat "$tmpfile" + echo "==========" + rm -f /run/systemd/resolved.conf.d/global-dns.conf + restart_resolved + } + + trap cleanup RETURN ERR + + local unit + local tmpfile + + unit="subscribe-dns-configuration-$(systemd-id128 new -u).service" + tmpfile=$(mktemp) + + # Clear global and per-interface DNS before monitoring the configuration change. + mkdir -p /run/systemd/resolved.conf.d/ + { + echo "[Resolve]" + echo "DNS=" + } > /run/systemd/resolved.conf.d/global-dns.conf + systemctl reload systemd-resolved.service + resolvectl dns dns0 "" + resolvectl domain dns0 "" + + # Start the call to io.systemd.Resolve.Monitor.SubscribeDNSConfiguration + systemd-run -u "$unit" -p "Type=exec" -p "StandardOutput=truncate:$tmpfile" \ + varlinkctl call --more --timeout=5 --graceful=io.systemd.TimedOut /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor.SubscribeDNSConfiguration '{}' + + # Wait until the initial configuration has been received. + timeout 5 bash -c "until [[ -s $tmpfile ]]; do sleep 0.1; done" + + # Update the global configuration. + mkdir -p /run/systemd/resolved.conf.d/ + { + echo "[Resolve]" + echo "DNS=8.8.8.8" + echo "Domains=lan" + } > /run/systemd/resolved.conf.d/global-dns.conf + systemctl reload systemd-resolved.service + + # Update a link configuration. + resolvectl dns dns0 8.8.4.4 1.1.1.1 + resolvectl domain dns0 ~. + + # Wait for the monitor to exit gracefully. + while systemctl --quiet is-active "$unit"; do + sleep 0.5 + done + + # Hack to remove the "Method call returned expected error" line from the output. + sed -i '/^Method call.*returned expected error/d' "$tmpfile" + + # Check that an initial reply was given with the settings applied BEFORE the monitor started. + grep -qF \ + '{"global":{"servers":null,"domains":null}}' \ + <(jq -cr --seq '.configuration[] | select(.ifname == null) | {"global": {servers: .servers, domains: .searchDomains}}' "$tmpfile") + grep -qF \ + '{"dns0":{"servers":null,"domains":null}}' \ + <(jq -cr --seq '.configuration[] | select(.ifname == "dns0") | {"dns0": {servers: .servers, domains: .searchDomains}}' "$tmpfile") + + # Check that the global configuration change was reflected. + grep -qF \ + '{"global":{"servers":[[8,8,8,8]],"domains":["lan"]}}' \ + <(jq -cr --seq '.configuration[] | select(.ifname == null and .servers != null and .searchDomains != null) | {"global":{servers: [.servers[] | .address], domains: [.searchDomains[] | .name]}}' "$tmpfile") + + # Check that the link configuration change was reflected. + grep -qF \ + '{"dns0":{"servers":[[8,8,4,4],[1,1,1,1]],"domains":["."]}}' \ + <(jq -cr --seq '.configuration[] | select(.ifname == "dns0" and .servers != null and .searchDomains != null) | {"dns0":{servers: [.servers[] | .address], domains: [.searchDomains[] | .name]}}' "$tmpfile") +} + # PRE-SETUP systemctl unmask systemd-resolved.service systemctl enable --now systemd-resolved.service From d6608da1e6a5993f3161e4a150df23cf1a9fcbfc Mon Sep 17 00:00:00 2001 From: Nick Rosbrook Date: Tue, 22 Oct 2024 10:00:49 -0400 Subject: [PATCH 9/9] networkd-test: add basic tests for systemd-networkd-wait-online --dns --- test/test-network/systemd-networkd-tests.py | 124 +++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index a151e00a2f..b4339e282b 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -1176,7 +1176,7 @@ class Utilities(): self.fail(f'Timed out waiting for {link} to reach state {operstate}/{setup_state}') return False - def wait_online(self, *links_with_operstate, timeout='20s', bool_any=False, ipv4=False, ipv6=False, setup_state='configured', setup_timeout=5): + def wait_online(self, *links_with_operstate, timeout='20s', bool_any=False, ipv4=False, ipv6=False, setup_state='configured', setup_timeout=5, bool_dns=False): """Wait for the links to reach the specified operstate and/or setup state. This is similar to wait_operstate() but can be used for multiple links, @@ -1190,6 +1190,8 @@ class Utilities(): Set 'bool_any' to True to wait for any (instead of all) of the given links. If this is set, no setup_state checks are done. + Set 'bool_dns' to True to wait for DNS servers to be accessible. + Set 'ipv4' or 'ipv6' to True to wait for IPv4 address or IPv6 address, respectively, of each of the given links. This is applied only for the operational state 'degraded' or above. @@ -1202,6 +1204,8 @@ class Utilities(): args = wait_online_cmd + [f'--timeout={timeout}'] + [f'--interface={link}' for link in links_with_operstate] + [f'--ignore={link}' for link in protected_links] if bool_any: args += ['--any'] + if bool_dns: + args += ['--dns'] if ipv4: args += ['--ipv4'] if ipv6: @@ -1613,6 +1617,124 @@ class WaitOnlineTests(unittest.TestCase, Utilities): self.wait_operstate('bridge99', '(off|no-carrier)', setup_state='configuring') self.wait_operstate('test1', 'degraded') + def do_test_wait_online_dns( + self, + global_dns='', + fallback_dns='', + expect_timeout=False, + network_dropin=None, + ): + global wait_online_env + + if network_dropin is not None: + network_dropin_path = os.path.join( + network_unit_dir, + '25-dhcp-client-use-dns-ipv4.network.d/test.conf' + ) + mkdir_p(os.path.dirname(network_dropin_path)) + with open(network_dropin_path, 'w') as f: + f.write(network_dropin) + + copy_network_unit( + '25-veth.netdev', + '25-dhcp-client-use-dns-ipv4.network', + '25-dhcp-server.network' + ) + start_networkd() + self.wait_online('veth-peer:routable') + + # Unless given, clear global DNS configuration + resolved_dropin = '/run/systemd/resolved.conf.d/global-dns.conf' + mkdir_p(os.path.dirname(resolved_dropin)) + with open(resolved_dropin, 'w') as f: + f.write(( + '[Resolve]\n' + f'DNS={global_dns}\n' + f'FallbackDNS={fallback_dns}\n' + )) + self.addCleanup(os.remove, resolved_dropin) + check_output('systemctl reload systemd-resolved') + + try: + wait_online_env_copy = wait_online_env.copy() + + wait_online_env['SYSTEMD_LOG_LEVEL'] = 'debug' + wait_online_env['SYSTEMD_LOG_TARGET'] = 'console' + + self.wait_online('veth99:routable', bool_dns=True) + + if expect_timeout: + # The above should have thrown an exception. + self.fail( + 'Expected systemd-networkd-wait-online to time out' + ) + + except subprocess.CalledProcessError as e: + if expect_timeout: + self.assertRegex( + e.output, + f'veth99: No link-specific DNS server is accessible', + f'Missing expected log message:\n{e.output}' + ) + else: + self.fail( + f'Command timed out:\n{e.output}' + ) + finally: + wait_online_env = wait_online_env_copy + + def test_wait_online_dns(self): + ''' test systemd-networkd-wait-online with --dns ''' + self.do_test_wait_online_dns() + + def test_wait_online_dns_global(self): + ''' + test systemd-networkd-wait-online with --dns, expect pass due to global DNS + ''' + + # Set UseDNS=no, and allow global DNS to be used. + self.do_test_wait_online_dns( + global_dns='192.168.5.1', + network_dropin=( + '[DHCPv4]\n' + 'UseDNS=no\n' + ) + ) + + def test_wait_online_dns_expect_timeout(self): + ''' test systemd-networkd-wait-online with --dns, and expect timeout ''' + + # Explicitly set DNSDefaultRoute=yes, and require link-specific DNS to be used. + self.do_test_wait_online_dns( + expect_timeout=True, + network_dropin=( + '[Network]\n' + 'DNSDefaultRoute=yes\n' + '[DHCPv4]\n' + 'UseDNS=no\n' + ) + ) + + def test_wait_online_dns_expect_timeout_global(self): + ''' + test systemd-networkd-wait-online with --dns, and expect timeout + despite global DNS + ''' + + # Configure Domains=~., and expect timeout despite global DNS servers + # being available. + self.do_test_wait_online_dns( + expect_timeout=True, + global_dns='192.168.5.1', + network_dropin=( + '[Network]\n' + 'Domains=~.\n' + '[DHCPv4]\n' + 'UseDNS=no\n' + ) + ) + + class NetworkdNetDevTests(unittest.TestCase, Utilities): def setUp(self):