fix(dns): find authoritative server for CNAME hosts - #1035
Conversation
j4n
left a comment
There was a problem hiding this comment.
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)): |
There was a problem hiding this comment.
| 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)
|
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:
|
|
My problem was with cloudflare. |
That's what we used to do until #851 was reported, some hidden master setups set an SOA record that can't be resolved |
Summary
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, soquery_dns()silently used the deployment host's recursive resolver.A validating resolver can reuse that NSEC denial for the immediately following CNAME query, causing
cmdeployto 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.pyruff format --check cmdeploy/src/cmdeploy/remote/rdns.py cmdeploy/src/cmdeploy/tests/test_dns.pyscripts/cmdeploy dns -vvalidation against the affected relay completed with all DNS entries verified