From bb277f8490f0d2d2f3f5da3a939d7ea23fda0f1e Mon Sep 17 00:00:00 2001 From: Dominik 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 --- 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