mirror of
https://github.com/isc-projects/bind9.git
synced 2026-05-28 04:34:54 -04:00
Add AXFR regression test for SIG covers preservation
diff.c rdata_covers() runs on both dns_diff_apply (IXFR, ns/update.c dynamic updates) and dns_diff_load (AXFR). After the previous commit refused SIG and NXT in dynamic updates, the AXFR path remains the most natural way to drive legacy SIG records into a secondary's zone DB and regression-gate the rdata_covers() fix. The test adds ans11 as an AsyncDnsServer primary for a small zone whose AXFR carries two SIG rdatas at the same owner with different covered types (A, MX) and different TTLs (600, 1200), and declares ns6 a secondary of that zone. With the bug present, dns_diff_load groups both tuples at typepair (SIG, 0) and the MX-covering record inherits the first-seen TTL (600); the fix keeps them at (SIG, A) and (SIG, MX) with their original TTLs. rndc dumpdb -zones on the secondary is used to inspect stored state directly, because the wire-level SIG query response merges same-(owner,type,class) RRs and masks the per-rdataset TTLs.
This commit is contained in:
parent
3a44a13232
commit
e9f880c78f
3 changed files with 194 additions and 0 deletions
117
bin/tests/system/nsupdate/ans11/ans.py
Normal file
117
bin/tests/system/nsupdate/ans11/ans.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
#
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# See the COPYRIGHT file distributed with this work for additional
|
||||
# information regarding copyright ownership.
|
||||
|
||||
"""
|
||||
GL#5818 Finding 1 regression support — AsyncDnsServer primary.
|
||||
|
||||
Serves a minimal zone "sigaxfr.nil." whose AXFR carries two SIG records
|
||||
at the same owner with different covered types (A and MX) and different
|
||||
TTLs (600 and 1200). A buggy secondary running dns_diff_load() with
|
||||
rdata_covers() that only recognises RRSIG will file both rdatas under
|
||||
typepair (SIG, 0) with the first tuple's TTL; a fixed secondary keeps
|
||||
them under (SIG, A) and (SIG, MX) with their distinct TTLs.
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import dns.name
|
||||
import dns.rcode
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
import dns.rrset
|
||||
|
||||
from isctest.asyncserver import (
|
||||
AsyncDnsServer,
|
||||
DnsResponseSend,
|
||||
DomainHandler,
|
||||
QueryContext,
|
||||
ResponseAction,
|
||||
)
|
||||
|
||||
ZONE = dns.name.from_text("sigaxfr.nil.")
|
||||
NS_NAME = dns.name.from_text("ns.sigaxfr.nil.")
|
||||
HOST = dns.name.from_text("host.sigaxfr.nil.")
|
||||
|
||||
SOA_TEXT = "ns.sigaxfr.nil. hostmaster.sigaxfr.nil. 1 3600 1200 604800 3600"
|
||||
|
||||
|
||||
def _make_sig_rdata(covered_text):
|
||||
"""Produce a legacy SIG (24) rdata via RRSIG (46) round-trip."""
|
||||
rrsig = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, covered_text)
|
||||
wire = rrsig.to_digestable()
|
||||
return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.SIG, wire, 0, len(wire))
|
||||
|
||||
|
||||
class SigAxfrServer(DomainHandler):
|
||||
"""Serve SOA and AXFR for sigaxfr.nil.; other qtypes get NOERROR/NODATA."""
|
||||
|
||||
domains = ["sigaxfr.nil."]
|
||||
|
||||
async def get_responses(
|
||||
self, qctx: QueryContext
|
||||
) -> AsyncGenerator[ResponseAction, None]:
|
||||
soa_rrset = dns.rrset.from_text(
|
||||
ZONE, 3600, dns.rdataclass.IN, dns.rdatatype.SOA, SOA_TEXT
|
||||
)
|
||||
|
||||
if qctx.qtype == dns.rdatatype.SOA:
|
||||
resp = qctx.response
|
||||
resp.answer.append(soa_rrset)
|
||||
yield DnsResponseSend(resp)
|
||||
return
|
||||
|
||||
if qctx.qtype != dns.rdatatype.AXFR:
|
||||
# Other types: empty NOERROR response.
|
||||
yield DnsResponseSend(qctx.response)
|
||||
return
|
||||
|
||||
# AXFR: opening SOA, NS, NS's A, two SIG RRs at the same owner
|
||||
# with distinct covered types and TTLs, closing SOA.
|
||||
resp = qctx.response
|
||||
resp.answer.append(soa_rrset)
|
||||
|
||||
ns_rrset = dns.rrset.from_text(
|
||||
ZONE, 3600, dns.rdataclass.IN, dns.rdatatype.NS, str(NS_NAME)
|
||||
)
|
||||
resp.answer.append(ns_rrset)
|
||||
|
||||
a_rrset = dns.rrset.from_text(
|
||||
NS_NAME, 3600, dns.rdataclass.IN, dns.rdatatype.A, "10.53.0.11"
|
||||
)
|
||||
resp.answer.append(a_rrset)
|
||||
|
||||
sig_a = _make_sig_rdata("A 6 2 600 20260331170000 20260318160000 21831 . 0000")
|
||||
sig_a_rrset = dns.rrset.RRset(HOST, dns.rdataclass.IN, dns.rdatatype.SIG)
|
||||
sig_a_rrset.add(sig_a, ttl=600)
|
||||
resp.answer.append(sig_a_rrset)
|
||||
|
||||
sig_mx = _make_sig_rdata(
|
||||
"MX 6 2 1200 20260331170000 20260318160000 21831 . 0000"
|
||||
)
|
||||
sig_mx_rrset = dns.rrset.RRset(HOST, dns.rdataclass.IN, dns.rdatatype.SIG)
|
||||
sig_mx_rrset.add(sig_mx, ttl=1200)
|
||||
resp.answer.append(sig_mx_rrset)
|
||||
|
||||
# Closing SOA terminates the AXFR.
|
||||
resp.answer.append(soa_rrset)
|
||||
|
||||
yield DnsResponseSend(resp)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
|
||||
server.install_response_handler(SigAxfrServer())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -36,3 +36,10 @@ zone "2.0.0.2.ip6.arpa" {
|
|||
file "2.0.0.2.ip6.addr.db";
|
||||
update-policy { grant * 6to4-self . NS(10) DS(4); };
|
||||
};
|
||||
|
||||
zone "sigaxfr.nil" {
|
||||
type secondary;
|
||||
primaries { 10.53.0.11; };
|
||||
file "sigaxfr.bk";
|
||||
request-ixfr no; # ans11 serves AXFR only
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ diff.c:rdata_covers() bug via inbound zone transfer is covered separately
|
|||
by the AXFR-based regression test in this file.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import dns.rcode
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
|
|
@ -102,6 +104,74 @@ def test_tcp_self_sig_record(ns6):
|
|||
assert not stored, "SIG record was stored despite REFUSED response"
|
||||
|
||||
|
||||
def test_sig_covers_preserved_via_axfr(ns6):
|
||||
"""Regression test for GL#5818 Finding 1, reached via AXFR.
|
||||
|
||||
ans11 serves an AXFR for sigaxfr.nil. containing two SIG rdatas at
|
||||
the same owner with different covered types (A, MX) and different
|
||||
TTLs (600, 1200). ns6 pulls the zone via dns_diff_load(), which
|
||||
calls diff.c rdata_covers(); before the fix that helper returned 0
|
||||
for SIG, so both tuples were grouped and filed under typepair
|
||||
(SIG, 0) with the first TTL (600) — the MX-covering record's TTL
|
||||
(1200) was silently dropped. With the fix the records land in
|
||||
distinct typepairs and both TTLs survive.
|
||||
|
||||
rndc dumpdb is used to inspect the secondary's stored state
|
||||
directly; the wire-level response can merge same-(owner,type,class)
|
||||
RRs and mask the difference.
|
||||
"""
|
||||
zone = "sigaxfr.nil"
|
||||
owner = f"host.{zone}."
|
||||
dump_path = ns6.directory / "named_dump.db"
|
||||
|
||||
# ns6 may have tried to SOA-poll ans11 before it was listening; force
|
||||
# a fresh refresh attempt and wait for the transfer to complete.
|
||||
with ns6.watch_log_from_here() as watcher:
|
||||
ns6.rndc(f"refresh {zone}")
|
||||
watcher.wait_for_line(f"zone {zone}/IN: transferred serial 1")
|
||||
|
||||
# Remove any stale dump and ask named for a fresh one.
|
||||
if dump_path.exists():
|
||||
dump_path.unlink()
|
||||
ns6.rndc("dumpdb -zones")
|
||||
|
||||
# rndc dumpdb is asynchronous; wait for the file and for its
|
||||
# trailing "Dump complete" marker.
|
||||
deadline_marker = "; Dump complete"
|
||||
for _ in range(50):
|
||||
if dump_path.exists():
|
||||
text = dump_path.read_text()
|
||||
if deadline_marker in text:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise AssertionError(f"{dump_path} never contained {deadline_marker!r}")
|
||||
|
||||
# Collect every SIG line for the owner from the dump. Format is:
|
||||
# <owner>. <ttl> IN SIG <covered> <alg> <labels> ...
|
||||
sig_lines = []
|
||||
for line in text.splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) < 6:
|
||||
continue
|
||||
if not fields[0].lower().startswith("host.sigaxfr.nil"):
|
||||
continue
|
||||
if fields[2] != "IN" or fields[3] != "SIG":
|
||||
continue
|
||||
sig_lines.append(fields)
|
||||
|
||||
assert (
|
||||
len(sig_lines) == 2
|
||||
), f"expected 2 SIG records at {owner}, got {len(sig_lines)}: {sig_lines}"
|
||||
|
||||
ttl_by_covers = {fields[4]: int(fields[1]) for fields in sig_lines}
|
||||
assert ttl_by_covers == {"A": 600, "MX": 1200}, (
|
||||
f"SIG records lost their covers/TTL binding: {ttl_by_covers}. With "
|
||||
"the Finding 1 bug both records are filed under typepair (SIG, 0) "
|
||||
"and share the first-seen TTL (600)."
|
||||
)
|
||||
|
||||
|
||||
def test_tcp_self_nxt_record(ns6):
|
||||
"""NXT (type 30) updates must be refused at the front door.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue