Use isctest.asyncserver in the "ixfr" system test

Replace the usage of the `bin/tests/system/ans.pl` server with an
instance of ControllableAsyncServer.
This commit is contained in:
Štěpán Balážik 2025-12-23 14:41:18 +01:00
parent 2302fe1235
commit 46ecbbed0a
4 changed files with 276 additions and 88 deletions

View file

@ -0,0 +1,265 @@
"""
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.
"""
import abc
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rrset
from typing import AsyncGenerator, Collection, Iterable
from isctest.asyncserver import (
ControllableAsyncDnsServer,
DnsResponseSend,
QueryContext,
ResponseHandler,
SwitchControlCommand,
)
def rrset(owner: str, rdtype: dns.rdatatype.RdataType, rdata: str) -> dns.rrset.RRset:
return dns.rrset.from_text(
owner,
300,
dns.rdataclass.IN,
rdtype,
rdata,
)
def soa(serial: int, *, owner: str = "nil.") -> dns.rrset.RRset:
return rrset(
owner,
dns.rdatatype.SOA,
f"ns.nil. root.nil. {serial} 300 300 604800 300",
)
def ns() -> dns.rrset.RRset:
return rrset(
"nil.",
dns.rdatatype.NS,
"ns.nil.",
)
def a(address: str, *, owner: str) -> dns.rrset.RRset:
return rrset(
owner,
dns.rdatatype.A,
address,
)
def txt(data: str, *, owner: str = "nil.") -> dns.rrset.RRset:
return rrset(
owner,
dns.rdatatype.TXT,
f'"{data}"',
)
class SoaHandler(ResponseHandler):
def __init__(self, serial: int):
self._serial = serial
def match(self, qctx: QueryContext) -> bool:
return qctx.qtype == dns.rdatatype.SOA
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[DnsResponseSend, None]:
qctx.response.answer.append(soa(self._serial))
yield DnsResponseSend(qctx.response)
class AxfrHandler(ResponseHandler):
@property
@abc.abstractmethod
def answers(self) -> Iterable[Collection[dns.rrset.RRset]]:
"""
Answer sections of response packets sent in response to
AXFR queries.
"""
raise NotImplementedError
def match(self, qctx: QueryContext) -> bool:
return qctx.qtype == dns.rdatatype.AXFR
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[DnsResponseSend, None]:
for answer in self.answers:
response = qctx.prepare_new_response()
for rrset_ in answer:
response.answer.append(rrset_)
yield DnsResponseSend(response)
class IxfrHandler(ResponseHandler):
@property
@abc.abstractmethod
def answer(self) -> Collection[dns.rrset.RRset]:
"""
Answer section of a response packet sent in response to
IXFR queries.
"""
raise NotImplementedError
def match(self, qctx: QueryContext) -> bool:
return qctx.qtype == dns.rdatatype.IXFR
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[DnsResponseSend, None]:
for rrset_ in self.answer:
qctx.response.answer.append(rrset_)
yield DnsResponseSend(qctx.response)
class InitialAfxrHandler(AxfrHandler):
answers = (
(soa(1),),
(
ns(),
txt("initial AXFR"),
a("10.0.0.61", owner="a.nil."),
a("10.0.0.62", owner="b.nil."),
),
(soa(1),),
)
class SuccessfulIfxrHandler(IxfrHandler):
answer = (
soa(3),
soa(1),
a("10.0.0.61", owner="a.nil."),
txt("initial AXFR"),
soa(2),
txt("successful IXFR"),
a("10.0.1.61", owner="a.nil."),
soa(2),
soa(3),
soa(3),
)
class NotExactIxfrHandler(IxfrHandler):
answer = (
soa(4),
soa(3),
txt("delete-nonexistent-txt-record"),
soa(4),
txt("this-txt-record-would-be-added"),
soa(4),
)
class FallbackNotExactAxfrHandler(AxfrHandler):
answers = (
(soa(3),),
(
ns(),
txt("fallback AXFR"),
),
(soa(3),),
)
class TooManyRecordsIxfrHandler(IxfrHandler):
answer = (
soa(4),
soa(3),
soa(4),
txt("text 1"),
txt("text 2"),
txt("text 3"),
txt("text 4"),
txt("text 5"),
txt("text 6: causing too many records"),
soa(4),
)
class FallbackTooManyRecordsAxfrHandler(AxfrHandler):
answers = (
(
soa(3),
ns(),
txt("fallback AXFR on too many records"),
),
(soa(3),),
)
class BadSoaOwnerIxfrHandler(IxfrHandler):
answer = (
soa(4),
soa(3),
soa(4, owner="bad-owner."),
txt("serial 4, malformed IXFR", owner="test.nil."),
soa(4),
)
class FallbackBadSoaOwnerAxfrHandler(AxfrHandler):
answers = (
(soa(4),),
(
ns(),
txt("serial 4, fallback AXFR", owner="test.nil."),
),
(soa(4),),
)
def main() -> None:
server = ControllableAsyncDnsServer(
default_aa=True, default_rcode=dns.rcode.NOERROR
)
switch_command = SwitchControlCommand(
{
"initial_axfr": (
SoaHandler(1),
InitialAfxrHandler(),
),
"successful_ixfr": (
SoaHandler(3),
SuccessfulIfxrHandler(),
),
"not_exact": (
SoaHandler(4),
NotExactIxfrHandler(),
FallbackNotExactAxfrHandler(),
),
"too_many_records": (
SoaHandler(4),
TooManyRecordsIxfrHandler(),
FallbackTooManyRecordsAxfrHandler(),
),
"bad_soa_owner": (
SoaHandler(4),
BadSoaOwnerIxfrHandler(),
FallbackBadSoaOwnerAxfrHandler(),
),
}
)
server.install_control_command(switch_command)
server.run()
if __name__ == "__main__":
main()

