atenet/dns: answer non-A actor queries instead of SERVFAIL - #874
Draft
Yuan Gao (ygao-g) wants to merge 1 commit into
Draft
atenet/dns: answer non-A actor queries instead of SERVFAIL#874Yuan Gao (ygao-g) wants to merge 1 commit into
Yuan Gao (ygao-g) wants to merge 1 commit into
Conversation
The generated Corefile's `template IN A` block was the last directive in the server block, so every query it did not match reached plugin.NextOrFailure with a nil Next and came back SERVFAIL: AAAA for a valid actor, and any non-actor name in the zone. musl libc maps rcode 2 to EAI_AGAIN and abandons getaddrinfo without reading the paired A answer, so Alpine clients could not resolve an actor at all, even on an IPv4-only cluster. Go's resolver tolerates it, which is why this went unnoticed. Append a NODATA template for real actor names on other qtypes and an NXDOMAIN catch-all for everything else, both carrying an SOA so the negative answers are cacheable. NXDOMAIN on the first would break musl too, which reads rcode 3 on either half of its A/AAAA pair as "no addresses". Every block with a `match` needs `fallthrough` -- on a regex miss the plugin returns SERVFAIL before evaluating later blocks -- and the catch-all must not have one.
Yuan Gao (ygao-g)
marked this pull request as draft
August 11, 2026 23:12
| escapedSuffix := strings.ReplaceAll(resources.ActorDNSSuffix, ".", `\.`) | ||
| directives = append(directives, fmt.Sprintf(` match "^%s\.%s\.%s\.$"`, resources.ResourceNameRegexPattern, resources.ResourceNameRegexPattern, escapedSuffix)) | ||
| actorMatch := fmt.Sprintf(` match "^%s\.%s\.%s\.$"`, resources.ResourceNameRegexPattern, resources.ResourceNameRegexPattern, escapedSuffix) | ||
| directives = append(directives, actorMatch) |
Contributor
Author
There was a problem hiding this comment.
Is this change really needed? Seems like a cleanup to me. If so, consider removing it to Reduce diff size
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #888
Part of #246
The generated Corefile's
template IN Ablock is the last directive in the server block, so anyquery it doesn't match reaches
plugin.NextOrFailurewith a nilNextand comes back SERVFAIL —AAAA for a valid actor, and any non-actor name in the zone.
This reproduces on an IPv4-only cluster and doesn't block on the dual-stack work in #246. musl
libc maps rcode 2 to
EAI_AGAINand abandonsgetaddrinfowithout reading the paired A answer, soan Alpine-based actor can't resolve another actor at all. Go's pure-Go resolver tolerates SERVFAIL
and uses the A answer anyway, which is why this went unnoticed: every DNS client in this repo is Go,
while actor images often aren't —
demos/sandboxis Alpine (.ko.yaml). SERVFAIL also can't benegatively cached, so every retry pays the full resolver timeout again.
Scope: this makes AAAA a correct empty answer; it does not publish one. An actor name still
has no IPv6 address after this change. Emitting AAAA is a later change in #246 and has to land after
the router's ingress listener accepts v6, or Happy Eyeballs will prefer a v6 address nothing is
listening on.
The fix appends two templates after the existing one:
break musl too, which reads rcode 3 on either half of its parallel A/AAAA pair as "no addresses at
all".
Both carry an SOA authority record, which is what makes the negative answers cacheable.
Two mechanics of the CoreDNS template plugin drive the shape, and both are easy to get wrong:
fall.Through()and returns SERVFAIL on the spot withoutevaluating any later template. So every block carrying a
matchneedsfallthrough, or theblocks after it are unreachable. (A class or qtype mismatch behaves differently — it moves on by
itself.)
IN Ablock must stay first and theregex-matched NODATA block must precede the catch-all. The catch-all must not declare
fallthrough— it's the block that terminates the chain.Testing
TestMakeCoreFileNegativeAnswerspins the ordering and shape the reasoning above depends on:template order via
strings.Index, exactly two actormatchdirectives and twofallthroughs, twoSOA authority records, and that the catch-all carries no
matchof its own.TestMakeCoreFile'sgolden directive list is extended to cover the new blocks.
These assert the rendered Corefile, not CoreDNS's behaviour on the wire — nothing here resolves a
name. The control-flow claims above come from reading the template plugin's
ServeDNSloop; an e2ethat queries the live zone and asserts the rcode is a separate change.
Locally on the branch:
go test -race ./...passes, andhack/verify-all.shpasses exceptpython-licenses.sh, which can't reach PyPI from my machine and is unrelated to this change (noPython here).
🤖 Generated with Claude Code