2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-30 05:57:52 +00:00

[9.20] new: test: Handle alias records in zone files loaded by AsyncDnsServer

dnspython does not treat CNAME records in zone files in any special way;
they are just RRsets belonging to zone nodes.  Process CNAMEs when
preparing zone-based responses just like a normal authoritative DNS
server would.

Adding proper DNAME support to AsyncDnsServer would add complexity to
its code for little gain: DNAME use in custom system test servers is
limited to crafting responses that attempt to trigger bugs in named.

This fact will not be obvious to AsyncDnsServer users as it
automatically loads all zone files it finds and handles CNAME records
like a normal authoritative DNS server would.

Therefore, to prevent surprises:

- raise an exception whenever DNAME records are found in any of the
zone files loaded by AsyncDnsServer,

- add a new optional argument to the AsyncDnsServer constructor that
enables suppressing this new behavior, enabling zones with DNAME
records to be loaded anyway.

This enables response handlers to use the DNAME records present in zone
files in arbitrary ways without complicating the "base" code.

Backport of MR !10409

Merge branch 'backport-michal/asyncserver-alias-records-9.20' into 'bind-9.20'

See merge request isc-projects/bind9!10525
This commit is contained in:
Michał Kępień 2025-05-30 16:22:54 +00:00
commit 00ad3b70ae

View File

@ -266,11 +266,16 @@ class QueryContext:
soa: Optional[dns.rrset.RRset] = None soa: Optional[dns.rrset.RRset] = None
node: Optional[dns.node.Node] = None node: Optional[dns.node.Node] = None
answer: Optional[dns.rdataset.Rdataset] = None answer: Optional[dns.rdataset.Rdataset] = None
alias: Optional[dns.name.Name] = None
@property @property
def qname(self) -> dns.name.Name: def qname(self) -> dns.name.Name:
return self.query.question[0].name return self.query.question[0].name
@property
def current_qname(self) -> dns.name.Name:
return self.alias or self.qname
@property @property
def qclass(self) -> RdataClass: def qclass(self) -> RdataClass:
return self.query.question[0].rdclass return self.query.question[0].rdclass
@ -528,14 +533,14 @@ class AsyncDnsServer(AsyncServer):
response from scratch, without using zone data at all. response from scratch, without using zone data at all.
""" """
def __init__(self, load_zones: bool = True): def __init__(self, acknowledge_manual_dname_handling: bool = False) -> None:
super().__init__(self._handle_udp, self._handle_tcp, "ans.pid") super().__init__(self._handle_udp, self._handle_tcp, "ans.pid")
self._zone_tree: _ZoneTree = _ZoneTree() self._zone_tree: _ZoneTree = _ZoneTree()
self._response_handlers: List[ResponseHandler] = [] self._response_handlers: List[ResponseHandler] = []
self._acknowledge_manual_dname_handling = acknowledge_manual_dname_handling
if load_zones: self._load_zones()
self._load_zones()
def install_response_handler( def install_response_handler(
self, handler: ResponseHandler, prepend: bool = False self, handler: ResponseHandler, prepend: bool = False
@ -568,11 +573,31 @@ class AsyncDnsServer(AsyncServer):
entry_path = pathlib.Path(entry.path) entry_path = pathlib.Path(entry.path)
if entry_path.suffix != ".db": if entry_path.suffix != ".db":
continue continue
origin = dns.name.from_text(entry_path.stem) zone = self._load_zone(entry_path)
logging.info("Loading zone file %s", entry_path)
zone = dns.zone.from_file(entry.path, origin, relativize=False)
self._zone_tree.add(zone) self._zone_tree.add(zone)
def _load_zone(self, zone_file_path: pathlib.Path) -> dns.zone.Zone:
origin = dns.name.from_text(zone_file_path.stem)
logging.info("Loading zone file %s", zone_file_path)
with open(zone_file_path, encoding="utf-8") as zone_file:
zone = dns.zone.from_file(zone_file, origin, relativize=False)
self._abort_if_dname_found_unless_acknowledged(zone)
return zone
def _abort_if_dname_found_unless_acknowledged(self, zone: dns.zone.Zone) -> None:
if self._acknowledge_manual_dname_handling:
return
error = f'DNAME records found in zone "{zone.origin}"; '
error += "this server does not handle DNAME in a standards-compliant way; "
error += "pass `acknowledge_manual_dname_handling=True` to the "
error += "AsyncDnsServer constructor to acknowledge this and load zone anyway"
for node in zone.nodes.values():
for rdataset in node:
if rdataset.rdtype == dns.rdatatype.DNAME:
raise ValueError(error)
async def _handle_udp( async def _handle_udp(
self, wire: bytes, addr: Tuple[str, int], transport: asyncio.DatagramTransport self, wire: bytes, addr: Tuple[str, int], transport: asyncio.DatagramTransport
) -> None: ) -> None:
@ -580,6 +605,7 @@ class AsyncDnsServer(AsyncServer):
peer = Peer(addr[0], addr[1]) peer = Peer(addr[0], addr[1])
responses = self._handle_query(wire, peer, DnsProtocol.UDP) responses = self._handle_query(wire, peer, DnsProtocol.UDP)
async for response in responses: async for response in responses:
logging.debug("Sending UDP message: %s", response.hex())
transport.sendto(response, addr) transport.sendto(response, addr)
async def _handle_tcp( async def _handle_tcp(
@ -672,6 +698,7 @@ class AsyncDnsServer(AsyncServer):
) -> None: ) -> None:
responses = self._handle_query(wire, peer, DnsProtocol.TCP) responses = self._handle_query(wire, peer, DnsProtocol.TCP)
async for response in responses: async for response in responses:
logging.debug("Sending TCP response: %s", response.hex())
writer.write(response) writer.write(response)
await writer.drain() await writer.drain()
@ -807,23 +834,28 @@ class AsyncDnsServer(AsyncServer):
if self._nxdomain_response(qctx): if self._nxdomain_response(qctx):
return return
if self._cname_response(qctx):
return
if self._nodata_response(qctx): if self._nodata_response(qctx):
return return
self._noerror_response(qctx) self._noerror_response(qctx)
def _refused_response(self, qctx: QueryContext) -> bool: def _refused_response(self, qctx: QueryContext) -> bool:
qctx.zone = self._zone_tree.find_best_zone(qctx.qname) zone = self._zone_tree.find_best_zone(qctx.current_qname)
if qctx.zone: if zone:
qctx.zone = zone
return False return False
qctx.response.set_rcode(dns.rcode.REFUSED) if not qctx.response.answer:
qctx.response.set_rcode(dns.rcode.REFUSED)
return True return True
def _delegation_response(self, qctx: QueryContext) -> bool: def _delegation_response(self, qctx: QueryContext) -> bool:
assert qctx.zone assert qctx.zone
name = qctx.qname name = qctx.current_qname
delegation = None delegation = None
while name != qctx.zone.origin: while name != qctx.zone.origin:
@ -868,9 +900,9 @@ class AsyncDnsServer(AsyncServer):
qctx.soa = qctx.zone.find_rrset(qctx.zone.origin, dns.rdatatype.SOA) qctx.soa = qctx.zone.find_rrset(qctx.zone.origin, dns.rdatatype.SOA)
assert qctx.soa assert qctx.soa
qctx.node = qctx.zone.get_node(qctx.qname) qctx.node = qctx.zone.get_node(qctx.current_qname)
if qctx.node or not any( if qctx.node or not any(
n for n in qctx.zone.nodes if n.is_subdomain(qctx.qname) n for n in qctx.zone.nodes if n.is_subdomain(qctx.current_qname)
): ):
return False return False
@ -888,6 +920,21 @@ class AsyncDnsServer(AsyncServer):
qctx.response.authority.append(qctx.soa) qctx.response.authority.append(qctx.soa)
return True return True
def _cname_response(self, qctx: QueryContext) -> bool:
assert qctx.node
cname = qctx.node.get_rdataset(qctx.qclass, dns.rdatatype.CNAME)
if not cname:
return False
cname_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, cname.rdtype)
cname_rrset.update(cname)
qctx.response.answer.append(cname_rrset)
qctx.alias = cname[0].target
self._prepare_response_from_zone_data(qctx)
return True
def _nodata_response(self, qctx: QueryContext) -> bool: def _nodata_response(self, qctx: QueryContext) -> bool:
assert qctx.node assert qctx.node
assert qctx.soa assert qctx.soa
@ -897,13 +944,14 @@ class AsyncDnsServer(AsyncServer):
return False return False
qctx.response.set_rcode(dns.rcode.NOERROR) qctx.response.set_rcode(dns.rcode.NOERROR)
qctx.response.authority.append(qctx.soa) if not qctx.response.answer:
qctx.response.authority.append(qctx.soa)
return True return True
def _noerror_response(self, qctx: QueryContext) -> None: def _noerror_response(self, qctx: QueryContext) -> None:
assert qctx.answer assert qctx.answer
answer_rrset = dns.rrset.RRset(qctx.qname, qctx.qclass, qctx.qtype) answer_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, qctx.qtype)
answer_rrset.update(qctx.answer) answer_rrset.update(qctx.answer)
qctx.response.set_rcode(dns.rcode.NOERROR) qctx.response.set_rcode(dns.rcode.NOERROR)