Hi Simon,
When a conditional forwarder (server=/domain/addr) points at an
authoritative-only nameserver that doesn't support recursion, three
things go wrong over time:
- process_reply() warns "nameserver X refused to do a recursive
query" on every response that lacks the RA bit. For an authoritative
server this is expected, not an error.
- reply_query() clears last_server on REFUSED, which forces the next
query into forwardall mode. That query sets last_server again on
success, so the one after that runs with forwardall == 0 re-entering the
code path where REFUSED triggers the retry and penalty. The clearing
sustains this cycle.
- reply_query() retries REFUSED through forward_query(), which bumps
failed_queries and tries other servers in the group. With only one
server for the domain the retry just re-sends to the same place; if that
fails again, the response eventually falls through to general upstreams
which return NXDOMAIN for the local domain.
The cumulative effect is that local-domain resolution degrades until
dnsmasq is restarted. This is a common setup with Active Directory DNS
or other authoritative-only forwarders behind conditional forwarding rules.
The attached patch adds a server->domain_len == 0 guard to all three
code paths so the existing REFUSED/RA-bit handling continues to apply to
general upstream servers but is skipped for domain-specific ones.
SERVFAIL still triggers retries for all servers as before, since that
indicates a genuine server error rather than an expected policy response.
Typical configuration that triggers the problem:
server=/localdomain.ca/192.168.0.2 # AD DNS, authoritative only
server=10.0.0.1 # Unbound, recursive
The AD DNS answers localdomain.ca queries without RA, and returns
REFUSED for anything outside its zone. Without the patch, the REFUSED
handling progressively penalizes 192.168.0.2 until localdomain.ca
queries start going to Unbound which returns NXDOMAIN.
This has first been reported at
https://github.com/pi-hole/FTL/issues/2836 but is something that needs
to be fixed in dnsmasq itself, so I'm submitting the patch here.
Cheers,
Dominik
From dc7dadeb83db9f1562f3d4d9b6da21bb5928511d Mon Sep 17 00:00:00 2001
From: Dominik <[email protected]>
Date: Mon, 6 Apr 2026 10:37:36 +0200
Subject: [PATCH] Don't penalise conditional forwarders for REFUSED responses
Conditional forwarders (server=/domain/addr) are typically
authoritative-only and don't support recursion. Three places in
forward.c treated them identically to general upstream resolvers,
causing cumulative damage when the forwarder returned REFUSED or
omitted the RA bit:
1. process_reply() logged "nameserver X refused to do a recursive
query" whenever the RA bit was absent, even for domain-specific
servers where this is expected. The warning is now skipped when
server->domain_len > 0.
2. reply_query() cleared last_server to -1 on REFUSED, which forced
the next query into forwardall mode. That query sets last_server
again on success, so the one after runs with forwardall == 0 and
re-enters the retry/penalty path on the next REFUSED. The
clearing sustains this cycle. It is now skipped when
server->domain_len > 0.
3. reply_query() retried REFUSED via forward_query(), which
incremented failed_queries on the original server and attempted
other servers in the same domain group. With a single forwarder
this just re-sent to the same server; with none left, the query
fell through to general upstreams that returned NXDOMAIN for the
local domain. REFUSED from domain-specific servers now skips
the retry path entirely (SERVFAIL still retries as before).
Taken together, these changes stop conditional forwarders from being
progressively degraded when they behave as authoritative servers
should: answering their zone without recursion, and refusing
everything else.
Signed-off-by: Dominik <[email protected]>
---
src/forward.c | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/src/forward.c b/src/forward.c
index ab1ca76..226a234 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -763,9 +763,12 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server
if (!is_sign && !option_bool(OPT_DNSSEC_PROXY))
header->hb4 &= ~HB4_AD;
- /* Complain loudly if the upstream server is non-recursive. */
+ /* Complain loudly if the upstream server is non-recursive.
+ Skip this warning for conditional forwarders (domain-specific servers)
+ since they are expected to be authoritative-only and not support recursion. */
if (!(header->hb4 & HB4_RA) && rcode == NOERROR &&
- server && !(server->flags & SERV_WARNED_RECURSIVE))
+ server && !(server->flags & SERV_WARNED_RECURSIVE) &&
+ server->domain_len == 0)
{
(void)prettyprint_addr(&server->addr, daemon->namebuff);
my_syslog(LOG_WARNING, _("nameserver %s refused to do a recursive query"), daemon->namebuff);
@@ -1225,7 +1228,10 @@ void reply_query(int fd, time_t now)
if (RCODE(header) != REFUSED)
daemon->serverarray[first]->last_server = c;
- else if (daemon->serverarray[first]->last_server == c)
+ else if (daemon->serverarray[first]->last_server == c && server->domain_len == 0)
+ /* Don't clear last_server for conditional forwarders (domain-specific servers)
+ on REFUSED - they are authoritative and REFUSED is expected for
+ queries they don't handle recursively. */
daemon->serverarray[first]->last_server = -1;
/* log_query gets called indirectly all over the place, so
@@ -1252,12 +1258,16 @@ void reply_query(int fd, time_t now)
return;
#endif
- if ((RCODE(header) == REFUSED || RCODE(header) == SERVFAIL) && forward->forwardall == 0)
- /* for broken servers, attempt to send to another one. */
+ if (((RCODE(header) == REFUSED && server->domain_len == 0) || RCODE(header) == SERVFAIL) && forward->forwardall == 0)
+ /* For broken servers, attempt to send to another one.
+ Don't retry REFUSED from conditional forwarders (domain-specific servers)
+ as they are authoritative and REFUSED is an expected response when
+ they don't support recursion. Retrying would just penalize the
+ server and send to general upstreams which can't resolve the domain. */
{
/* Get the saved query back. */
blockdata_retrieve(forward->stash, forward->stash_len, (void *)header);
-
+
forward_query(-1, NULL, NULL, 0, header, forward->stash_len, 0, now, forward, 0, 0);
return;
}
--
2.43.0
_______________________________________________
Dnsmasq-discuss mailing list
[email protected]
https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss