From 75567f86ca66f7aa598ccb6c093af8224e5e8753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 25 Mar 2025 05:01:34 +0100 Subject: [PATCH 1/3] Make response handlers global by default Instead of requiring each class inheriting from ResponseHandler to define its match() method, make the latter non-abstract and default to returning True for all queries. This will reduce the amount of boilerplate code in custom servers. --- bin/tests/system/isctest/asyncserver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/tests/system/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index b4270fa899..996db22893 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -360,10 +360,13 @@ class ResponseHandler(abc.ABC): method. """ - @abc.abstractmethod + # pylint: disable=unused-argument def match(self, qctx: QueryContext) -> bool: """ - Matching logic - query is handled when it returns True. + Matching logic - the first handler whose `match()` method returns True + is used for handling the query. + + The default for each handler is to handle all queries. """ return True From f24a534ff1b7be611ab320b041b58103d5607eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 25 Mar 2025 05:01:34 +0100 Subject: [PATCH 2/3] Add a response handler for ignoring all queries Dropping all incoming queries is a typical use case for a custom server used in BIND 9 system tests. Add a response handler implementing that behavior so that it can be reused. --- bin/tests/system/isctest/asyncserver.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/tests/system/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index 996db22893..dd2cf0c04c 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -383,6 +383,17 @@ class ResponseHandler(abc.ABC): yield DnsResponseSend(qctx.response) +class IgnoreAllQueries(ResponseHandler): + """ + Do not respond to any queries sent to the server. + """ + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + yield ResponseDrop() + + class DomainHandler(ResponseHandler): """ Base class used for deriving custom domain handlers. From a8878cf35d6ea35f5580bf880a628889f885993f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 25 Mar 2025 05:01:34 +0100 Subject: [PATCH 3/3] Use isctest.asyncserver in the "upforwd" test Replace the custom DNS server used in the "upforwd" system test with new code based on the isctest.asyncserver module. The ans4 server currently used in that test is a copy of bin/tests/system/ans.pl modified to receive queries over UDP and TCP without ever responding to any of them. --- bin/tests/system/upforwd/ans4/ans.pl | 365 --------------------------- bin/tests/system/upforwd/ans4/ans.py | 20 ++ 2 files changed, 20 insertions(+), 365 deletions(-) delete mode 100644 bin/tests/system/upforwd/ans4/ans.pl create mode 100644 bin/tests/system/upforwd/ans4/ans.py diff --git a/bin/tests/system/upforwd/ans4/ans.pl b/bin/tests/system/upforwd/ans4/ans.pl deleted file mode 100644 index 20392a4c11..0000000000 --- a/bin/tests/system/upforwd/ans4/ans.pl +++ /dev/null @@ -1,365 +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 the name server from hell. It provides canned -# responses based on pattern matching the queries, and -# can be reprogrammed on-the-fly over a TCP connection. -# -# The server listens for control connections on port 5301. -# A control connection is a TCP stream of lines like -# -# /pattern/ -# name ttl type rdata -# name ttl type rdata -# ... -# /pattern/ -# name ttl type rdata -# name ttl type rdata -# ... -# -# There can be any number of patterns, each associated -# with any number of response RRs. Each pattern is a -# Perl regular expression. -# -# Each incoming query is converted into a string of the form -# "qname qtype" (the printable query domain name, space, -# printable query type) and matched against each pattern. -# -# The first pattern matching the query is selected, and -# the RR following the pattern line are sent in the -# answer section of the response. -# -# Each new control connection causes the current set of -# patterns and responses to be cleared before adding new -# ones. -# -# The server handles UDP and TCP queries. Zone transfer -# responses work, but must fit in a single 64 k message. -# -# Now you can add TSIG, just specify key/key data with: -# -# /pattern / -# name ttl type rdata -# name ttl type rdata -# -# Note that this data will still be sent with any request for -# pattern, only this data will be signed. Currently, this is only -# done for TCP. - - -use IO::File; -use IO::Socket; -use Data::Dumper; -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.4"; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $udpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $localport, Proto => "udp", Reuse => 1) or die "$!"; - -my $tcpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $localport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -print "listening on $server_addr:$localport.\n"; - -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 @answers = (); -my @rules; -sub handleUDP { - my ($buf) = @_; - 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; - } - - $packet->header->qr(1); - $packet->header->aa(1); - - my @questions = $packet->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - - # get the existing signature if any, and clear the additional section - my $prev_tsig; - while (my $rr = $packet->pop("additional")) { - if ($rr->type eq "TSIG") { - $prev_tsig = $rr; - } - } - - my $r; - foreach $r (@rules) { - my $pattern = $r->{pattern}; - my($dbtype, $key_name, $key_data) = split(/ /,$pattern); - print "[handleUDP] $dbtype, $key_name, $key_data \n"; - if ("$qname $qtype" =~ /$dbtype/) { - my $a; - foreach $a (@{$r->{answer}}) { - $packet->push("answer", $a); - } - if(defined($key_name) && defined($key_data)) { - # Sign the packet - print " Signing the response with " . - "$key_name/$key_data\n"; - my $tsig = Net::DNS::RR-> - new("$key_name TSIG $key_data"); - - # These kluges are necessary because Net::DNS - # doesn't know how to sign responses. We - # clear compnames so that the TSIG key and - # algorithm name won't be compressed, and - # add one to arcount because the signing - # function will attempt to decrement it, - # which is incorrect in a response. Finally - # we set request_mac to the previous digest. - $packet->{"compnames"} = {}; - $packet->{"header"}{"arcount"} += 1; - if (defined($prev_tsig)) { - my $rmac = pack('n H*', - $prev_tsig->mac_size, - $prev_tsig->mac); - $tsig->{"request_mac"} = - unpack("H*", $rmac); - } - - $packet->sign_tsig($tsig); - } - last; - } - } - #$packet->print; - - return $packet->data; -} - -# namelen: -# given a stream of data, reads a DNS-formatted name and returns its -# total length, thus making it possible to skip past it. -sub namelen { - my ($data) = @_; - my $len = 0; - my $label_len = 0; - do { - $label_len = unpack("c", $data); - $data = substr($data, $label_len + 1); - $len += $label_len + 1; - } while ($label_len != 0); - return ($len); -} - -# packetlen: -# given a stream of data, reads a DNS wire-format packet and returns -# its total length, making it possible to skip past it. -sub packetlen { - my ($data) = @_; - my $q; - my $rr; - - my ($header, $offset) = Net::DNS::Header->parse(\$data); - for (1 .. $header->qdcount) { - ($q, $offset) = Net::DNS::Question->parse(\$data, $offset); - } - for (1 .. $header->ancount) { - ($rr, $offset) = Net::DNS::RR->parse(\$data, $offset); - } - for (1 .. $header->nscount) { - ($rr, $offset) = Net::DNS::RR->parse(\$data, $offset); - } - for (1 .. $header->arcount) { - ($rr, $offset) = Net::DNS::RR->parse(\$data, $offset); - } - return $offset; -} - -# sign_tcp_continuation: -# This is a hack to correct the problem that Net::DNS has no idea how -# to sign multiple-message TCP responses. Several data that are included -# in the digest when signing a query or the first message of a response are -# omitted when signing subsequent messages in a TCP stream. -# -# Net::DNS::Packet->sign_tsig() has the ability to use a custom signing -# function (specified by calling Packet->sign_func()). We use this -# function as the signing function for TCP continuations, and it removes -# the unwanted data from the digest before calling the default sign_hmac -# function. -sub sign_tcp_continuation { - my ($key, $data) = @_; - - # copy out first two bytes: size of the previous MAC - my $rmacsize = unpack("n", $data); - $data = substr($data, 2); - - # copy out previous MAC - my $rmac = substr($data, 0, $rmacsize); - $data = substr($data, $rmacsize); - - # try parsing out the packet information - my $plen = packetlen($data); - my $pdata = substr($data, 0, $plen); - $data = substr($data, $plen); - - # remove the keyname, ttl, class, and algorithm name - $data = substr($data, namelen($data)); - $data = substr($data, 6); - $data = substr($data, namelen($data)); - - # preserve the TSIG data - my $tdata = substr($data, 0, 8); - - # prepare a new digest and sign with it - $data = pack("n", $rmacsize) . $rmac . $pdata . $tdata; - return Net::DNS::RR::TSIG::sign_hmac($key, $data); -} - -sub handleTCP { - my ($buf) = @_; - 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; - } - - $packet->header->qr(1); - $packet->header->aa(1); - - my @questions = $packet->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - - # get the existing signature if any, and clear the additional section - my $prev_tsig; - my $signer; - while (my $rr = $packet->pop("additional")) { - if ($rr->type eq "TSIG") { - $prev_tsig = $rr; - } - } - - my @results = (); - my $count_these = 0; - - my $r; - foreach $r (@rules) { - my $pattern = $r->{pattern}; - my($dbtype, $key_name, $key_data) = split(/ /,$pattern); - print "[handleTCP] $dbtype, $key_name, $key_data \n"; - if ("$qname $qtype" =~ /$dbtype/) { - $count_these++; - my $a; - foreach $a (@{$r->{answer}}) { - $packet->push("answer", $a); - } - if(defined($key_name) && defined($key_data)) { - # sign the packet - print " Signing the data with " . - "$key_name/$key_data\n"; - - my $tsig = Net::DNS::RR-> - new("$key_name TSIG $key_data"); - - # These kluges are necessary because Net::DNS - # doesn't know how to sign responses. We - # clear compnames so that the TSIG key and - # algorithm name won't be compressed, and - # add one to arcount because the signing - # function will attempt to decrement it, - # which is incorrect in a response. Finally - # we set request_mac to the previous digest. - $packet->{"compnames"} = {}; - $packet->{"header"}{"arcount"} += 1; - if (defined($prev_tsig)) { - my $rmac = pack('n H*', - $prev_tsig->mac_size, - $prev_tsig->mac); - $tsig->{"request_mac"} = - unpack("H*", $rmac); - } - - $tsig->sign_func($signer) if defined($signer); - $packet->sign_tsig($tsig); - $signer = \&sign_tcp_continuation; - - my $copy = - Net::DNS::Packet->new(\($packet->data)); - $prev_tsig = $copy->pop("additional"); - } - #$packet->print; - push(@results,$packet->data); - $packet = new Net::DNS::Packet(\$buf, 0); - $packet->header->qr(1); - $packet->header->aa(1); - } - } - print " A total of $count_these patterns matched\n"; - return \@results; -} - -# Main -my $rin; -my $rout; -for (;;) { - $rin = ''; - vec($rin, fileno($tcpsock), 1) = 1; - vec($rin, fileno($udpsock), 1) = 1; - - select($rout = $rin, undef, undef, undef); - - if (vec($rout, fileno($udpsock), 1)) { - printf "UDP request\n"; - my $buf; - $udpsock->recv($buf, 512); - } elsif (vec($rout, fileno($tcpsock), 1)) { - my $conn = $tcpsock->accept; - my $buf; - for (;;) { - my $lenbuf; - my $n = $conn->sysread($lenbuf, 2); - last unless $n == 2; - my $len = unpack("n", $lenbuf); - $n = $conn->sysread($buf, $len); - last unless $n == $len; - } - sleep(1); - $conn->close; - } -} diff --git a/bin/tests/system/upforwd/ans4/ans.py b/bin/tests/system/upforwd/ans4/ans.py new file mode 100644 index 0000000000..bd6e863bd7 --- /dev/null +++ b/bin/tests/system/upforwd/ans4/ans.py @@ -0,0 +1,20 @@ +""" +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 + + +if __name__ == "__main__": + server = AsyncDnsServer() + server.install_response_handler(IgnoreAllQueries()) + server.run()