From ee435a7ef77d20921e95c1c1c44be0cc17d84858 Mon Sep 17 00:00:00 2001 From: missytake Date: Fri, 8 May 2026 19:34:42 +0200 Subject: [PATCH] fix(dns): query correct NS if MNAME server is hidden (#954) replaces #870 fix #851 * fix(dns): address possible IndexError * fix(dns): remove redundant docstring * fix(dns): don't make NS explicit if None * bump cmlxc to 0.13.5 which fixes a powerdns config issue * remove the unneccessary SOA mocks, simplify mock tests, and run ruff format Co-authored-by: holger krekel --- .github/workflows/ci.yaml | 3 ++- cmdeploy/src/cmdeploy/remote/rdns.py | 20 ++++++++++++-------- cmdeploy/src/cmdeploy/tests/test_dns.py | 22 +++++++++++++++++++--- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5a4e0b7f..e80a9fc4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,8 +57,9 @@ jobs: lxc-test: name: LXC deploy and test - uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.10.0 + uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@v0.13.5 with: + cmlxc_version: v0.13.5 cmlxc_commands: | cmlxc init # single cmdeploy relay test diff --git a/cmdeploy/src/cmdeploy/remote/rdns.py b/cmdeploy/src/cmdeploy/remote/rdns.py index 581ecd8a..ed4ad479 100644 --- a/cmdeploy/src/cmdeploy/remote/rdns.py +++ b/cmdeploy/src/cmdeploy/remote/rdns.py @@ -64,21 +64,25 @@ def get_dkim_entry(mail_domain, pre_command, dkim_selector): ) -def query_dns(typ, domain): - # Get autoritative nameserver from the SOA record. - soa_answers = [ +def get_authoritative_ns(domain): + ns_replies = [ x.split() for x in shell( - f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress + f"dig -r -q {domain} -t NS +noall +authority +answer", print=log_progress ).split("\n") ] - soa = [a for a in soa_answers if len(a) >= 3 and a[3] == "SOA"] - if not soa: + filtered_replies = [a for a in ns_replies if len(a) >= 5 and a[3] == "NS"] + if not filtered_replies: return - ns = soa[0][4] + return filtered_replies[0][4] + + +def query_dns(typ, domain): + ns = get_authoritative_ns(domain) # Query authoritative nameserver directly to bypass DNS cache. - res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress) + direct_ns = f"@{ns}" if ns else "" + res = shell(f"dig {direct_ns} -r -q {domain} -t {typ} +short", print=log_progress) return next((line for line in res.split("\n") if not line.startswith(";")), "") diff --git a/cmdeploy/src/cmdeploy/tests/test_dns.py b/cmdeploy/src/cmdeploy/tests/test_dns.py index 828c52fb..4750b9ce 100644 --- a/cmdeploy/src/cmdeploy/tests/test_dns.py +++ b/cmdeploy/src/cmdeploy/tests/test_dns.py @@ -4,6 +4,7 @@ import pytest from cmdeploy import remote from cmdeploy.dns import check_full_zone, check_initial_remote_data, parse_zone_records +from cmdeploy.remote.rdns import get_authoritative_ns @pytest.fixture @@ -14,11 +15,15 @@ def mockdns_base(monkeypatch): if command.startswith("dig"): if command == "dig": return "." - if "SOA" in command: + if "with.public.soa" in command and "NS" in command: + return "domain.with.public.soa. 2419 IN NS ns1.first-ns.de." + if "with.hidden.soa" in command and "NS" in command: return ( - "delta.chat. 21600 IN SOA ns1.first-ns.de. dns.hetzner.com." - " 2025102800 14400 1800 604800 3600" + "domain.with.hidden.soa. 2137 IN NS ns1.desec.io.\n" + "domain.with.hidden.soa. 2137 IN NS ns2.desec.org." ) + if "NS" in command: + return "delta.chat. 21600 IN NS ns1.first-ns.de." command_chunks = command.split() domain, typ = command_chunks[4], command_chunks[6] try: @@ -125,6 +130,17 @@ class TestPerformInitialChecks: assert not l +@pytest.mark.parametrize( + ("domain", "ns"), + [ + ("domain.with.public.soa", "ns1.first-ns.de."), + ("domain.with.hidden.soa", "ns1.desec.io."), + ], +) +def test_get_authoritative_ns(domain, ns, mockdns): + assert get_authoritative_ns(domain) == ns + + def test_parse_zone_records(): text = """ ; This is a comment