View file

@ -32,27 +32,16 @@ n=0
DIGOPTS="+tcp +noadd +nosea +nostat +noquest +nocomm +nocmd -p ${PORT}"
RNDCCMD="$RNDC -p ${CONTROLPORT} -c ../_common/rndc.conf -s"
sendcmd() {
send 10.53.0.2 "${EXTRAPORT1}"
switch_responses() {
RESPONSES_KEY="${1}"
$DIG $DIGOPTS "@10.53.0.2" "${RESPONSES_KEY}.switch._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1
}
n=$((n + 1))
echo_i "testing initial AXFR ($n)"
ret=0
sendcmd <<EOF
/SOA/
nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300
/AXFR/
nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300
/AXFR/
nil. 300 NS ns.nil.
nil. 300 TXT "initial AXFR"
a.nil. 60 A 10.0.0.61
b.nil. 60 A 10.0.0.62
/AXFR/
nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300
EOF
switch_responses "initial_axfr"
sleep 1
@ -84,21 +73,7 @@ ret=0
# We change the IP address of a.nil., and the TXT record at the apex.
# Then we do a SOA-only update.
sendcmd <<EOF
/SOA/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
/IXFR/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300
a.nil. 60 A 10.0.0.61
nil. 300 TXT "initial AXFR"
nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300
nil. 300 TXT "successful IXFR"
a.nil. 60 A 10.0.1.61
nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
EOF
switch_responses "successful_ixfr"
sleep 1
@ -115,25 +90,7 @@ echo_i "testing AXFR fallback after IXFR failure (not exact error) ($n)"
ret=0
# Provide a broken IXFR response and a working fallback AXFR response
sendcmd <<EOF
/SOA/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/IXFR/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
nil. 300 TXT "delete-nonexistent-txt-record"
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
nil. 300 TXT "this-txt-record-would-be-added"
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/AXFR/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
/AXFR/
nil. 300 NS ns.nil.
nil. 300 TXT "fallback AXFR"
/AXFR/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
EOF
switch_responses "not_exact"
sleep 1
@ -150,28 +107,7 @@ echo_i "testing AXFR fallback after IXFR failure (too many records) ($n)"
ret=0
# Provide an IXFR response that would cause a "too many records" condition
sendcmd <<EOF
/SOA/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/IXFR/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
nil. 300 TXT "text 1"
nil. 300 TXT "text 2"
nil. 300 TXT "text 3"
nil. 300 TXT "text 4"
nil. 300 TXT "text 5"
nil. 300 TXT "text 6: causing too many records"
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/AXFR/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
nil. 300 NS ns.nil.
nil. 300 TXT "fallback AXFR on too many records"
/AXFR/
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
EOF
switch_responses "too_many_records"
sleep 1
@ -196,23 +132,7 @@ ret=0
nextpart ns1/named.run >/dev/null
# Provide a broken IXFR response and a working fallback AXFR response.
sendcmd <<EOF
/SOA/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/IXFR/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300
bad-owner. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
test.nil. 300 TXT "serial 4, malformed IXFR"
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/AXFR/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
/AXFR/
nil. 300 NS ns.nil.
test.nil. 300 TXT "serial 4, fallback AXFR"
/AXFR/
nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300
EOF
switch_responses "bad_soa_owner"
$RNDCCMD 10.53.0.1 refresh nil | sed 's/^/ns1 /' | cat_i
# A broken server would accept the malformed IXFR and apply its contents to the

View file

@ -11,6 +11,9 @@
import pytest
# isctest.asyncserver requires dnspython >= 2.0.0
pytest.importorskip("dns", minversion="2.0.0")
pytestmark = pytest.mark.extra_artifacts(
[
"dig.out*",