From bb1f182beeb5ca5c8ecc4638d302cb6ed183acc8 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 12 Apr 2026 13:17:53 +0200 Subject: [PATCH] Fix local host records being overridden by upstream NXDOMAIN When a locally-configured hostname (from /etc/hosts, DHCP, or host-record) is queried for a record type with no local answer (e.g. AAAA when only A exists), the query is forwarded upstream. If the domain only exists locally, upstream returns NXDOMAIN, which incorrectly overrides the local host record's existence. Fix in two places: 1. answer_request(): When no answer is found for the requested type but the domain has local host/DHCP/config records, return NODATA instead of forwarding. The domain exists locally, just not for the requested type. 2. process_reply(): Move the NXDOMAIN-to-NODATA conversion for locally-known domains before the DNSSEC bogus-answer gate. Local host records are authoritative for domain existence regardless of upstream DNSSEC validation status. --- src/forward.c | 24 +++++++++++++----------- src/rfc1035.c | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/forward.c b/src/forward.c index 1a7c586..501d8e4 100644 --- a/src/forward.c +++ b/src/forward.c @@ -788,21 +788,23 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server return resize_packet(header, n, pheader, plen); } + /* Convert NXDOMAIN to NODATA for locally-known domains. This must happen + unconditionally, even for DNSSEC BOGUS answers, since local host records + are authoritative for domain existence. */ + if (!(header->hb3 & HB3_TC) && rcode == NXDOMAIN && + extract_name(header, n, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0) && + (check_for_local_domain(daemon->namebuff, now) || lookup_domain(daemon->namebuff, F_CONFIG, NULL, NULL))) + { + header->hb3 |= HB3_AA; + SET_RCODE(header, NOERROR); + rcode = NOERROR; + cache_secure = 0; + } + if (header->hb3 & HB3_TC) log_query(F_UPSTREAM, NULL, NULL, "truncated", 0); else if (!bogusanswer || (header->hb4 & HB4_CD)) { - if (rcode == NXDOMAIN && extract_name(header, n, NULL, daemon->namebuff, EXTR_NAME_EXTRACT, 0) && - (check_for_local_domain(daemon->namebuff, now) || lookup_domain(daemon->namebuff, F_CONFIG, NULL, NULL))) - { - /* if we forwarded a query for a locally known name (because it was for - an unknown type) and the answer is NXDOMAIN, convert that to NODATA, - since we know that the domain exists, even if upstream doesn't */ - header->hb3 |= HB3_AA; - SET_RCODE(header, NOERROR); - cache_secure = 0; - } - if (daemon->doctors && do_doctor(header, n, daemon->namebuff)) cache_secure = 0; diff --git a/src/rfc1035.c b/src/rfc1035.c index 0e31b83..4714d3f 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -2302,6 +2302,27 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, } } + /* If we couldn't answer (e.g. AAAA query with only A host record, or an + unsupported RR type) but the domain has local records from /etc/hosts + or DHCP, return NODATA instead of forwarding upstream. This prevents + upstream NXDOMAIN responses from overriding local domain existence. */ + if (!ans) + { + struct crec *local; + + for (local = cache_find_by_name(NULL, name, now, F_IPV4 | F_IPV6 | F_CNAME); + local; + local = cache_find_by_name(local, name, now, F_IPV4 | F_IPV6 | F_CNAME)) + if (local->flags & (F_HOSTS | F_DHCP | F_CONFIG)) + { + ans = 1; + sec_data = 0; + auth = 0; + log_query(F_NEG | F_CONFIG | (qtype == T_A ? F_IPV4 : F_IPV6), name, NULL, NULL, 0); + break; + } + } + if (!ans) return 0; /* failed to answer a question */ -- 2.43.0