Skip to content

fix(dns): find authoritative server for CNAME hosts - #1035

Draft
cs224 wants to merge 1 commit into
chatmail:mainfrom
cs224:agent/fix-dns-authoritative-lookup
Draft

fix(dns): find authoritative server for CNAME hosts#1035
cs224 wants to merge 1 commit into
chatmail:mainfrom
cs224:agent/fix-dns-authoritative-lookup

Conversation

@cs224

@cs224 cs224 commented Aug 2, 2026

Copy link
Copy Markdown

Summary

  • walk toward the DNS zone apex when an NS query for the exact hostname returns no NS record
  • keep querying the discovered authoritative server directly instead of falling back to a recursive resolver
  • cover SOA/NSEC responses at CNAME hostnames with a regression test

Root cause

get_authoritative_ns() only inspected the NS response for the exact record name. Some authoritative DNS setups return an SOA plus a signed NSEC denial for an NS query at a CNAME hostname. The function then returned no server, so query_dns() silently used the deployment host's recursive resolver.

A validating resolver can reuse that NSEC denial for the immediately following CNAME query, causing cmdeploy to report a correct CNAME as missing. Retrying recreates the same sequence.

Walking the labels toward the zone apex finds the public authoritative NS and restores the intended cache-bypassing lookup.

Validation

  • pytest cmdeploy/src/cmdeploy/tests/test_dns.py -q (24 passed)
  • pytest cmdeploy/src/cmdeploy/tests --ignore=cmdeploy/src/cmdeploy/tests/online -q (49 passed)
  • ruff check cmdeploy/src/cmdeploy/remote/rdns.py cmdeploy/src/cmdeploy/tests/test_dns.py
  • ruff format --check cmdeploy/src/cmdeploy/remote/rdns.py cmdeploy/src/cmdeploy/tests/test_dns.py
  • live scripts/cmdeploy dns -v validation against the affected relay completed with all DNS entries verified

@j4n j4n left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fix is necessary, but I think the root cause is broader than the description says, and there is one bug in the walk.
NS queries only returns records at a zone boundaries; anywhere else you get NODATA with the zone SOA:

$ dig -q www.nine.testrun.org -t NS +noall +authority +answer
www.nine.testrun.org.  3600 IN CNAME nine.testrun.org.
testrun.org.           3600 IN SOA   ns1.first-ns.de. ...   # no NS

$ dig -q _dmarc.nine.testrun.org -t NS +noall +authority +answer
testrun.org.           3600 IN SOA   ns1.first-ns.de. ...   # no NS

$ dig -q nine.testrun.org -t NS +noall +authority +answer
testrun.org.           3600 IN SOA   ns1.first-ns.de. ...   # no NS

$ dig -q testrun.org -t NS +noall +authority +answer
testrun.org.           3600 IN NS    ns1.first-ns.de.       # only here

So get_authoritative_ns() returns None for every record in the zonefile of a relay hosted at a subdomain, and query_dns() silently uses the deployment host's recursive resolver. The CNAME/NSEC case is only one way this surfaces; ordinary negative caching of a freshly added record is the more common one; we should adapt the change log entry accordingly.
One change request inline.

return filtered_replies[0][4]
"""Find the closest authoritative nameserver for a domain."""
labels = domain.rstrip(".").split(".")
for index in range(len(labels)):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for index in range(len(labels)):
for index in range(len(labels) - 1):

The walk runs one label too far, up to the registry (e.g., .org), and if that is not recursing, there will be another lookup failure, so stop one before (at example.org)

@link2xt

link2xt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Would be nice to have some domain example to test it for real. I have found some cloudflared domain with this behavior, only returning SOA record in response to NS request, maybe this is what cloudflare DNS does.

I'm not sure walking up through subdomains until some returns an NS record is correct, at least this is not what actual resolvers do. Other options are:

  1. Look at the SOA record directly. First column is the domain name that can be queried for NS records, this usually works. SOA record itself contains one authoritative nameserver, not sure if using it directly can fail. This is not what actual resolvers do, so might be theoretically correct but fail in practice if somehow we get broken SOA record or the nameserver listed there is temporarily down. The upside is that it is simple, you do one SOA request and then one NS request (or no NS request if you take the nameserver from the SOA record).
  2. Do what the recursive resolvers do, but without caching. Maybe by running dig with +trace, and ask the root servers for the NS record, then the next returned NS server for NS server etc. until you get NS servers for the chatmail domain.

@cs224

cs224 commented Aug 14, 2026

Copy link
Copy Markdown
Author

My problem was with cloudflare.

@missytake

Copy link
Copy Markdown
Contributor
1. Look at the SOA record directly. First column is the domain name that can be queried for NS records, this usually works. SOA record itself contains one authoritative nameserver, not sure if using it directly can fail. This is not what actual resolvers do, so might be theoretically correct but fail in practice if somehow we get broken SOA record or the nameserver listed there is temporarily down. The upside is that it is simple, you do one SOA request and then one NS request (or no NS request if you take the nameserver from the SOA record).

That's what we used to do until #851 was reported, some hidden master setups set an SOA record that can't be resolved

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.

4 participants