timesyncd: resolve NTP hostnames through resolved's varlink interface (#43380)

With DNSSEC on, resolved can't validate anything until the clock is
correct,
and without an RTC the only thing that fixes the clock is talking to the
NTP servers we're trying to resolve.

On glibc the timesyncd unit sets SYSTEMD_NSS_RESOLVE_VALIDATE=0 and
nss-resolve turns that into a per-query flag, which breaks the
chicken/egg. musl and systems not using nss-resolve have no NSS, so
nothing
reads it there and we're stuck.

So talk to resolved directly when it's there, and pass
SD_RESOLVED_NO_VALIDATE until we've synced the clock ourselves. Lookups
after that are validated normally, which is better than what we get from
nss. If resolved isn't around we keep using getaddrinfo().

Tested on musl in qemu with the RTC set to 2020 and DNSSEC=yes.
timesyncd syncs when it couldn't before, while `resolvectl query` still
fails with signature-expired, so the "relaxation" is scoped to only
timesyncd.
Once the clock is synced subsequent timesyncd resolver queries no longer
include the flag to disable validation.

fixes #20235
This commit is contained in:
Yu Watanabe
2026-08-17 02:27:17 +09:00
committed by GitHub
4 changed files with 205 additions and 11 deletions
+132 -6
View File
@@ -10,8 +10,10 @@
#include "sd-bus.h"
#include "sd-daemon.h"
#include "sd-json.h"
#include "sd-messages.h"
#include "sd-network.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "clock-util.h"
@@ -23,6 +25,7 @@
#include "format-util.h"
#include "fs-util.h"
#include "hashmap.h"
#include "in-addr-util.h"
#include "list.h"
#include "log.h"
#include "logarithm.h"
@@ -30,6 +33,8 @@
#include "random-util.h"
#include "ratelimit.h"
#include "resolve-private.h"
#include "resolve-varlink-util.h"
#include "resolved-def.h"
#include "socket-util.h"
#include "string-util.h"
#include "strv.h"
@@ -780,6 +785,119 @@ static int manager_resolve_handler(sd_resolve_query *q, int ret, const struct ad
return manager_begin(m);
}
static int manager_resolve_varlink_reply(
sd_varlink *link,
sd_json_variant *parameters,
const char *error_id,
sd_varlink_reply_flags_t flags,
void *userdata) {
Manager *m = ASSERT_PTR(userdata);
int r;
assert(m->current_server_name);
_unused_ _cleanup_(sd_varlink_unrefp) sd_varlink *link_free = TAKE_PTR(m->resolve_varlink);
if (!isempty(error_id)) {
log_debug("Failed to resolve %s: %s", m->current_server_name->string, error_id);
/* Try next one */
return manager_connect(m);
}
_cleanup_(resolve_hostname_reply_done) ResolveHostnameReply reply = {};
r = dispatch_resolve_hostname_reply(/* name = */ NULL, parameters, SD_JSON_LOG, &reply);
if (r < 0) {
log_warning_errno(r, "Failed to parse reply for %s, ignoring: %m", m->current_server_name->string);
return manager_connect(m);
}
FOREACH_ARRAY(entry, reply.addresses, reply.n_addresses) {
_cleanup_free_ char *pretty = NULL;
union sockaddr_union sa = {};
ServerAddress *a;
if (!IN_SET(entry->family, AF_INET, AF_INET6)) {
log_debug("Ignoring unsuitable address protocol for %s.", m->current_server_name->string);
continue;
}
r = sockaddr_set_in_addr(&sa, entry->family, &entry->in_addr.address, 123);
if (r < 0) {
log_warning_errno(r, "Failed to convert address for %s, trying next host: %m", m->current_server_name->string);
return manager_connect(m);
}
/* Only link-local addresses need a scope, setting it on a global one pins us to one interface. */
if (entry->family == AF_INET6 && in6_addr_is_link_local(&entry->in_addr.address.in6))
sa.in6.sin6_scope_id = entry->ifindex;
r = server_address_new(m->current_server_name, &a, &sa, sockaddr_len(&sa));
if (r < 0) {
log_warning_errno(r, "Failed to add server address, trying next host: %m");
return manager_connect(m);
}
(void) server_address_pretty(a, &pretty);
log_debug("Resolved address %s for %s.", strna(pretty), m->current_server_name->string);
}
if (!m->current_server_name->addresses) {
log_error("Failed to find suitable address for host %s.", m->current_server_name->string);
/* Try next host */
return manager_connect(m);
}
manager_set_server_address(m, m->current_server_name->addresses);
return manager_begin(m);
}
/* Resolve the current server name through resolved's Varlink interface. This returns 0 if resolved is
* unavailable */
static int manager_resolve_varlink(Manager *m) {
_cleanup_(sd_varlink_close_unrefp) sd_varlink *link = NULL;
int r;
assert(m);
assert(m->current_server_name);
r = sd_varlink_connect_address(&link, "/run/systemd/resolve/io.systemd.Resolve");
if (r < 0) {
log_debug_errno(r, "Failed to connect to systemd-resolved, using getaddrinfo(): %m");
return 0;
}
(void) sd_varlink_set_description(link, "timesyncd-resolve");
(void) sd_varlink_set_relative_timeout(link, SD_RESOLVED_QUERY_TIMEOUT_USEC);
r = sd_varlink_attach_event(link, m->event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach Varlink connection to event loop: %m");
r = sd_varlink_bind_reply(link, manager_resolve_varlink_reply);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink reply callback: %m");
sd_varlink_set_userdata(link, m);
/* Ask resolved to skip DNSSEC validation as long as our clock is not synchronized yet. */
r = sd_varlink_invokebo(
link,
"io.systemd.Resolve.ResolveHostname",
SD_JSON_BUILD_PAIR_STRING("name", m->current_server_name->string),
SD_JSON_BUILD_PAIR_CONDITION(!socket_ipv6_is_enabled(), "family", SD_JSON_BUILD_INTEGER(AF_INET)),
SD_JSON_BUILD_PAIR_UNSIGNED("flags", m->synchronized ? 0 : SD_RESOLVED_NO_VALIDATE));
if (r < 0)
return log_error_errno(r, "Failed to call ResolveHostname(): %m");
m->resolve_varlink = TAKE_PTR(link);
return 1;
}
static int manager_retry_connect(sd_event_source *source, usec_t usec, void *userdata) {
Manager *m = ASSERT_PTR(userdata);
@@ -868,19 +986,26 @@ int manager_connect(Manager *m) {
manager_set_server_name(m, f);
}
/* Tell the resolver to reread /etc/resolv.conf, in
* case it changed. */
res_init();
/* Flush out any previously resolved addresses */
server_name_flush_addresses(m->current_server_name);
log_debug("Resolving %s...", m->current_server_name->string);
/* Prefer resolved's varlink interface. If that's unavailable, then fall through to getaddrinfo below. */
r = manager_resolve_varlink(m);
if (r < 0)
return r;
if (r > 0)
return 1;
/* Tell the resolver to reread /etc/resolv.conf, in
* case it changed. */
res_init();
struct addrinfo hints = {
.ai_flags = AI_NUMERICSERV|AI_ADDRCONFIG,
.ai_socktype = SOCK_DGRAM,
.ai_family = socket_ipv6_is_supported() ? AF_UNSPEC : AF_INET,
.ai_family = socket_ipv6_is_enabled() ? AF_UNSPEC : AF_INET,
};
r = resolve_getaddrinfo(m->resolve, &m->resolve_query, m->current_server_name->string, "123", &hints, manager_resolve_handler, NULL, m);
@@ -901,6 +1026,7 @@ void manager_disconnect(Manager *m) {
assert(m);
m->resolve_query = sd_resolve_query_unref(m->resolve_query);
m->resolve_varlink = sd_varlink_close_unref(m->resolve_varlink);
m->event_timer = sd_event_source_unref(m->event_timer);
@@ -1035,7 +1161,7 @@ static bool manager_is_connected(Manager *m) {
/* Return true when the manager is sending a request, resolving a server name, or
* in a poll interval. */
return m->server_socket >= 0 || m->resolve_query || m->event_timer;
return m->server_socket >= 0 || m->resolve_query || m->resolve_varlink || m->event_timer;
}
static int manager_network_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+1
View File
@@ -41,6 +41,7 @@ typedef struct Manager {
/* peer */
sd_resolve_query *resolve_query;
sd_varlink *resolve_varlink;
sd_event_source *event_receive;
ServerName *current_server_name;
ServerAddress *current_server_address;
+71
View File
@@ -248,6 +248,19 @@ assert_timesyncd_state() {
return 1
}
assert_timesyncd_log() {
local cursor="${1:?}"
local match="${2:?}"
for _ in {0..29}; do
journalctl --sync
journalctl -q -u systemd-timesyncd.service --after-cursor="$(<"$cursor")" --grep "$match" >/dev/null && return 0
sleep .5
done
return 1
}
testcase_ntp() {
# timesyncd has ConditionVirtualization=!container by default; drop/mock that for testing
if systemd-detect-virt --container --quiet; then
@@ -409,6 +422,64 @@ EOF
rm -f /run/systemd/network/ntp99.*
}
teardown_timesyncd_resolve() {
set +eu
systemctl start systemd-resolved-varlink.socket
systemctl start systemd-resolved.service
rm -rf /run/systemd/system/systemd-timesyncd.service.d
systemctl daemon-reload
systemctl stop systemd-timesyncd
}
testcase_timesyncd_resolve() {
local cursor
if systemd-detect-virt -cq; then
echo "This test case requires a VM, skipping..."
return 0
fi
if ! command -v resolvectl >/dev/null; then
echo "This test requires systemd-resolved, skipping..."
return 0
fi
trap teardown_timesyncd_resolve RETURN
mkdir -p /run/systemd/system/systemd-timesyncd.service.d
cat >/run/systemd/system/systemd-timesyncd.service.d/10-debug.conf <<EOF
[Service]
Environment=SYSTEMD_LOG_LEVEL=debug
EOF
systemctl daemon-reload
systemctl unmask systemd-resolved
systemctl unmask systemd-timesyncd
systemctl start systemd-resolved-varlink.socket
systemctl start systemd-resolved.service
systemctl restart systemd-timesyncd
cursor="$(mktemp)"
# Should use resolved for resolving names
journalctl -n0 -q --cursor-file="$cursor"
busctl call org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager \
SetRuntimeNTPServers as 1 foo.localhost
# Only the varlink path logs this, so it tells us which resolver ran
assert_timesyncd_log "$cursor" "io.systemd.Resolve.ResolveHostname"
# Without resolved, it should fall back to getaddrinfo
systemctl stop systemd-resolved-varlink.socket systemd-resolved-monitor.socket
systemctl stop systemd-resolved.service
journalctl -n0 -q --cursor-file="$cursor"
busctl call org.freedesktop.timesync1 /org/freedesktop/timesync1 org.freedesktop.timesync1.Manager \
SetRuntimeNTPServers as 1 foo.localhost
assert_timesyncd_log "$cursor" "Failed to connect to systemd-resolved, using getaddrinfo"
}
teardown_timedated_alternate_paths() {
set +eu
+1 -5
View File
@@ -13,7 +13,7 @@ Documentation=man:systemd-timesyncd.service(8)
ConditionCapability=CAP_SYS_TIME
ConditionVirtualization=!container
DefaultDependencies=no
After=systemd-sysusers.service
After=systemd-sysusers.service systemd-resolved.service systemd-resolved-varlink.socket
Before=time-set.target sysinit.target shutdown.target
Conflicts=shutdown.target
Wants=time-set.target
@@ -22,10 +22,6 @@ Wants=time-set.target
AmbientCapabilities=CAP_SYS_TIME
BusName=org.freedesktop.timesync1
CapabilityBoundingSet=CAP_SYS_TIME
# Turn off DNSSEC validation for hostname look-ups, since those need the
# correct time to work, but we likely won't acquire that without NTP. Let's
# break this chicken-and-egg cycle here.
Environment=SYSTEMD_NSS_RESOLVE_VALIDATE=0
ExecStart={{LIBEXECDIR}}/systemd-timesyncd
LockPersonality=yes
MemoryDenyWriteExecute=yes