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.
This commit is contained in:
2026-05-21 20:40:27 -04:00
parent bbb126e435
commit 8f33f1b849
2 changed files with 30 additions and 4 deletions

View File

@@ -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)) ans = _rr(qname, TYPE_A, CLASS_IN, 30, _rdata_A(ip))
return _build_header(qid, flags, 1, 1, 0, 0) + q + ans return _build_header(qid, flags, 1, 1, 0, 0) + q + ans
if ZONE_MODE == "recursive": 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) 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) return _refused_response(qid, rd, qname, qtype, CLASS_IN)
flags = _flags_response(rd=rd, aa=True, rcode=RCODE_NOERROR) flags = _flags_response(rd=rd, aa=True, rcode=RCODE_NOERROR)

View File

@@ -487,8 +487,31 @@ class TestZoneModeRecursive:
resp = mod._handle(query, "1.1.1.1", 1234, "udp") resp = mod._handle(query, "1.1.1.1", 1234, "udp")
assert resp is not None assert resp is not None
flags = struct.unpack_from(">H", resp, 2)[0] flags = struct.unpack_from(">H", resp, 2)[0]
ra = bool(flags & 0x0080) assert bool(flags & 0x0080) # RA=1
assert ra
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 ────────────────────────────────────────────────────── # ── Service registration ──────────────────────────────────────────────────────