From 3a6b977a924d460e6f3d0a05fded6f6d5bf498e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Thu, 27 Nov 2025 17:48:16 +0100 Subject: [PATCH 1/8] Remove unused digdelv/ans6 server This server receives no queries during the test and doesn't affect the test outcome. --- bin/tests/system/digdelv/ans6/ans.pl | 84 ---------------------------- 1 file changed, 84 deletions(-) delete mode 100755 bin/tests/system/digdelv/ans6/ans.pl diff --git a/bin/tests/system/digdelv/ans6/ans.pl b/bin/tests/system/digdelv/ans6/ans.pl deleted file mode 100755 index 39d02b2c69..0000000000 --- a/bin/tests/system/digdelv/ans6/ans.pl +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/perl -w - -# 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. - -use IO::File; -use IO::Socket; -use Net::DNS; -use Net::DNS::Packet; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $sock = IO::Socket::INET->new(LocalAddr => "10.53.0.6", - LocalPort => $localport, Proto => "udp") or die "$!"; - -my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; -print $pidf "$$\n" or die "cannot write pid file: $!"; -$pidf->close or die "cannot close pid file: $!"; -sub rmpid { unlink "ans.pid"; exit 1; }; - -$SIG{INT} = \&rmpid; -$SIG{TERM} = \&rmpid; - -for (;;) { - $sock->recv($buf, 512); - - print "**** request from " , $sock->peerhost, " port ", $sock->peerport, "\n"; - - my $packet; - - if ($Net::DNS::VERSION > 0.68) { - $packet = new Net::DNS::Packet(\$buf, 0); - $@ and die $@; - } else { - my $err; - ($packet, $err) = new Net::DNS::Packet(\$buf, 0); - $err and die $err; - } - - print "REQUEST:\n"; - $packet->print; - - $packet->header->qr(1); - - my @questions = $packet->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - - my $donotrespond = 0; - - $packet->header->aa(1); - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 10.53.0.5")); - } else { - $donotrespond = 1; - } - - if ($donotrespond == 0) { - my $sendsock = - IO::Socket::INET->new(LocalAddr => "10.53.1.2", - PeerAddr => $sock->peerhost, - PeerPort => $sock->peerport, - Proto => "udp") or die "$!"; - print "**** response from ", $sendsock->sockhost, " to " , - $sendsock->peerhost, " port ", $sendsock->peerport, "\n"; - $sendsock->send($packet->data); - $sendsock->close; - print "RESPONSE:\n"; - $packet->print; - print "\n"; - } else { - print "DROP:\n"; - } -} From 648f2534b1579c3a47c4a223c89db5ef9933c0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Thu, 27 Nov 2025 18:12:55 +0100 Subject: [PATCH 2/8] Replace digdelv/ans4 with AsyncDnsServer Configure the AsyncDnsServer to ignore all queries to ensure the same behaviour as with "//" command for ans.pl. --- bin/tests/system/digdelv/ans4/ans.py | 25 +++++++++++++++++++++++++ bin/tests/system/digdelv/ans4/startme | 1 - bin/tests/system/digdelv/tests.sh | 2 -- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 bin/tests/system/digdelv/ans4/ans.py delete mode 100644 bin/tests/system/digdelv/ans4/startme diff --git a/bin/tests/system/digdelv/ans4/ans.py b/bin/tests/system/digdelv/ans4/ans.py new file mode 100644 index 0000000000..6f5346ff86 --- /dev/null +++ b/bin/tests/system/digdelv/ans4/ans.py @@ -0,0 +1,25 @@ +# 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 isctest.asyncserver import ( + AsyncDnsServer, + IgnoreAllQueries, +) + + +def main() -> None: + server = AsyncDnsServer() + server.install_response_handler(IgnoreAllQueries()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/digdelv/ans4/startme b/bin/tests/system/digdelv/ans4/startme deleted file mode 100644 index 8b13789179..0000000000 --- a/bin/tests/system/digdelv/ans4/startme +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index 775bca31f4..aa49344ee6 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -382,8 +382,6 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking dig preserves origin on TCP retries ($n)" ret=0 - # Ask ans4 to still accept TCP connections, but not respond to queries - echo "//" | sendcmd 10.53.0.4 dig_with_opts -d +tcp @10.53.0.4 +retry=1 +time=1 +domain=bar foo >dig.out.test$n 2>&1 && ret=1 test "$(grep -c "trying origin bar" dig.out.test$n)" -eq 2 || ret=1 grep "using root origin" /dev/null && ret=1 From c50a7d2de198a83692d144e5b5b7e4394e1563a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 28 Nov 2025 11:19:24 +0100 Subject: [PATCH 3/8] Replace digdelv/ans7 with AsyncDnsServer ans7 server always replies with an UPDATE opcode in the message. --- bin/tests/system/digdelv/ans7/ans.pl | 68 --------------------- bin/tests/system/digdelv/ans7/ans.py | 40 +++++++++++++ bin/tests/system/digdelv/tests.sh | 88 ++++++++++------------------ 3 files changed, 70 insertions(+), 126 deletions(-) delete mode 100755 bin/tests/system/digdelv/ans7/ans.pl create mode 100644 bin/tests/system/digdelv/ans7/ans.py diff --git a/bin/tests/system/digdelv/ans7/ans.pl b/bin/tests/system/digdelv/ans7/ans.pl deleted file mode 100755 index a7aa60eb9d..0000000000 --- a/bin/tests/system/digdelv/ans7/ans.pl +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/perl -w - -# 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. - -use IO::File; -use IO::Socket; -use Net::DNS; -use Net::DNS::Packet; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $sock = IO::Socket::INET->new(LocalAddr => "10.53.0.7", - LocalPort => $localport, Proto => "udp") or die "$!"; - -my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; -print $pidf "$$\n" or die "cannot write pid file: $!"; -$pidf->close or die "cannot close pid file: $!"; -sub rmpid { unlink "ans.pid"; exit 1; }; - -$SIG{INT} = \&rmpid; -$SIG{TERM} = \&rmpid; - -STDOUT->autoflush(1); - -print "Net::DNS::VERSION => $Net::DNS::VERSION\n"; - -for (;;) { - $sock->recv($buf, 512); - - print "**** request from " , $sock->peerhost, " port ", $sock->peerport, "\n"; - - my $packet; - - if ($Net::DNS::VERSION > 0.68) { - $packet = new Net::DNS::Packet(\$buf, 0); - $@ and die $@; - } else { - my $err; - ($packet, $err) = new Net::DNS::Packet(\$buf, 0); - $err and die $err; - } - - print "REQUEST:\n"; - $packet->print; - - $packet->header->qr(1); - $packet->header->opcode(5); - - my @questions = $packet->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - $packet->push("update", rr_del("$qname SOA")); - - print "RESPONSE:\n"; - $packet->print; - - $sock->send($packet->data); -} diff --git a/bin/tests/system/digdelv/ans7/ans.py b/bin/tests/system/digdelv/ans7/ans.py new file mode 100644 index 0000000000..77e7141590 --- /dev/null +++ b/bin/tests/system/digdelv/ans7/ans.py @@ -0,0 +1,40 @@ +# 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.opcode +import dns.rcode + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + ResponseHandler, + QueryContext, +) + + +class ReplyUpdateHandler(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_opcode(dns.opcode.UPDATE) + yield DnsResponseSend(qctx.response) + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handler(ReplyUpdateHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index aa49344ee6..ec356dc7f4 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -71,72 +71,44 @@ NOSPLIT="$(sed /dev/null && HAS_PYYAML=1 -# -# test whether ans7/ans.pl will be able to send a UPDATE response. -# if it can't, we will log that below. -# -if "$PERL" -e 'use Net::DNS; use Net::DNS::Packet; my $p = new Net::DNS::Packet; $p->header->opcode(5);' >/dev/null 2>&1; then - checkupdate=1 -else - checkupdate=0 -fi +n=$((n + 1)) +echo_i "check nslookup handles UPDATE response ($n)" +ret=0 +"$NSLOOKUP" -q=CNAME -timeout=1 "-port=$PORT" foo.bar 10.53.0.7 >nslookup.out.test$n 2>&1 && ret=1 +grep "Opcode mismatch" nslookup.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) -if [ -x "$NSLOOKUP" -a $checkupdate -eq 1 ]; then +n=$((n + 1)) +echo_i "check host handles UPDATE response ($n)" +ret=0 +"$HOST" -W 1 -t CNAME -p $PORT foo.bar 10.53.0.7 >host.out.test$n 2>&1 && ret=1 +grep "Opcode mismatch" host.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) - n=$((n + 1)) - echo_i "check nslookup handles UPDATE response ($n)" - ret=0 - "$NSLOOKUP" -q=CNAME -timeout=1 "-port=$PORT" foo.bar 10.53.0.7 >nslookup.out.test$n 2>&1 && ret=1 - grep "Opcode mismatch" nslookup.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - -fi - -if [ -x "$HOST" -a $checkupdate -eq 1 ]; then - - n=$((n + 1)) - echo_i "check host handles UPDATE response ($n)" - ret=0 - "$HOST" -W 1 -t CNAME -p $PORT foo.bar 10.53.0.7 >host.out.test$n 2>&1 && ret=1 - grep "Opcode mismatch" host.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - -fi - -if [ -x "$NSUPDATE" -a $checkupdate -eq 1 ]; then - - n=$((n + 1)) - echo_i "check nsupdate handles UPDATE response to QUERY ($n)" - ret=0 - res=0 - $NSUPDATE <nsupdate.out.test$n 2>&1 || res=$? +n=$((n + 1)) +echo_i "check nsupdate handles UPDATE response to QUERY ($n)" +ret=0 +res=0 +$NSUPDATE <nsupdate.out.test$n 2>&1 || res=$? server 10.53.0.7 ${PORT} add x.example.com 300 in a 1.2.3.4 send EOF - test $res -eq 1 || ret=1 - grep "invalid OPCODE in response to SOA query" nsupdate.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - -fi +test $res -eq 1 || ret=1 +grep "invalid OPCODE in response to SOA query" nsupdate.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) if [ -x "$DIG" ]; then - - if [ $checkupdate -eq 1 ]; then - - n=$((n + 1)) - echo_i "check dig handles UPDATE response ($n)" - ret=0 - dig_with_opts @10.53.0.7 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1 - grep "Opcode mismatch" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - else - echo_i "Skipped UPDATE handling test" - fi + n=$((n + 1)) + echo_i "check dig handles UPDATE response ($n)" + ret=0 + dig_with_opts @10.53.0.7 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1 + grep "Opcode mismatch" dig.out.test$n >/dev/null || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status + ret)) n=$((n + 1)) echo_i "checking dig short form works ($n)" From 20887ff80f18e28bd5404f40488151c855f26d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 28 Nov 2025 15:34:38 +0100 Subject: [PATCH 4/8] Rename ResponseDropAndCloseConnection action The action can be used to close the connection even after some response was sent, depending on the ordering of actions in the handler that uses it. Rename it to CloseConnection to use a more fitting name. --- bin/tests/system/isctest/asyncserver.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bin/tests/system/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index eaf9ded26c..86de57cf71 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -429,10 +429,9 @@ class _ConnectionTeardownRequested(Exception): @dataclass -class ResponseDropAndCloseConnection(ResponseAction): +class CloseConnection(ResponseAction): """ - Action which makes the server close the connection after the DNS query is - received by the server (TCP only). + Action which makes the server close the connection (TCP only). The connection may be closed with a delay if requested. """ @@ -555,8 +554,8 @@ class ConnectionReset(ConnectionHandler): make the server send an RST segment; this happens when the server closes a client's socket while there is still unread data in that socket's buffer. If closing the connection _after_ the query is read by the server is enough - for a given use case, the ResponseDropAndCloseConnection response handler - should be used instead. + for a given use case, the CloseConnection response handler should be used + instead. """ delay: float = 0.0 From 575f0e39165815566a35a9442e2737b26d45a518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 28 Nov 2025 16:13:43 +0100 Subject: [PATCH 5/8] Replace digdelv/ans5 with ControllableAsyncDnsServer The server has three modes of operation - either no response, a partial AXFR or a complete AXFR. To test the fallback behaviour of dig, these actions are be combined in a specific sequences. To set up the desired server behaviour, use the _control queries for the server. --- bin/tests/system/digdelv/ans5/ans.pl | 176 --------------------------- bin/tests/system/digdelv/ans5/ans.py | 104 ++++++++++++++++ bin/tests/system/digdelv/tests.sh | 24 ++-- 3 files changed, 117 insertions(+), 187 deletions(-) delete mode 100644 bin/tests/system/digdelv/ans5/ans.pl create mode 100644 bin/tests/system/digdelv/ans5/ans.py diff --git a/bin/tests/system/digdelv/ans5/ans.pl b/bin/tests/system/digdelv/ans5/ans.pl deleted file mode 100644 index 63964060d7..0000000000 --- a/bin/tests/system/digdelv/ans5/ans.pl +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/perl - -# 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. - -# This is a TCP-only DNS server whose aim is to facilitate testing how dig -# copes with prematurely closed TCP connections. -# -# This server can be configured (through a separate control socket) with a -# series of responses to send for subsequent incoming TCP DNS queries. Only -# one query is handled before closing each connection. In order to keep things -# simple, the server is not equipped with any mechanism for handling malformed -# queries. -# -# Available response types are defined in the %response_types hash in the -# getAnswerSection() function below. Each RR returned is generated dynamically -# based on the QNAME found in the incoming query. - -use IO::File; -use Net::DNS; -use Net::DNS::Packet; - -use strict; - -# Ignore SIGPIPE so we won't fail if peer closes a TCP socket early -local $SIG{PIPE} = 'IGNORE'; - -# Flush logged output after every line -local $| = 1; - -my $server_addr = "10.53.0.5"; -if (@ARGV > 0) { - $server_addr = @ARGV[0]; -} - -my $mainport = int($ENV{'PORT'}); -if (!$mainport) { $mainport = 5300; } -my $ctrlport = int($ENV{'EXTRAPORT1'}); -if (!$ctrlport) { $ctrlport = 5301; } - -my $ctlsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $ctrlport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -my $tcpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $mainport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; -print $pidf "$$\n" or die "cannot write pid file: $!"; -$pidf->close or die "cannot close pid file: $!";; -sub rmpid { unlink "ans.pid"; exit 1; }; - -$SIG{INT} = \&rmpid; -$SIG{TERM} = \&rmpid; - -my @response_sequence = ("complete_axfr"); -my $connection_counter = 0; - -# Return the next answer type to send, incrementing the connection counter and -# making sure the latter does not exceed the size of the array holding the -# configured response sequence. -sub getNextResponseType { - my $response_type = $response_sequence[$connection_counter]; - - $connection_counter++; - $connection_counter %= scalar(@response_sequence); - - return $response_type; -} - -# Return an array of resource records comprising the answer section of a given -# response type. -sub getAnswerSection { - my ($response_type, $qname) = @_; - - my %response_types = ( - no_response => [], - - partial_axfr => [ - Net::DNS::RR->new("$qname 300 IN SOA . . 0 0 0 0 300"), - Net::DNS::RR->new("$qname NS ."), - ], - - complete_axfr => [ - Net::DNS::RR->new("$qname 300 IN SOA . . 0 0 0 0 300"), - Net::DNS::RR->new("$qname NS ."), - Net::DNS::RR->new("$qname 300 IN SOA . . 0 0 0 0 300"), - ], - ); - - return $response_types{$response_type}; -} - - -# Generate a Net::DNS::Packet containing the response to send on the current -# TCP connection. If the answer section of the response is determined to be -# empty, no data will be sent on the connection at all (immediate EOF). -sub generateResponse { - my ($buf) = @_; - my $request; - - if ($Net::DNS::VERSION > 0.68) { - $request = new Net::DNS::Packet(\$buf, 0); - $@ and die $@; - } else { - my $err; - ($request, $err) = new Net::DNS::Packet(\$buf, 0); - $err and die $err; - } - - my @questions = $request->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - my $qclass = $questions[0]->qclass; - my $id = $request->header->id; - - my $packet = new Net::DNS::Packet($qname, $qtype, $qclass); - $packet->header->qr(1); - $packet->header->aa(1); - $packet->header->id($id); - - my $response_type = getNextResponseType(); - my $answers = getAnswerSection($response_type, $qname); - for my $rr (@$answers) { - $packet->push("answer", $rr); - } - - print " Sending \"$response_type\" response\n"; - - return $packet->data if @$answers; -} - -my $rin; -my $rout; -for (;;) { - $rin = ''; - vec($rin, fileno($ctlsock), 1) = 1; - vec($rin, fileno($tcpsock), 1) = 1; - - select($rout = $rin, undef, undef, undef); - - if (vec($rout, fileno($ctlsock), 1)) { - my $conn = $ctlsock->accept; - @response_sequence = split(' ', $conn->getline); - $connection_counter = 0; - print "Response sequence set to: @response_sequence\n"; - $conn->close; - } elsif (vec($rout, fileno($tcpsock), 1)) { - my $buf; - my $lenbuf; - my $conn = $tcpsock->accept; - my $n = $conn->sysread($lenbuf, 2); - die unless $n == 2; - my $len = unpack("n", $lenbuf); - $n = $conn->sysread($buf, $len); - die unless $n == $len; - print "TCP request\n"; - my $response = generateResponse($buf); - if ($response) { - $len = length($response); - $n = $conn->syswrite(pack("n", $len), 2); - $n = $conn->syswrite($response, $len); - print " Sent: $n chars via TCP\n"; - } else { - print " No response sent\n"; - } - $conn->close; - } -} diff --git a/bin/tests/system/digdelv/ans5/ans.py b/bin/tests/system/digdelv/ans5/ans.py new file mode 100644 index 0000000000..a53045305d --- /dev/null +++ b/bin/tests/system/digdelv/ans5/ans.py @@ -0,0 +1,104 @@ +# 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 logging +from typing import AsyncGenerator, List, Optional + +import dns.rcode +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + CloseConnection, + ControlCommand, + ControllableAsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseAction, + ResponseHandler, +) + + +class ErraticAxfrHandler(ResponseHandler): + allowed_actions = ["no-response", "partial-axfr", "complete-axfr"] + + def __init__(self, actions: List[str]) -> None: + self.actions = actions + self.counter = 0 + for action in actions: + assert action in self.allowed_actions + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + action = self.actions[self.counter % len(self.actions)] + self.counter += 1 + + logging.info("current response action: %s", action) + + if action == "no-response": + yield CloseConnection() + return + + soa_rr = dns.rrset.from_text( + qctx.qname, 300, qctx.qclass, dns.rdatatype.SOA, ". . 0 0 0 0 300" + ) + ns_rr = dns.rrset.from_text(qctx.qname, 300, qctx.qclass, dns.rdatatype.NS, ".") + + qctx.response.answer.append(soa_rr) + qctx.response.answer.append(ns_rr) + + if action == "partial-axfr": + yield DnsResponseSend(qctx.response) + elif action == "complete-axfr": + qctx.response.answer.append(soa_rr) + yield DnsResponseSend(qctx.response) + yield CloseConnection() + + +class ResponseSequenceCommand(ControlCommand): + control_subdomain = "response-sequence" + + def __init__(self) -> None: + self._current_handler: Optional[ResponseHandler] = None + + def handle( + self, args: List[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str: + for action in args: + if action not in ErraticAxfrHandler.allowed_actions: + logging.error("invalid %s action '%s'", self, action) + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return f"invalid action '{action}'; must be one of {ErraticAxfrHandler.allowed_actions}" + + actions = args + + if self._current_handler is not None: + server.uninstall_response_handler(self._current_handler) + + self._current_handler = ErraticAxfrHandler(actions) + server.install_response_handler(self._current_handler) + + msg = f"reponse sequence set to {actions}" + logging.info(msg) + return msg + + +def main() -> None: + server = ControllableAsyncDnsServer( + default_aa=True, default_rcode=dns.rcode.NOERROR + ) + server.install_control_command(ResponseSequenceCommand()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index ec356dc7f4..eb753349ca 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -19,10 +19,6 @@ set -e status=0 n=0 -sendcmd() { - send "${1}" "$EXTRAPORT1" -} - dig_with_opts() { "$DIG" -p "$PORT" "$@" } @@ -31,6 +27,12 @@ mdig_with_opts() { "$MDIG" -p "$PORT" "$@" } +set_response_sequence() { + SEQUENCE="${1}" + LOGID="${2}" + dig_with_opts @10.53.0.5 "${SEQUENCE}.response-sequence._control" TXT >dig.out.control${LOGID} 2>&1 || ret=1 +} + # Check if response in file $1 has the correct TTL range. # The response record must have RRtype $2 and class IN (CLASS1). # Maximum TTL is given by $3. This works in most cases where TTL is @@ -1079,7 +1081,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (immediate -> immediate) ($n)" ret=0 - echo "no_response no_response" | sendcmd 10.53.0.5 + set_response_sequence no-response $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || ret=1 @@ -1089,7 +1091,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (partial AXFR -> partial AXFR) ($n)" ret=0 - echo "partial_axfr partial_axfr" | sendcmd 10.53.0.5 + set_response_sequence partial-axfr $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || ret=1 @@ -1099,7 +1101,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (immediate -> partial AXFR) ($n)" ret=0 - echo "no_response partial_axfr" | sendcmd 10.53.0.5 + set_response_sequence no-response.partial-axfr $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || ret=1 @@ -1109,7 +1111,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (partial AXFR -> immediate) ($n)" ret=0 - echo "partial_axfr no_response" | sendcmd 10.53.0.5 + set_response_sequence partial-axfr.no-response $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 && ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 2 ] || ret=1 @@ -1119,7 +1121,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (immediate -> complete AXFR) ($n)" ret=0 - echo "no_response complete_axfr" | sendcmd 10.53.0.5 + set_response_sequence no-response.complete-axfr $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 || ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || ret=1 @@ -1129,7 +1131,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (partial AXFR -> complete AXFR) ($n)" ret=0 - echo "partial_axfr complete_axfr" | sendcmd 10.53.0.5 + set_response_sequence partial-axfr.complete-axfr $n dig_with_opts @10.53.0.5 example AXFR +tries=2 >dig.out.test$n 2>&1 || ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || ret=1 @@ -1139,7 +1141,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "checking +tries=1 won't retry twice upon TCP EOF ($n)" ret=0 - echo "no_response no_response" | sendcmd 10.53.0.5 + set_response_sequence no-response $n dig_with_opts @10.53.0.5 example AXFR +tries=1 >dig.out.test$n 2>&1 && ret=1 # Sanity check: ensure ans5 behaves as expected. [ $(grep "communications error.*end of file" dig.out.test$n | wc -l) -eq 1 ] || ret=1 From 0b7a089c7f575e6005fbcf84c8faf4dd439d521a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 28 Nov 2025 16:41:44 +0100 Subject: [PATCH 6/8] Replace digdelv/ans8 with AsyncDnsServer Previously, the ans8 server had different response modes that applied to all queries. Replace it with AsyncDnsServer that has serves the different response modes under different domains without the need to change the server behaviour at runtime. Add the new queries that require an ns3 fallback to the ns3/example.db zone. --- bin/tests/system/digdelv/ans8/ans.py | 226 +++++---------------- bin/tests/system/digdelv/ns2/example.db.in | 3 + bin/tests/system/digdelv/tests.sh | 22 +- 3 files changed, 59 insertions(+), 192 deletions(-) diff --git a/bin/tests/system/digdelv/ans8/ans.py b/bin/tests/system/digdelv/ans8/ans.py index 3e18edc1cc..d959b597e2 100644 --- a/bin/tests/system/digdelv/ans8/ans.py +++ b/bin/tests/system/digdelv/ans8/ans.py @@ -9,194 +9,68 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -from __future__ import print_function -import os -import sys -import signal -import socket -import select -import struct +from typing import AsyncGenerator -import dns, dns.message -from dns.rcode import * +import dns +import dns.rcode -modes = [ - b"silent", # Do not respond - b"close", # UDP: same as silent; TCP: also close the connection - b"servfail", # Always respond with SERVFAIL - b"unstable", # Constantly switch between "silent" and "servfail" -] -mode = modes[0] -n = 0 +from isctest.asyncserver import ( + AsyncDnsServer, + CloseConnection, + DnsResponseSend, + DomainHandler, + IgnoreAllQueries, + QueryContext, + ResponseAction, + ResponseDrop, +) -def ctrl_channel(msg): - global modes, mode, n +class SilentHandler(DomainHandler, IgnoreAllQueries): + """Handler that doesn't respond.""" - msg = msg.splitlines().pop(0) - print("Received control message: %s" % msg) - - if msg in modes: - mode = msg - n = 0 - print("New mode: %s" % str(mode)) + domains = ["silent.example"] -def create_servfail(msg): - m = dns.message.from_wire(msg) - qname = m.question[0].name.to_text() - rrtype = m.question[0].rdtype - typename = dns.rdatatype.to_text(rrtype) +class CloseHandler(DomainHandler): + """Handler that doesn't respond and closes TCP connection.""" - with open("query.log", "a") as f: - f.write("%s %s\n" % (typename, qname)) - print("%s %s" % (typename, qname), end=" ") + domains = ["close.example"] - r = dns.message.make_response(m) - r.set_rcode(SERVFAIL) - return r + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + yield CloseConnection() -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +class SilentThenServfailHandler(DomainHandler): + """Handler that drops one query and response to the next one with SERVFAIL.""" + + domains = ["silent-then-servfail.example"] + counter = 0 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + if self.counter % 2 == 0: + yield ResponseDrop() + else: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + yield DnsResponseSend(qctx.response, authoritative=False) + self.counter += 1 -ip4 = "10.53.0.8" +def main() -> None: + server = AsyncDnsServer() + server.install_response_handlers( + [ + CloseHandler(), + SilentHandler(), + SilentThenServfailHandler(), + ] + ) + server.run() -try: - port = int(os.environ["PORT"]) -except: - port = 5300 -try: - ctrlport = int(os.environ["EXTRAPORT1"]) -except: - ctrlport = 5300 - -query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -query4_udp.bind((ip4, port)) - -query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -query4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -query4_tcp.bind((ip4, port)) -query4_tcp.listen(100) - -ctrl4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -ctrl4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ctrl4_tcp.bind((ip4, ctrlport)) -ctrl4_tcp.listen(100) - -signal.signal(signal.SIGTERM, sigterm) - -f = open("ans.pid", "w") -pid = os.getpid() -print(pid, file=f) -f.close() - -running = True - -print("Listening on %s port %d" % (ip4, port)) -print("Listening on %s port %d" % (ip4, ctrlport)) -print("Ctrl-c to quit") - -input = [query4_udp, query4_tcp, ctrl4_tcp] - -hung_conns = [] - -while running: - try: - inputready, outputready, exceptready = select.select(input, [], []) - except select.error as e: - break - except socket.error as e: - break - except KeyboardInterrupt: - break - - for s in inputready: - if s == query4_udp: - n = n + 1 - print("UDP query received on %s" % ip4, end=" ") - msg = s.recvfrom(65535) - if ( - mode == b"silent" - or mode == b"close" - or (mode == b"unstable" and n % 2 == 1) - ): - # Do not respond. - print("NO RESPONSE (%s)" % str(mode)) - continue - elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0): - rsp = create_servfail(msg[0]) - if rsp: - print(dns.rcode.to_text(rsp.rcode())) - s.sendto(rsp.to_wire(), msg[1]) - else: - print("NO RESPONSE (can not create a response)") - else: - raise (Exception("unsupported mode: %s" % mode)) - elif s == query4_tcp: - n = n + 1 - print("TCP query received on %s" % ip4, end=" ") - conn = None - try: - if mode == b"silent" or (mode == b"unstable" and n % 2 == 1): - conn, addr = s.accept() - # Do not respond and hang the connection. - print("NO RESPONSE (%s)" % str(mode)) - hung_conns.append(conn) - continue - elif mode == b"close": - conn, addr = s.accept() - # Do not respond and close the connection. - print("NO RESPONSE (%s)" % str(mode)) - conn.close() - continue - elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0): - conn, addr = s.accept() - # get TCP message length - msg = conn.recv(2) - if len(msg) != 2: - print("NO RESPONSE (can not read the message length)") - conn.close() - continue - length = struct.unpack(">H", msg[:2])[0] - msg = conn.recv(length) - if len(msg) != length: - print("NO RESPONSE (can not read the message)") - conn.close() - continue - rsp = create_servfail(msg) - if rsp: - print(dns.rcode.to_text(rsp.rcode())) - wire = rsp.to_wire() - conn.send(struct.pack(">H", len(wire))) - conn.send(wire) - else: - print("NO RESPONSE (can not create a response)") - else: - raise (Exception("unsupported mode: %s" % mode)) - except socket.error as e: - print("NO RESPONSE (error: %s)" % str(e)) - if conn: - conn.close() - elif s == ctrl4_tcp: - print("Control channel connected") - conn = None - try: - # Handle control channel input - conn, addr = s.accept() - msg = conn.recv(1024) - if msg: - ctrl_channel(msg) - conn.close() - except s.timeout: - pass - if conn: - conn.close() - - if not running: - break +if __name__ == "__main__": + main() diff --git a/bin/tests/system/digdelv/ns2/example.db.in b/bin/tests/system/digdelv/ns2/example.db.in index 7e73d3e97a..10b1045dee 100644 --- a/bin/tests/system/digdelv/ns2/example.db.in +++ b/bin/tests/system/digdelv/ns2/example.db.in @@ -33,6 +33,9 @@ c AAAA fd92:7065:b8e:ffff::3 d A 10.0.0.0 d AAAA fd92:7065:b8e:ffff:: +silent A 10.0.0.1 +close A 10.0.0.1 + xn--caf-dma A 10.1.2.3 foo TXT "testing" diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index eb753349ca..c07ed392c0 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -1275,20 +1275,16 @@ if [ -x "$DIG" ]; then # See [GL #3020] for more information n=$((n + 1)) echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)" - # Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes) - echo "unstable" | sendcmd 10.53.0.8 ret=0 - dig_with_opts +timeout=1 +nofail @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +nofail @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) echo_i "check that dig handles TCP timeout followed by a SERVFAIL correctly ($n)" - # Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes) - echo "unstable" | sendcmd 10.53.0.8 ret=0 - dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1321,10 +1317,8 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig tries the next server after a TCP socket read error ($n)" - # Ask ans8 to be in "close" mode, which closes the connection after accepting it - echo "close" | sendcmd 10.53.0.8 ret=0 - dig_with_opts +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +tcp @10.53.0.8 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1346,20 +1340,16 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig tries the next server after UDP socket read timeouts ($n)" - # Ask ans8 to be in "silent" mode - echo "silent" | sendcmd 10.53.0.8 ret=0 - dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) echo_i "check that dig tries the next server after TCP socket read timeouts ($n)" - # Ask ans8 to be in "silent" mode - echo "silent" | sendcmd 10.53.0.8 ret=0 - dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1368,7 +1358,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig correctly refuses to use a server with a IPv4 mapped IPv6 address after failing with a regular IP address ($n)" ret=0 - dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F ";; Skipping mapped address" dig.out.test$n >/dev/null || ret=1 grep -F ";; No acceptable nameservers" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi From 6c69abf7833b1ed2c5edd8a71faf5658e70a697b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Fri, 28 Nov 2025 16:49:08 +0100 Subject: [PATCH 7/8] Add dnspython>=2.0.0 requirement for digdelv Now that the test uses AsyncDnsServer, require the appropriate dnspython version for the test. --- bin/tests/system/digdelv/tests_sh_digdelv.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/tests/system/digdelv/tests_sh_digdelv.py b/bin/tests/system/digdelv/tests_sh_digdelv.py index 5767bc1a6e..3ed7811c1c 100644 --- a/bin/tests/system/digdelv/tests_sh_digdelv.py +++ b/bin/tests/system/digdelv/tests_sh_digdelv.py @@ -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( [ "delv.out.*", @@ -20,7 +23,6 @@ pytestmark = pytest.mark.extra_artifacts( "nsupdate.out.*", "yamlget.out.*", "ans*/ans.run", - "ans*/query.log", "ns*/anchor.*", "ns*/dsset-*", "ns*/keydata", From 9b63187a99b00ffc272612247a2ce5eba84fb71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicki=20K=C5=99=C3=AD=C5=BEek?= Date: Wed, 7 Jan 2026 16:31:37 +0100 Subject: [PATCH 8/8] Renumber ans7->ans6 and ans8->ans7 in digdelv test Since there was no 10.53.0.6 server in the test, renumber the remaining ones so that there's no gap in the server names. This commit simply moves the ans.py files without any changes and renumbers the IP addresses in tests. --- bin/tests/system/digdelv/ans6/ans.py | 40 +++++++++++++++ bin/tests/system/digdelv/ans7/ans.py | 52 ++++++++++++++++--- bin/tests/system/digdelv/ans8/ans.py | 76 ---------------------------- bin/tests/system/digdelv/tests.sh | 20 ++++---- 4 files changed, 94 insertions(+), 94 deletions(-) create mode 100644 bin/tests/system/digdelv/ans6/ans.py delete mode 100644 bin/tests/system/digdelv/ans8/ans.py diff --git a/bin/tests/system/digdelv/ans6/ans.py b/bin/tests/system/digdelv/ans6/ans.py new file mode 100644 index 0000000000..77e7141590 --- /dev/null +++ b/bin/tests/system/digdelv/ans6/ans.py @@ -0,0 +1,40 @@ +# 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.opcode +import dns.rcode + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + ResponseHandler, + QueryContext, +) + + +class ReplyUpdateHandler(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_opcode(dns.opcode.UPDATE) + yield DnsResponseSend(qctx.response) + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handler(ReplyUpdateHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/digdelv/ans7/ans.py b/bin/tests/system/digdelv/ans7/ans.py index 77e7141590..d959b597e2 100644 --- a/bin/tests/system/digdelv/ans7/ans.py +++ b/bin/tests/system/digdelv/ans7/ans.py @@ -11,28 +11,64 @@ from typing import AsyncGenerator -import dns.opcode +import dns import dns.rcode from isctest.asyncserver import ( AsyncDnsServer, + CloseConnection, DnsResponseSend, - ResponseHandler, + DomainHandler, + IgnoreAllQueries, QueryContext, + ResponseAction, + ResponseDrop, ) -class ReplyUpdateHandler(ResponseHandler): +class SilentHandler(DomainHandler, IgnoreAllQueries): + """Handler that doesn't respond.""" + + domains = ["silent.example"] + + +class CloseHandler(DomainHandler): + """Handler that doesn't respond and closes TCP connection.""" + + domains = ["close.example"] + async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.response.set_opcode(dns.opcode.UPDATE) - yield DnsResponseSend(qctx.response) + ) -> AsyncGenerator[ResponseAction, None]: + yield CloseConnection() + + +class SilentThenServfailHandler(DomainHandler): + """Handler that drops one query and response to the next one with SERVFAIL.""" + + domains = ["silent-then-servfail.example"] + counter = 0 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + if self.counter % 2 == 0: + yield ResponseDrop() + else: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + yield DnsResponseSend(qctx.response, authoritative=False) + self.counter += 1 def main() -> None: - server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) - server.install_response_handler(ReplyUpdateHandler()) + server = AsyncDnsServer() + server.install_response_handlers( + [ + CloseHandler(), + SilentHandler(), + SilentThenServfailHandler(), + ] + ) server.run() diff --git a/bin/tests/system/digdelv/ans8/ans.py b/bin/tests/system/digdelv/ans8/ans.py deleted file mode 100644 index d959b597e2..0000000000 --- a/bin/tests/system/digdelv/ans8/ans.py +++ /dev/null @@ -1,76 +0,0 @@ -# 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 -import dns.rcode - -from isctest.asyncserver import ( - AsyncDnsServer, - CloseConnection, - DnsResponseSend, - DomainHandler, - IgnoreAllQueries, - QueryContext, - ResponseAction, - ResponseDrop, -) - - -class SilentHandler(DomainHandler, IgnoreAllQueries): - """Handler that doesn't respond.""" - - domains = ["silent.example"] - - -class CloseHandler(DomainHandler): - """Handler that doesn't respond and closes TCP connection.""" - - domains = ["close.example"] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - yield CloseConnection() - - -class SilentThenServfailHandler(DomainHandler): - """Handler that drops one query and response to the next one with SERVFAIL.""" - - domains = ["silent-then-servfail.example"] - counter = 0 - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - if self.counter % 2 == 0: - yield ResponseDrop() - else: - qctx.response.set_rcode(dns.rcode.SERVFAIL) - yield DnsResponseSend(qctx.response, authoritative=False) - self.counter += 1 - - -def main() -> None: - server = AsyncDnsServer() - server.install_response_handlers( - [ - CloseHandler(), - SilentHandler(), - SilentThenServfailHandler(), - ] - ) - server.run() - - -if __name__ == "__main__": - main() diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index c07ed392c0..31360dd342 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -76,7 +76,7 @@ $PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1 n=$((n + 1)) echo_i "check nslookup handles UPDATE response ($n)" ret=0 -"$NSLOOKUP" -q=CNAME -timeout=1 "-port=$PORT" foo.bar 10.53.0.7 >nslookup.out.test$n 2>&1 && ret=1 +"$NSLOOKUP" -q=CNAME -timeout=1 "-port=$PORT" foo.bar 10.53.0.6 >nslookup.out.test$n 2>&1 && ret=1 grep "Opcode mismatch" nslookup.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -84,7 +84,7 @@ status=$((status + ret)) n=$((n + 1)) echo_i "check host handles UPDATE response ($n)" ret=0 -"$HOST" -W 1 -t CNAME -p $PORT foo.bar 10.53.0.7 >host.out.test$n 2>&1 && ret=1 +"$HOST" -W 1 -t CNAME -p $PORT foo.bar 10.53.0.6 >host.out.test$n 2>&1 && ret=1 grep "Opcode mismatch" host.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -94,7 +94,7 @@ echo_i "check nsupdate handles UPDATE response to QUERY ($n)" ret=0 res=0 $NSUPDATE <nsupdate.out.test$n 2>&1 || res=$? -server 10.53.0.7 ${PORT} +server 10.53.0.6 ${PORT} add x.example.com 300 in a 1.2.3.4 send EOF @@ -107,7 +107,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check dig handles UPDATE response ($n)" ret=0 - dig_with_opts @10.53.0.7 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1 + dig_with_opts @10.53.0.6 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1 grep "Opcode mismatch" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1276,7 +1276,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)" ret=0 - dig_with_opts +timeout=1 +nofail @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +nofail @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1284,7 +1284,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig handles TCP timeout followed by a SERVFAIL correctly ($n)" ret=0 - dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +nofail +tcp @10.53.0.7 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1318,7 +1318,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig tries the next server after a TCP socket read error ($n)" ret=0 - dig_with_opts +tcp @10.53.0.8 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +tcp @10.53.0.7 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1341,7 +1341,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig tries the next server after UDP socket read timeouts ($n)" ret=0 - dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 @10.53.0.7 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1349,7 +1349,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig tries the next server after TCP socket read timeouts ($n)" ret=0 - dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts +timeout=1 +tcp @10.53.0.7 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -1358,7 +1358,7 @@ if [ -x "$DIG" ]; then n=$((n + 1)) echo_i "check that dig correctly refuses to use a server with a IPv4 mapped IPv6 address after failing with a regular IP address ($n)" ret=0 - dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 silent.example >dig.out.test$n 2>&1 || ret=1 + dig_with_opts @10.53.0.7 @::ffff:10.53.0.7 silent.example >dig.out.test$n 2>&1 || ret=1 grep -F ";; Skipping mapped address" dig.out.test$n >/dev/null || ret=1 grep -F ";; No acceptable nameservers" dig.out.test$n >/dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi