]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: honour per-link DNSOverTLS=yes for certificate verification
authorLuca Boccassi <luca.boccassi@gmail.com>
Fri, 24 Apr 2026 21:03:17 +0000 (22:03 +0100)
committerLennart Poettering <lennart@poettering.net>
Wed, 8 Jul 2026 15:29:03 +0000 (17:29 +0200)
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.

src/resolve/resolved-dnstls.c
test/units/TEST-75-RESOLVED.sh

index 6bcbdb526023faa0ae98a92f04856ad68271dd12..f39e0447c2190f40fdf34efb7c4a0bdc1fc5fcde 100644 (file)
@@ -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);
index d9757b385596ebd6c6b031004b5e47d0d5075624..770d9769604e12f7ec7b30704054a7badb43a07e 100755 (executable)
@@ -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