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.

(cherry picked from commit 052ff5fb5d)
This commit is contained in:
Luca Boccassi
2026-04-24 22:03:17 +01:00
parent 4cff7ce255
commit c4e5f003ed
2 changed files with 109 additions and 1 deletions

View File

@@ -92,7 +92,7 @@ int dnstls_stream_connect_tls(DnsStream *stream, DnsServer *server) {
return -EIO;
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);

View File

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