From: Luca Boccassi Date: Fri, 24 Apr 2026 21:03:17 +0000 (+0100) Subject: resolved: honour per-link DNSOverTLS=yes for certificate verification X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=052ff5fb5d57e498c92dc3a0f187486a889bfeff;p=thirdparty%2Fsystemd.git resolved: honour per-link DNSOverTLS=yes for certificate verification dnstls_stream_connect_tls() reads the manager-wide dns_over_tls_mode field directly, so when the global mode was 'no' or 'opportunistic' but a link was configured with strict DNSOverTLS=yes, the DoT connection was established without SSL_VERIFY_PEER or hostname/IP checks. Use dns_server_get_dns_over_tls_mode() which honours the per-link override. --- diff --git a/src/resolve/resolved-dnstls.c b/src/resolve/resolved-dnstls.c index 6bcbdb52602..f39e0447c21 100644 --- a/src/resolve/resolved-dnstls.c +++ b/src/resolve/resolved-dnstls.c @@ -101,7 +101,7 @@ int dnstls_stream_connect_tls(DnsStream *stream, DnsServer *server) { return openssl_to_errno(sym_ERR_get_error()); sym_SSL_set_bio(s, TAKE_PTR(rb), TAKE_PTR(wb)); - if (server->manager->dns_over_tls_mode == DNS_OVER_TLS_YES) { + if (dns_server_get_dns_over_tls_mode(server) == DNS_OVER_TLS_YES) { X509_VERIFY_PARAM *v; sym_SSL_set_verify(s, SSL_VERIFY_PEER, NULL); diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh index d9757b38559..770d9769604 100755 --- a/test/units/TEST-75-RESOLVED.sh +++ b/test/units/TEST-75-RESOLVED.sh @@ -1498,6 +1498,114 @@ EOF grep -qF "1.2.3.4" "$RUN_OUT" } +# Regression test for CVE/strict-DoT verification: when the global +# DNSOverTLS= is "no" or "opportunistic" but a specific link is configured +# with DNSOverTLS=yes (per-link strict mode), resolved must still enable +# OpenSSL peer certificate verification and host/IP name checking on that +# link. Previously dnstls_stream_connect_tls() looked at the manager-wide +# field directly instead of dns_server_get_dns_over_tls_mode(), so a +# self-signed/untrusted DoT server would be silently accepted on a strict +# link, allowing trivial MITM. +testcase_dot_strict_per_link_verify() { + if ! command -v openssl >/dev/null; then + echo "openssl not found, skipping per-link strict DoT test" + return 0 + fi + + local certdir s_server_pid="" link_addr="10.123.99.1" + local journal_cursor + + certdir="$(mktemp -d)" + + # shellcheck disable=SC2317,SC2329 + cleanup() { + if [[ -n "$s_server_pid" ]] && kill -0 "$s_server_pid" 2>/dev/null; then + kill "$s_server_pid" || : + wait "$s_server_pid" 2>/dev/null || : + fi + rm -f /run/systemd/resolved.conf.d/95-strict-dot-test.conf + rm -f /run/systemd/network/10-dns3-strict-dot.netdev + rm -f /run/systemd/network/10-dns3-strict-dot.network + networkctl reload || : + ip link del dns3-sdot 2>/dev/null || : + systemctl reload systemd-resolved.service || : + rm -rf "$certdir" "$journal_cursor" + } + + trap cleanup RETURN ERR + + # Self-signed cert for CN=strict-dot.test, with a SAN matching the link + # IP. It is intentionally NOT in the system CA store, so peer + # verification (when actually enabled) MUST reject it. + openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$certdir/key.pem" -out "$certdir/cert.pem" \ + -subj "/CN=strict-dot.test" \ + -addext "subjectAltName=IP:${link_addr},DNS:strict-dot.test" \ + -days 1 + + # Dummy interface that hosts both the fake DoT server and the strict + # client-side configuration. + { + echo "[NetDev]" + echo "Name=dns3-sdot" + echo "Kind=dummy" + } >/run/systemd/network/10-dns3-strict-dot.netdev + { + echo "[Match]" + echo "Name=dns3-sdot" + echo "" + echo "[Network]" + echo "IPv6AcceptRA=no" + echo "Address=${link_addr}/24" + echo "DNS=${link_addr}" + echo "DNSOverTLS=yes" + echo "DNSSEC=no" + } >/run/systemd/network/10-dns3-strict-dot.network + + # Force the manager-wide DoT mode to "no". Before the fix this would + # cause SSL_VERIFY_PEER to NOT be set, even though the link is strict. + { + echo "[Resolve]" + echo "DNSOverTLS=no" + } >/run/systemd/resolved.conf.d/95-strict-dot-test.conf + + networkctl reload + networkctl reconfigure dns3-sdot + /usr/lib/systemd/systemd-networkd-wait-online --timeout=30 --interface=dns3-sdot --dns + + systemctl reload systemd-resolved.service + + assert_eq "$(resolvectl --json=short dnsovertls dns3-sdot | jq -rc '.[0].dnsOverTLS')" 'yes' + + # Start an unauthenticated TLS listener using the self-signed cert. + # -naccept lets resolved reconnect a few times during retries. + openssl s_server \ + -accept "${link_addr}:853" \ + -cert "$certdir/cert.pem" -key "$certdir/key.pem" \ + -quiet -naccept 8 /dev/null 2>&1 & + s_server_pid=$! + + timeout 10s bash -c "until ss -lnt 'sport = :853' | grep -F ${link_addr} >/dev/null; do sleep 0.2; done" + + journal_cursor="$(mktemp)" + journalctl -n0 -q --cursor-file="$journal_cursor" + + # The query must fail: a strict-DoT link must refuse an untrusted cert. + set +e + timeout 15s resolvectl query -i dns3-sdot strict-dot.test + local rc=$? + set -e + if [[ $rc -eq 0 ]]; then + echo "resolvectl query against untrusted DoT server on strict link succeeded, should have failed" + return 1 + fi + + # Ensure the test failed for the right reason + journalctl --sync + journalctl -u systemd-resolved.service --cursor-file="$journal_cursor" \ + --grep "Failed to invoke SSL_do_handshake.*(certificate verify failed|self[- ]signed certificate|unable to get local issuer)" +} + testcase_static_record() { mkdir -p /run/systemd/resolve/static.d/ cat >/run/systemd/resolve/static.d/statictest.rr <