From 8f33f1b849db3672e7f74f183907e08b3b0c70f6 Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 21 May 2026 20:40:27 -0400 Subject: [PATCH] fix(dns): recursive mode now returns sinkhole A answer, not NXDOMAIN RA=1 + empty answer section is immediately detectable as fake by any open-resolver scanner. Recursive mode now behaves like open mode (127.0.0.x sinkhole, deterministic on qname) with RA=1 and AA=0, matching what a real recursive resolver returns. --- decnet/templates/dns/server.py | 7 +++++-- tests/service_testing/test_dns.py | 27 +++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/decnet/templates/dns/server.py b/decnet/templates/dns/server.py index 07c4d98c..4c901fa7 100644 --- a/decnet/templates/dns/server.py +++ b/decnet/templates/dns/server.py @@ -481,9 +481,12 @@ def _auth_response(qid: int, rd: bool, qname: str, qtype: int) -> bytes: ans = _rr(qname, TYPE_A, CLASS_IN, 30, _rdata_A(ip)) return _build_header(qid, flags, 1, 1, 0, 0) + q + ans if ZONE_MODE == "recursive": - flags = _flags_response(rd=rd, aa=False, ra=True, rcode=RCODE_NXDOMAIN) + h = int(hashlib.sha256(qname.encode()).hexdigest()[:2], 16) or 1 + ip = f"127.0.0.{h}" + flags = _flags_response(rd=rd, aa=False, ra=True, rcode=RCODE_NOERROR) q = _encode_name(qname) + struct.pack(">HH", qtype, CLASS_IN) - return _build_header(qid, flags, 1, 0, 0, 0) + q + ans = _rr(qname, TYPE_A, CLASS_IN, 30, _rdata_A(ip)) + return _build_header(qid, flags, 1, 1, 0, 0) + q + ans return _refused_response(qid, rd, qname, qtype, CLASS_IN) flags = _flags_response(rd=rd, aa=True, rcode=RCODE_NOERROR) diff --git a/tests/service_testing/test_dns.py b/tests/service_testing/test_dns.py index ce4999dc..41a13341 100644 --- a/tests/service_testing/test_dns.py +++ b/tests/service_testing/test_dns.py @@ -487,8 +487,31 @@ class TestZoneModeRecursive: resp = mod._handle(query, "1.1.1.1", 1234, "udp") assert resp is not None flags = struct.unpack_from(">H", resp, 2)[0] - ra = bool(flags & 0x0080) - assert ra + assert bool(flags & 0x0080) # RA=1 + + def test_recursive_mode_returns_answer_for_out_of_zone(self): + mod, _ = _load_dns({"DNS_ZONE_MODE": "recursive"}) + query = _build_query("evil.example.com", mod.TYPE_A) + resp = mod._handle(query, "1.1.1.1", 1234, "udp") + assert resp is not None + assert _rcode(resp) == mod.RCODE_NOERROR + _, ancount, _, _ = _counts(resp) + assert ancount >= 1 + + def test_recursive_mode_not_authoritative(self): + mod, _ = _load_dns({"DNS_ZONE_MODE": "recursive"}) + query = _build_query("evil.example.com", mod.TYPE_A) + resp = mod._handle(query, "1.1.1.1", 1234, "udp") + assert resp is not None + flags = struct.unpack_from(">H", resp, 2)[0] + assert not bool(flags & 0x0400) # AA=0 + + def test_recursive_mode_sinkhole_in_loopback(self): + mod, _ = _load_dns({"DNS_ZONE_MODE": "recursive"}) + query = _build_query("evil.example.com", mod.TYPE_A) + resp = mod._handle(query, "1.1.1.1", 1234, "udp") + assert resp is not None + assert b"\x7f" in resp # sinkhole 127.x # ── Service registration ──────────────────────────────────────────────────────