Hi, I believe I found a tiny bug in dnsfunnel, possibly caused by s6-dns.
For context: I wrote a tiny DNS sink that works as follows:
- Loads a blocklist from a CDB
- Matches DNS questions first by query family, if not INTERNET => respond NOTIMP
- Matches domain against the blocklist, if blocked => respond NXDOMAIN, otherwise
respond REFUSED (NODATA could be more correct, but I changed it during debugging
and didn't go back, I'm not sure it falls through in dnsfunnel like REFUDES does anyway)
The idea being that you would have it as first option in resolv.conv/dnsfunnel and fall back
to other resolver (in my case, dnscache) for questions that were not about domains or not
blocked.
With resolv.conf only it seems to work properly, blocking ad domains near instantly while
very quickly responding from dnscache otherwise. With dnsfunnel non-blocked domains
always take exactly three seconds, consistent with the first retry after every server failed.
My expectations there are that it matches resolv.conf behavior, falling back immediately
to the second server when receiving REFUSED.
I believe the cause to be:
s6dns_engine.c:181 s6dns_engine_write_udp(): return (errno = EAGAIN, 1) ; // query sent
s6dns_engine.c:330 s6dns_engine_event(): // calls s6dns_engine_write_udp(), return value discarded
s6dns_engine.c:340 if (error_isagain(errno)) return 0 ; // errno=EAGAIN survives to next event
--- reply arrives ---
skalibs fd_recv.c:10,14 int e = errno ; ... if (r >= 0) errno = e ; // recv() success restores errno=EAGAIN
s6dns_engine.c:208 s6dns_engine_read_udp(): r = fd_recv(...) ; r >= 0 // errno still EAGAIN
s6dns_engine.c:211-215 relevant() returns 1 (match) // doesn't touch errno
s6dns_engine.c:233-238 switch (h.rcode) { case 1/4/5: ...; default: prepare_next(dt, stamp, 0) ; }
s6dns_engine.c:161-163 prepare_next(): if (!error_isagain(errno)) { ... } // errno==EAGAIN -> skip
Then we wait until the first UDP timeout from s6dns_engine_timeout().
A quick and dirty fix could be:
```
diff --git a/src/libs6dns/s6dns_engine.c b/src/libs6dns/s6dns_engine.c
index 85d83a2..8315bbd 100644
--- a/src/libs6dns/s6dns_engine.c
+++ b/src/libs6dns/s6dns_engine.c
_at__at_ -235,7 +235,7 _at__at_ static int s6dns_engine_read_udp (s6dns_engine_t *dt, tain const *stamp)
case 0 : case 3 : break ; /* normal operation */
case 1 : case 4 : case 5 :
memset(s6dns_ip46list_ip(&dt->servers, dt->curserver), 0, SKALIBS_IP_SIZE) ; /* do not query it again */
- default : prepare_next(dt, stamp, 0) ; return 0 ;
+ default : errno = 0; prepare_next(dt, stamp, 0) ; return 0 ;
}
if (!stralloc_catb(&dt->sa, buf, r))
{
```
Note however I haven't tried the proposed fix and have no idea if it could break something else.
But it would evaluate `error_isagain` to false, causing the failover to be instant in that call site.
Arguably, it should be correct as well, as none of those cases are actual errors, just possible
valid DNS responses.
Cheers,
Mario.
Received on Thu Jul 16 2026 - 16:14:12 CEST