From cb321709e9f20cd0949aa375dad2b97050394659 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Sun, 18 Jan 2026 18:57:08 +0000 Subject: [PATCH] Fix DNSSEC fail with CNAME replies to DS queries. A CNAME reply to a DNSSEC query was confusing the validation logic. It now accepts a signed CNAME reply to a DS query as proof that no DS exists at the domain. This fixes the DS/zone break detection logic. --- CHANGELOG | 5 ++++ src/dnsmasq.h | 2 +- src/dnssec.c | 77 +++++++++++++++++++++++++++++---------------------- src/forward.c | 4 +-- 4 files changed, 52 insertions(+), 36 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d07bb55..bf042e7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,11 @@ version 2.93 (eg rivcoed.org) will still fail. Thanks to Petr Menšík for the bug report. + Fix DNSSEC fail with CNAME replies to DS queries. As CNAME reply + to a DNSSEC query was confusing the validation logic. It now + accepts a signed CNAME reply to a DS query as proof that no DS + exists at the domain. This fixes the DS/zone break detection logic. + version 2.92 Redesign the interaction between DNSSEC validation and per-domain diff --git a/src/dnsmasq.h b/src/dnsmasq.h index b794e55..55051b8 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -1467,7 +1467,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class, int *validate_count); int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class, - int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count); + int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_count); int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen); size_t filter_rrsigs(struct dns_header *header, size_t plen); int setup_timestamp(void); diff --git a/src/dnssec.c b/src/dnssec.c index 83cc879..c9cbacd 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -991,7 +991,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char char *keyname, int class, int *validate_counter) { unsigned char *p = (unsigned char *)(header+1); - int qtype, qclass, rc, i, neganswer = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0; + int qtype, qclass, rc, i, neganswer = 0, prim_ok = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0; int aclass, atype, rdlen, flags; unsigned long ttl; union all_addr a; @@ -1002,7 +1002,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char if (RCODE(header) == SERVFAIL) servfail = neganswer = nons = 1; else - rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl, validate_counter); + rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &prim_ok, &nons, &neg_ttl, validate_counter); p = (unsigned char *)(header+1); if (ntohs(header->qdcount) != 1 || @@ -1019,27 +1019,32 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char { if (STAT_ISEQUAL(rc, STAT_INSECURE)) { - if (option_bool(OPT_BOGUSPRIV) && - (flags = in_arpa_name_2_addr(name, &a)) && - ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) + /* A INSECURE DS answer is OK if it's negative and there's a CNAME answer to the DS answer which is + signed, since that's enough to prove that the DS record doesn't exist. */ + if (!neganswer || !prim_ok) { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); - neganswer = 1; - nons = 0; /* If we're faking a DS, fake one with an NS. */ - neg_ttl = DNSSEC_ASSUMED_DS_TTL; - } - else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) - { - my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); - neganswer = 1; - nons = 0; /* If we're faking a DS, fake one with an NS. */ - neg_ttl = DNSSEC_ASSUMED_DS_TTL; - } - else - { - my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); - log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); - return STAT_BOGUS | DNSSEC_FAIL_INDET; + if (option_bool(OPT_BOGUSPRIV) && + (flags = in_arpa_name_2_addr(name, &a)) && + ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) + { + my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); + neganswer = 1; + nons = 0; /* If we're faking a DS, fake one with an NS. */ + neg_ttl = DNSSEC_ASSUMED_DS_TTL; + } + else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) + { + my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); + neganswer = 1; + nons = 0; /* If we're faking a DS, fake one with an NS. */ + neg_ttl = DNSSEC_ASSUMED_DS_TTL; + } + else + { + my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); + log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); + return STAT_BOGUS | DNSSEC_FAIL_INDET; + } } } else @@ -1964,7 +1969,7 @@ static int zone_status(char *name, int class, char *keyname, time_t now) if the nons argument is non-NULL. */ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, - int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_counter) + int *class, int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_counter) { static unsigned char **targets = NULL; static int target_sz = 0; @@ -2273,6 +2278,20 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch secure = STAT_INSECURE; } + + /* For a DS record, we are interested also in if the answer to the DS query was + a CNAME RRset which validated. That's proof that the DS doesn't exist, + even if it's a CNAME which is not signed, and therefore we have no proof + of what it actually _is_. This return tells us that the answer to + primary query is secure, even is the whole answer is insecure, because + something down the CNAME list doesn't validate or doesn't exist. + Note that prim_ok is only valid when neganswer is true, ie either + the answer is the requested record or it's a CNAME that ends + in a missing answer or an unsigned zone. + */ + if (prim_ok) + *prim_ok = !targets[0]; + /* OK, all the RRsets validate, now see if we have a missing answer or CNAME target. */ for (j = 0; j namebuff, daemon->keyname, forward->class, &orig->validate_counter); else status = dnssec_validate_reply(now, header, plen, daemon->namebuff, daemon->keyname, &forward->class, - !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, &orig->validate_counter); + !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, &orig->validate_counter); if (STAT_ISEQUAL(status, STAT_ABANDONED)) log_resource = 1; @@ -2277,7 +2277,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount); else new_status = dnssec_validate_reply(now, header, n, name, keyname, &class, - !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, validatecount); + !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, validatecount); if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY) && !STAT_ISEQUAL(new_status, STAT_ABANDONED)) break;