Hi Simon, all,
we have a DNSSEC regression in v2.93 that a Pi-hole user ran into [1], and I
think the attached patch is the right fix - but I would like your eyes
on it,
since it touches the DS/CNAME logic you reworked in 707bc2d25.
The symptom: reverse (PTR) lookups for RFC-1918 space that are forwarded
to a
local resolver via a domain-specific server (i.e., our rev-server) return
SERVFAIL as soon as DNSSEC validation is on. It worked in 2.91 and breaks in
2.93. The user's log shows the validator walking up to the private reverse
zone and then abandoning:
dnssec-query[DS] 168.192.in-addr.arpa to 1.1.1.1
validation 22.0.168.192.in-addr.arpa is ABANDONED
and the familiar "insecure reply received for DS ... RFC-1918 address" line
never shows up anymore.
The cause is that 707bc2d25 ("Fix DNSSEC fail with CNAME replies to DS
queries") dropped the
/* Empty DS without NSECS */
if (qtype == T_DS)
return STAT_INSECURE;
shortcut from dnssec_validate_reply(). That matters here because the public
resolvers people forward to (1.1.1.1, 8.8.8.8, 9.9.9.9, ...) serve the
private
reverse zones as empty zones per RFC 6303, and answer the DS query with a
*bare* NXDOMAIN - no SOA, no NSEC, nothing to prove non-existence with:
$ dig +dnssec DS 168.192.in-addr.arpa @1.1.1.1
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
Without the shortcut, prove_non_existence() returns DNSSEC_FAIL_NONSEC,
and we
then call zone_status() for the very name whose DS we are in the middle of
resolving. That name is not cached (it cannot be yet), so zone_status()
returns STAT_NEED_DS for it, dnssec_validate_ds() passes that back up, we
re-issue the identical DS query, the self-dependency trips the loop
detector,
and the whole validation is abandoned - hence the SERVFAIL. The bogus-priv
escape hatch never gets a look-in, as it only fires on rc == STAT_INSECURE.
I did not simply revert the shortcut, since your CNAME-to-DS change
relies on
the code below it. Instead I gated the restored return on the primary query
(j == 0), which is exactly the "the DS query itself came back empty"
case; the
CNAME targets are j > 0 and keep going through prim_ok as before. That
way we
retain both behaviours: a signed CNAME answer to a DS query is still
accepted
as proof of no-DS, and an empty DS answer for the queried name itself is
once
more reported as INSECURE, so the bogus-priv / domain-specific-server
heuristics can decide instead of looping into ABANDONED.
Patch attached, against current master. Reproduction, in case it helps:
dnsmasq --no-daemon --log-queries=extra --dnssec --bogus-priv \
--trust-anchor=<root DS> \
--rev-server=192.168.0.0/24,<local-auth> --server=1.1.1.1
dig @127.0.0.1 22.0.168.192.in-addr.arpa PTR
# 2.93: SERVFAIL, "validation ... is ABANDONED"
# patched: NOERROR, "validation result is INSECURE"
(the local authoritative server answers the PTR from an unsigned zone;
1.1.1.1
answers the DS for 168.192.in-addr.arpa with the bare NXDOMAIN shown above.)
Thanks,
Dominik
[1] https://github.com/pi-hole/FTL/issues/2942
From bb277f8490f0d2d2f3f5da3a939d7ea23fda0f1e Mon Sep 17 00:00:00 2001
From: Dominik <[email protected]>
Date: Sun, 12 Jul 2026 11:04:40 +0200
Subject: [PATCH] Restore INSECURE result for empty DS replies without NSEC
records
Commit 707bc2d25c27fc1a6c14d3fb8d383eea1ef57e5c ("Fix DNSSEC fail with
CNAME replies to DS queries") removed the following shortcut from
dnssec_validate_reply():
/* Empty DS without NSECS */
if (qtype == T_DS)
return STAT_INSECURE;
This broke reverse-DNS (PTR) resolution for RFC-1918 private ranges that
are forwarded to a local resolver with a domain-specific server, when
DNSSEC validation is enabled.
Many public resolvers serve the reverse zones of private address space
as empty zones (RFC 6303) and answer a DS query for such a zone with a
bare NXDOMAIN/NODATA that carries no SOA, NSEC or RRSIG record. For
example, 168.192.in-addr.arpa:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
;; flags: qr ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
With the shortcut removed, such a reply makes prove_non_existence()
return DNSSEC_FAIL_NONSEC, and zone_status() is then consulted for the
very name whose DS we are currently trying to resolve. Since that DS is
not (and cannot yet be) cached, zone_status() returns STAT_NEED_DS for
that same name. dnssec_validate_ds() propagates STAT_NEED_DS, the
identical DS query is re-issued, the resulting self-dependency is
detected as a loop and the validation is abandoned - the client gets
SERVFAIL. The bogus-priv RFC-1918 escape hatch in dnssec_validate_ds()
never runs because it is gated on rc == STAT_INSECURE.
Restore the previous behaviour, scoped to the primary query (j == 0), so
that the CNAME-to-DS case fixed by 707bc2d25 - which acts on CNAME
targets (j > 0) via prim_ok - is preserved. An empty DS reply with no
proof for the queried name itself is again reported as STAT_INSECURE, so
the bogus-priv / domain-specific-server heuristics can decide, instead
of looping into STAT_ABANDONED.
Signed-off-by: Dominik <[email protected]>
---
src/dnssec.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/dnssec.c b/src/dnssec.c
index 71c9bac..08a0cfa 100644
--- a/src/dnssec.c
+++ b/src/dnssec.c
@@ -2297,7 +2297,20 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
{
if (rc_nsec & DNSSEC_FAIL_WORK)
return STAT_ABANDONED;
-
+
+ /* Empty DS reply with no NSEC/NSEC3 proof. This is what many
+ public resolvers return for the reverse zones of RFC-1918
+ private address space, which they serve as empty zones (RFC 6303),
+ ie a bare NXDOMAIN/NODATA with no records to prove non-existence.
+ Treat the answer to the DS query itself (j == 0) as insecure so
+ the caller's bogus-priv/domain-specific-server heuristics can act
+ on it. Without this, zone_status() below returns STAT_NEED_DS for
+ the very name we're validating the DS for, and the resulting
+ self-dependent DS query is detected as a loop and abandoned.
+ CNAME targets (j > 0) are handled via prim_ok above. */
+ if (qtype == T_DS && j == 0)
+ return STAT_INSECURE;
+
if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) &&
!STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE))
{
--
2.43.0
_______________________________________________
Dnsmasq-discuss mailing list
[email protected]
https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss