Skip to content

atenet/dns: answer non-A actor queries instead of SERVFAIL - #874

Draft
Yuan Gao (ygao-g) wants to merge 1 commit into
agent-substrate:mainfrom
ygao-g:atenet-dns-rcode
Draft

atenet/dns: answer non-A actor queries instead of SERVFAIL#874
Yuan Gao (ygao-g) wants to merge 1 commit into
agent-substrate:mainfrom
ygao-g:atenet-dns-rcode

Conversation

@ygao-g

@ygao-g Yuan Gao (ygao-g) commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #888
Part of #246

The generated Corefile's template IN A block is the last directive in the server block, so any
query it doesn't match reaches plugin.NextOrFailure with a nil Next and 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_AGAIN and abandons getaddrinfo without reading the paired A answer, so
an 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/sandbox is Alpine (.ko.yaml). SERVFAIL also can't be
negatively 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:

  • NODATA (NOERROR + SOA) for a real actor name queried on a non-A qtype. NXDOMAIN here would
    break musl too, which reads rcode 3 on either half of its parallel A/AAAA pair as "no addresses at
    all".
  • An NXDOMAIN catch-all for names in the zone that genuinely don't exist.

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:

  • On a regex miss the plugin consults fall.Through() and returns SERVFAIL on the spot without
    evaluating any later template. So every block carrying a match needs fallthrough, or the
    blocks after it are unreachable. (A class or qtype mismatch behaves differently — it moves on by
    itself.)
  • Templates are evaluated in Corefile order, so the IN A block must stay first and the
    regex-matched NODATA block must precede the catch-all. The catch-all must not declare
    fallthrough — it's the block that terminates the chain.

Testing

TestMakeCoreFileNegativeAnswers pins the ordering and shape the reasoning above depends on:
template order via strings.Index, exactly two actor match directives and two fallthroughs, two
SOA authority records, and that the catch-all carries no match of its own. TestMakeCoreFile's
golden 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 ServeDNS loop; an e2e
that queries the live zone and asserts the rcode is a separate change.

Locally on the branch: go test -race ./... passes, and hack/verify-all.sh passes except
python-licenses.sh, which can't reach PyPI from my machine and is unrelated to this change (no
Python here).

  • Tests pass
  • Appropriate changes to documentation are included in the PR

🤖 Generated with Claude Code

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.
@ygao-g
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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change really needed? Seems like a cleanup to me. If so, consider removing it to Reduce diff size

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

atenet/dns: actor zone returns SERVFAIL for every query it does not answer

1 participant