Test named reconfiguration during zone transfer's SOA request

This new test checks that named can correctly process an interrupted
SOA request during zone transfer, caused by reconfiguration.

Co-authored-by: Michał Kępień <michal@isc.org>
This commit is contained in:
Aram Sargsyan 2025-05-21 14:57:47 +00:00 committed by Ondřej Surý
parent b07ec4f0b3
commit aa6ca3e776
4 changed files with 162 additions and 0 deletions

View file

@ -0,0 +1,111 @@
"""
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.
"""
from typing import AsyncGenerator
import dns.message
import dns.rdataclass
import dns.rdatatype
import dns.rrset
from isctest.asyncserver import (
ControllableAsyncDnsServer,
DomainHandler,
QueryContext,
ResponseAction,
DnsResponseSend,
ToggleResponsesCommand,
)
class AXFRServer(DomainHandler):
"""
Yield SOA and AXFR responses. Every new AXFR response increments the SOA
version.
"""
domains = ["xfr-and-reconfig"]
def __init__(self) -> None:
super().__init__()
self.soa_version = 0
async def get_responses(
self, qctx: QueryContext
) -> AsyncGenerator[ResponseAction, None]:
# This is oversimplified because I am lazy - we are appending the SOA
# RRset to the ANSWER section for _every_ QTYPE. named is only
# expected to send a SOA query over UDP and then an AXFR query over
# TCP. Responses to both of those start with a SOA RRset in the ANSWER
# section :-)
soa_message = dns.message.make_response(qctx.query)
soa_rrset = dns.rrset.from_text(
qctx.qname,
300,
dns.rdataclass.IN,
dns.rdatatype.SOA,
f". . {self.soa_version} 0 0 0 0",
)
soa_message.answer.append(soa_rrset)
yield DnsResponseSend(soa_message, authoritative=True)
if qctx.qtype == dns.rdatatype.SOA:
# If QTYPE=SOA, the SOA record is the complete response.
return
if qctx.qtype != dns.rdatatype.AXFR:
# If QTYPE=AXFR, we will continue cramming RRsets into the ANSWER
# section of a subsequent DNS message below.
#
# If QTYPE was not SOA or AXFR, abort. Yeah, we just sent a broken
# response by yielding DnsResponseSend() with a SOA RRset in the
# ANSWER section above. We will have to carry that burden for the
# rest of our lives.
return
# Send just the obligatory NS RRset at zone apex in the next message.
# This is stupidly inefficient, but makes looping below simpler as we
# will already have been done with the mandatory stuff by then.
ns_message = dns.message.make_response(qctx.query)
ns_rrset = dns.rrset.from_text(
qctx.qname, 300, dns.rdataclass.IN, dns.rdatatype.NS, "."
)
ns_message.answer.append(ns_rrset)
yield DnsResponseSend(ns_message, authoritative=True)
# Generate the AXFR with a txt rrset.
txt_message = dns.message.make_response(qctx.query)
txt_rrset = dns.rrset.from_text(
qctx.qname,
300,
dns.rdataclass.IN,
dns.rdatatype.TXT,
"foo bar",
)
txt_message.answer.append(txt_rrset)
yield DnsResponseSend(txt_message, authoritative=True)
# Finish the AXFR transaction by sending the second SOA RRset.
yield DnsResponseSend(soa_message, authoritative=True)
# This makes sure that the next SOA request causes a new zone transfer
self.soa_version += 1
if __name__ == "__main__":
server = ControllableAsyncDnsServer([ToggleResponsesCommand])
server.install_response_handler(AXFRServer())
server.run()

View file

@ -111,3 +111,10 @@ zone "ixfr-too-many-diffs" {
primaries { 10.53.0.1; };
file "ixfr-too-many-diffs.bk";
};
zone "xfr-and-reconfig" {
type secondary;
primaries { 10.53.0.9; };
file "xfr-and-reconfig.bk";
request-ixfr no; # ans9 supports only axfr
};

View file

@ -19,6 +19,10 @@ DIGOPTS="+tcp +noadd +nosea +nostat +noquest +nocomm +nocmd -p ${PORT}"
RNDCCMD="$RNDC -c ../_common/rndc.conf -p ${CONTROLPORT} -s"
NS_PARAMS="-m record -c named.conf -d 99 -g -T maxcachesize=2097152"
dig_with_opts() (
"$DIG" -p "$PORT" "$@"
)
status=0
n=0
@ -752,5 +756,44 @@ if [ $tmp -eq 0 ]; then
fi
status=$((status + tmp))
nextpart ns6/named.run >/dev/null
sendcmd() (
dig_with_opts "@${1}" "${2}._control." TXT +time=5 +tries=1 +tcp >/dev/null 2>&1
)
# See #5307#note_558185
n=$((n + 1))
echo_i "test reconfiguration when zone transfer is in the middle of a SOA query (part 1) ($n)"
tmp=0
# Check that xfr-and-reconfig has been successfully transferred by the secondary.
grep -F 'zone xfr-and-reconfig/IN: zone transfer finished: success' ns6/named.run 2>&1 >/dev/null || tmp=0
# Make ans6 receive queries without responding to them.
sendcmd 10.53.0.9 "disable.send-responses"
sleep 1
# Try to reload the zone from an unresponsive primary.
$RNDCCMD 10.53.0.6 reload xfr-and-reconfig 2>&1 | sed 's/^/ns6 /' | cat_i
sleep 1
# Reconfigure named while zone transfer attempt is in progress.
$RNDCCMD 10.53.0.6 reconfig 2>&1 | sed 's/^/ns6 /' | cat_i
# Confirm that the ongoing SOA request was canceled, caused by the reconfiguratoin.
retry_quiet 60 wait_for_message "refresh: request result: shutting down" || tmp=1
if test $tmp != 0; then echo_i "failed"; fi
status=$((status + tmp))
nextpart ns6/named.run >/dev/null
n=$((n + 1))
echo_i "test reconfiguration when zone transfer is in the middle of a SOA query (part 2) ($n)"
tmp=0
# Make ans6 receive queries and respond to them.
sendcmd 10.53.0.9 "enable.send-responses"
sleep 1
# Try to reload the zone from the primary.
$RNDCCMD 10.53.0.6 reload xfr-and-reconfig 2>&1 | sed 's/^/ns6 /' | cat_i
retry_quiet 60 wait_for_message "zone xfr-and-reconfig/IN: Transfer started." || tmp=1
if test $tmp != 0; then echo_i "failed"; fi
status=$((status + tmp))
echo_i "exit status: $status"
[ $status -eq 0 ] || exit 1

View file

@ -51,6 +51,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns6/primary.db.jnl",
"ns6/sec.bk",
"ns6/xot-primary-try-next.bk",
"ns6/xfr-and-reconfig.bk",
"ns7/edns-expire.bk",
"ns7/primary2.db",
"ns7/sec.bk",