From: Štěpán Balážik Date: Fri, 23 Jan 2026 18:51:45 +0000 (+0100) Subject: Reimplement xfer/ans5 using ControllableAsyncServer X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f9ed3650acdc2c5b38d8b36729b045ca63f983ef;p=thirdparty%2Fbind9.git Reimplement xfer/ans5 using ControllableAsyncServer Remove the last usage of the `ans.pl` server and the server itself. --- diff --git a/bin/tests/system/ans.pl b/bin/tests/system/ans.pl deleted file mode 100644 index bfd36bc0f17..00000000000 --- a/bin/tests/system/ans.pl +++ /dev/null @@ -1,596 +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 queries on port 5300 (or PORT). -# -# The server listens for control connections on port 5301 (or EXTRAPORT1). -# -# 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. If an empty pattern ("//") is -# received, the server will ignore all incoming queries (TCP -# connections will still be accepted, but both UDP queries -# and TCP queries will not be responded to). If a non-empty -# pattern is then received over the same control connection, -# default behavior is restored. -# -# 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. -# -# /pattern NOTIMP / -# /pattern NOTIMP/ -# -# Return a NOTIMP response -# -# /pattern EDNS=NOTIMP / -# /pattern EDNS=NOTIMP/ -# -# Return a NOTIMP response to an EDNS request -# -# /pattern EDNS=FORMERR / -# /pattern EDNS=FORMERR/ -# -# Return a FORMERR response to an EDNS request -# -# /pattern bad-id / -# /pattern bad-id/ -# -# will add 50 to the message id of the response. - - -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; - -# We default to listening on 10.53.0.2 for historical reasons -# XXX: we should also be able to specify IPv6 -my $server_addr = "10.53.0.2"; -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 $hmac_algorithm = $ENV{'DEFAULT_HMAC'}; -if (!defined($hmac_algorithm)) { $hmac_algorithm = "hmac-sha256"; } - -# XXX: we should also be able to set the port numbers to listen on. -my $ctlsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $ctrlport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -my $udpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $mainport, Proto => "udp", Reuse => 1) or die "$!"; - -my $tcpsock = IO::Socket::INET->new(LocalAddr => "$server_addr", - LocalPort => $mainport, Proto => "tcp", Listen => 5, Reuse => 1) or die "$!"; - -print "listening on $server_addr:$mainport,$ctrlport.\n"; -print "Using Net::DNS $Net::DNS::VERSION\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; -my $udphandler; -my $tcphandler; - -sub handleUDP { - 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); - - # get the existing signature if any, and clear the additional section - my $prev_tsig; - while (my $rr = $request->pop("additional")) { - $prev_tsig = $rr if ($rr->type eq "TSIG"); - } - - 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)) { - my $tsig; - # Sign the packet - print " Signing the response with " . - "$key_name/$key_data\n"; - - if ($Net::DNS::VERSION < 0.69) { - $tsig = Net::DNS::RR->new( - "$key_name TSIG $key_data"); - } else { - $tsig = Net::DNS::RR->new( - name => $key_name, - algorithm => $hmac_algorithm, - type => 'TSIG', - key => $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"} = {} - if ($Net::DNS::VERSION < 0.70); - $packet->{"header"}{"arcount"} += 1 - if ($Net::DNS::VERSION < 0.70); - if (defined($prev_tsig)) { - if ($Net::DNS::VERSION < 0.73) { - my $rmac = pack('n H*', - length($prev_tsig->mac)/2, - $prev_tsig->mac); - $tsig->{"request_mac"} = - unpack("H*", $rmac); - } else { - $tsig->request_mac( - $prev_tsig->mac); - } - } - - $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; - my $offset; - - # - # decode/encode were introduced in Net::DNS 0.68 - # parse is no longer a method and calling it here makes perl croak. - # - my $decode = 0; - $decode = 1 if ($Net::DNS::VERSION >= 0.68); - - if ($decode) { - ($header, $offset) = Net::DNS::Header->decode(\$data); - } else { - ($header, $offset) = Net::DNS::Header->parse(\$data); - } - - for (1 .. $header->qdcount) { - if ($decode) { - ($q, $offset) = - Net::DNS::Question->decode(\$data, $offset); - } else { - ($q, $offset) = - Net::DNS::Question->parse(\$data, $offset); - } - } - for (1 .. $header->ancount) { - if ($decode) { - ($q, $offset) = Net::DNS::RR->decode(\$data, $offset); - } else { - ($q, $offset) = Net::DNS::RR->parse(\$data, $offset); - } - } - for (1 .. $header->nscount) { - if ($decode) { - ($q, $offset) = Net::DNS::RR->decode(\$data, $offset); - } else { - ($q, $offset) = Net::DNS::RR->parse(\$data, $offset); - } - } - for (1 .. $header->arcount) { - if ($decode) { - ($q, $offset) = Net::DNS::RR->decode(\$data, $offset); - } else { - ($q, $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 $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 @additional = $request->additional; - my $has_opt = 0; - foreach (@additional) { - $has_opt = 1 if (ref($_) eq 'Net::DNS::RR::OPT'); - } - - my $opaque; - - my $packet = new Net::DNS::Packet($qname, $qtype, $qclass); - $packet->header->qr(1); - $packet->header->aa(1); - $packet->header->id($id); - - # get the existing signature if any, and clear the additional section - my $prev_tsig; - my $signer; - my $continuation = 0; - if ($Net::DNS::VERSION < 0.81) { - while (my $rr = $request->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, $tname) = split(/ /,$pattern); - print "[handleTCP] $dbtype, $key_name, $key_data, $tname \n"; - if ("$qname $qtype" =~ /$dbtype/) { - $count_these++; - my $a; - my $done = 0; - - while (defined($key_name) && - ($key_name eq "NOTIMP" || $key_name eq "EDNS=NOTIMP" || - $key_name eq "EDNS=FORMERR" || $key_name eq "bad-id")) { - - if (defined($key_name) && $key_name eq "NOTIMP") { - $packet->header->rcode('NOTIMP') if (!$done); - $key_name = $key_data; - ($key_data, $tname) = split(/ /,$tname); - $done = 1; - } - - if (defined($key_name) && $key_name eq "EDNS=NOTIMP") { - if ($has_opt) { - $packet->header->rcode('NOTIMP') if (!$done); - $done = 1; - } - $key_name = $key_data; - ($key_data, $tname) = split(/ /,$tname); - } - - if (defined($key_name) && $key_name eq "EDNS=FORMERR") { - if ($has_opt) { - $packet->header->rcode('FORMERR') if (!$done); - $done = 1; - } - $key_name = $key_data; - ($key_data, $tname) = split(/ /,$tname); - } - - if (defined($key_name) && $key_name eq "bad-id") { - $packet->header->id(($id+50)%0xffff); - $key_name = $key_data; - ($key_data, $tname) = split(/ /,$tname); - } - } - - if (!$done) { - foreach $a (@{$r->{answer}}) { - $packet->push("answer", $a); - } - } - - if (defined($key_name) && defined($key_data)) { - my $tsig; - # sign the packet - print " Signing the data with " . - "$key_name/$key_data\n"; - - if ($Net::DNS::VERSION < 0.69) { - $tsig = Net::DNS::RR->new( - "$key_name TSIG $key_data"); - $tsig->algorithm = $hmac_algorithm; - } elsif ($Net::DNS::VERSION >= 0.81 && - $continuation) { - } elsif ($Net::DNS::VERSION >= 0.75 && - $continuation) { - $tsig = $prev_tsig; - } else { - $tsig = Net::DNS::RR->new( - name => $key_name, - algorithm => $hmac_algorithm, - type => 'TSIG', - key => $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"} = {} - if ($Net::DNS::VERSION < 0.70); - $packet->{"header"}{"arcount"} += 1 - if ($Net::DNS::VERSION < 0.70); - if (defined($prev_tsig)) { - if ($Net::DNS::VERSION < 0.73) { - my $rmac = pack('n H*', - length($prev_tsig->mac)/2, - $prev_tsig->mac); - $tsig->{"request_mac"} = - unpack("H*", $rmac); - } elsif ($Net::DNS::VERSION < 0.81) { - $tsig->request_mac( - $prev_tsig->mac); - } - } - - $tsig->sign_func($signer) if defined($signer); - $tsig->continuation($continuation) if - ($Net::DNS::VERSION >= 0.71 && - $Net::DNS::VERSION <= 0.74 ); - if ($Net::DNS::VERSION < 0.81) { - $packet->sign_tsig($tsig); - } elsif ($continuation) { - $opaque = $packet->sign_tsig($opaque); - } else { - $opaque = $packet->sign_tsig($request); - } - $signer = \&sign_tcp_continuation - if ($Net::DNS::VERSION < 0.70); - $continuation = 1; - - my $copy = - Net::DNS::Packet->new(\($packet->data)); - $prev_tsig = $copy->pop("additional"); - } - #$packet->print; - push(@results,$packet->data); - last if ($done); - if ($tname eq "") { - $tname = $qname; - } - $packet = new Net::DNS::Packet($tname, $qtype, $qclass); - $packet->header->qr(1); - $packet->header->aa(1); - $packet->header->id($id); - } - } - print " A total of $count_these patterns matched\n"; - return \@results; -} - -# Main -my $rin; -my $rout; -for (;;) { - $rin = ''; - vec($rin, fileno($ctlsock), 1) = 1; - vec($rin, fileno($tcpsock), 1) = 1; - vec($rin, fileno($udpsock), 1) = 1; - - select($rout = $rin, undef, undef, undef); - - if (vec($rout, fileno($ctlsock), 1)) { - warn "ctl conn"; - my $conn = $ctlsock->accept; - my $rule = (); - @rules = (); - while (my $line = $conn->getline) { - chomp $line; - if ($line =~ m!^/(.*)/$!) { - if (length($1) == 0) { - $udphandler = sub { return; }; - $tcphandler = sub { return; }; - } else { - $udphandler = \&handleUDP; - $tcphandler = \&handleTCP; - $rule = { pattern => $1, answer => [] }; - push(@rules, $rule); - } - } else { - push(@{$rule->{answer}}, - new Net::DNS::RR($line)); - } - } - $conn->close; - #print Dumper(@rules); - #print "+=+=+ $rules[0]->{'pattern'}\n"; - #print "+=+=+ $rules[0]->{'answer'}->[0]->{'rname'}\n"; - #print "+=+=+ $rules[0]->{'answer'}->[0]\n"; - } elsif (vec($rout, fileno($udpsock), 1)) { - printf "UDP request\n"; - my $buf; - $udpsock->recv($buf, 512); - my $result = &$udphandler($buf); - if (defined($result)) { - my $num_chars = $udpsock->send($result); - print " Sent $num_chars bytes via UDP\n"; - } - } 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; - print "TCP request\n"; - my $result = &$tcphandler($buf); - if (defined($result)) { - foreach my $response (@$result) { - $len = length($response); - $n = $conn->syswrite(pack("n", $len), 2); - $n = $conn->syswrite($response, $len); - print " Sent: $n chars via TCP\n"; - } - } - } - $conn->close; - } -} diff --git a/bin/tests/system/start.pl b/bin/tests/system/start.pl index 81b228e23e0..57d99270efd 100755 --- a/bin/tests/system/start.pl +++ b/bin/tests/system/start.pl @@ -329,7 +329,7 @@ sub construct_ans_command { } elsif (-e "$testdir/$server/ans.pl") { $command = "$PERL ans.pl"; } else { - $command = "$PERL $srcdir/ans.pl 10.53.0.$n"; + die "unable to find ans.pl or ans.py in \"$testdir/$server\"\n"; } if ($options) { diff --git a/bin/tests/system/xfer/ans5/ans.py b/bin/tests/system/xfer/ans5/ans.py new file mode 100644 index 00000000000..d26fb926a88 --- /dev/null +++ b/bin/tests/system/xfer/ans5/ans.py @@ -0,0 +1,433 @@ +# 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 collections.abc import AsyncGenerator, Collection +from typing import final + +import abc + +import dns.message +import dns.name +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import dns.tsig + +from isctest.asyncserver import ( + AxfrHandler, + ControllableAsyncDnsServer, + DnsProtocol, + DnsResponseSend, + QueryContext, + ResponseAction, + ResponseHandler, + SwitchControlCommand, +) +from isctest.vars.algorithms import ALG_VARS + +GOOD_KEY_DATA = "LSAnCU+Z" +DEFAULT_KEY = dns.tsig.Key("tsig_key", GOOD_KEY_DATA, ALG_VARS["DEFAULT_HMAC"]) +BAD_KEY = dns.tsig.Key("bad_key", GOOD_KEY_DATA, ALG_VARS["DEFAULT_HMAC"]) +UNUSED_KEY = dns.tsig.Key("unused_key", GOOD_KEY_DATA, ALG_VARS["DEFAULT_HMAC"]) +KEYRING = {key.name: key for key in (DEFAULT_KEY, BAD_KEY, UNUSED_KEY)} + +KEY_WITH_BAD_DATA = dns.tsig.Key("tsig_key", "abcd1234ffff", ALG_VARS["DEFAULT_HMAC"]) + + +class ResponseHandlerWrapper(ResponseHandler, abc.ABC): + def __init__(self, inner: ResponseHandler) -> None: + self._inner = inner + + def match(self, qctx: QueryContext) -> bool: + return self._inner.match(qctx) + + def _on_query_received(self, qctx: QueryContext) -> None: + pass + + @abc.abstractmethod + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + raise NotImplementedError + + @final + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + self._on_query_received(qctx) + async for response_action in self._inner.get_responses(qctx): + self._modify_response(qctx, response_action) + yield response_action + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self._inner})" + + +class SignResponses(ResponseHandlerWrapper): + """ + This handler encapsulates another handler and signs all responses it yields + with TSIG using the specified key. + + If the query is over TCP, it maintains the TSIG context across multiple + messages to allow proper signing of multi-message responses. + + Ideally, TSIG context would be handled in isctest.asyncserver, but that would + require more extensive changes there, so it is implemented here for a single + test. + """ + + def __init__(self, inner: ResponseHandler, key: dns.tsig.Key = DEFAULT_KEY) -> None: + super().__init__(inner) + self._key = key + self._tsig_ctx: dns.tsig.GSSTSig | dns.tsig.HMACTSig | None = None + + def _on_query_received(self, qctx: QueryContext) -> None: + self._tsig_ctx = None + + def _apply_tsig_context(self, response: dns.message.Message) -> None: + # On TCP we need to maintain the TSIG context across multiple messages. + # Force TSIG materialization to get the updated context by calling to_wire(). + _ = response.to_wire(multi=True, tsig_ctx=self._tsig_ctx) + # Cache TSIG context for the next message. + self._tsig_ctx = response.tsig_ctx + + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + assert isinstance( + response_action, DnsResponseSend + ), "SignResponses can only wrap handlers that yield DnsResponseSend" + response_action.response.use_tsig(self._key) + if qctx.protocol == DnsProtocol.TCP: + self._apply_tsig_context(response_action.response) + + def __str__(self) -> str: + return f"SignResponses({self._inner}, key={self._key})" + + +class SignFirstResponse(ResponseHandlerWrapper): + def __init__(self, inner: ResponseHandler, key: dns.tsig.Key = DEFAULT_KEY) -> None: + super().__init__(inner) + self._key = key + self._first_yielded = False + + def _on_query_received(self, qctx: QueryContext) -> None: + self._first_yielded = False + + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + assert isinstance( + response_action, DnsResponseSend + ), "SignFirstResponse can only wrap handlers that yield DnsResponseSend" + if not self._first_yielded: + response_action.response.use_tsig(self._key) + self._first_yielded = True + else: + response_action.response.tsig = None + + def __str__(self) -> str: + return f"SignFirstResponse({self._inner}, key={self._key})" + + +class Add50ToMessageIdFromSecondResponse(ResponseHandlerWrapper): + def __init__(self, inner: ResponseHandler) -> None: + super().__init__(inner) + self._first_yielded = False + + def _on_query_received(self, qctx: QueryContext) -> None: + self._first_yielded = False + + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + if self._first_yielded: + assert isinstance( + response_action, DnsResponseSend + ), "Add50ToMessageIdFromSecondResponse can only wrap handlers that yield DnsResponseSend from the second response onward" + response_action.response.id += 50 + else: + self._first_yielded = True + + +class ClearTsig(ResponseHandlerWrapper): + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + assert isinstance( + response_action, DnsResponseSend + ), "ClearTsig can only wrap handlers that yield DnsResponseSend" + response_action.response.tsig = None + + +def rrset( + owner: str | dns.name.Name, + ttl: int, + rdtype: dns.rdatatype.RdataType, + rdata: str, +) -> dns.rrset.RRset: + return dns.rrset.from_text( + owner, + ttl, + dns.rdataclass.IN, + rdtype, + rdata, + ) + + +def soa( + serial: int, + *, + owner: str = "nil.", + mname: str = "ns.nil.", + rname: str = "root.nil.", +) -> dns.rrset.RRset: + return rrset( + owner, + 300, + dns.rdatatype.SOA, + f"{mname} {rname} {serial} 300 300 604800 300", + ) + + +class SoaHandler(ResponseHandler): + def __init__(self, serial: int = 1) -> None: + 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) + + +def ns() -> dns.rrset.RRset: + return rrset( + "nil.", + 300, + dns.rdatatype.NS, + "ns.nil.", + ) + + +def txt(data: str) -> dns.rrset.RRset: + return rrset( + "nil.", + 300, + dns.rdatatype.TXT, + f'"{data}"', + ) + + +def a() -> dns.rrset.RRset: + return rrset( + "a.nil.", + 60, + dns.rdatatype.A, + "10.0.0.61", + ) + + +def extra_a() -> dns.rrset.RRset: + return rrset( + "b.nil.", + 60, + dns.rdatatype.A, + "10.0.0.62", + ) + + +class XferAxfrHandler(AxfrHandler): + def __init__( + self, + *, + txt_data: str, + soa_serial: int = 1, + extra_a_record: bool = False, + final_soa_mismatch: bool = False, + ) -> None: + self._txt_data = txt_data + self._soa_serial = soa_serial + self._extra_a_record = extra_a_record + self._final_soa_mismatch = final_soa_mismatch + + @property + def initial_soa(self) -> dns.rrset.RRset: + return soa(self._soa_serial) + + @property + def zone_contents(self) -> Collection[dns.rrset.RRset]: + records = [ns(), txt(self._txt_data), a()] + if self._extra_a_record: + records.append(extra_a()) + return records + + @property + def final_soa(self) -> dns.rrset.RRset: + if self._final_soa_mismatch: + return soa(self._soa_serial, mname="whatever.", rname="other.") + return soa(self._soa_serial) + + +class WrongQnameInFinalSoa(ResponseHandlerWrapper): + def __init__(self, inner: XferAxfrHandler) -> None: + super().__init__(inner) + self._messages_until_final_soa = 2 + + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + if self._messages_until_final_soa == 0: + assert isinstance( + response_action, DnsResponseSend + ), "WrongQnameInFinalSoaAxfrHandler can only wrap handlers that yield DnsResponseSend from the final SOA response" + response_action.response.question[0].name = dns.name.from_text("ns.wrong.") + self._messages_until_final_soa -= 1 + + +class IxfrNotimpHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + return qctx.qtype == dns.rdatatype.IXFR + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_rcode(dns.rcode.NOTIMP) + yield DnsResponseSend(qctx.response) + + +class AxfrEdnsRcodeHandler(ResponseHandler): + def __init__(self, rcode: dns.rcode.Rcode) -> None: + self._rcode = rcode + + def match(self, qctx: QueryContext) -> bool: + return qctx.qtype == dns.rdatatype.AXFR and qctx.query.edns > -1 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_rcode(self._rcode) + yield DnsResponseSend(qctx.response) + + +def main() -> None: + server = ControllableAsyncDnsServer( + default_aa=True, default_rcode=dns.rcode.NOERROR, keyring=KEYRING + ) + switch_command = SwitchControlCommand( + { + "badkeydata": ( + SignResponses(SoaHandler(serial := 3)), + SignResponses( + XferAxfrHandler(soa_serial=serial, txt_data="bad keydata AXFR"), + KEY_WITH_BAD_DATA, + ), + ), + "badmessageid": ( + SignResponses(SoaHandler()), + SignResponses( + Add50ToMessageIdFromSecondResponse( + XferAxfrHandler(txt_data="bad message id") + ), + ), + ), + "ednsformerr": ( + SignResponses(SoaHandler()), + SignResponses(AxfrEdnsRcodeHandler(rcode=dns.rcode.FORMERR)), + SignResponses(XferAxfrHandler(txt_data="EDNS FORMERR")), + ), + "ednsnotimp": ( + SignResponses(SoaHandler()), + SignResponses(AxfrEdnsRcodeHandler(rcode=dns.rcode.NOTIMP)), + SignResponses(XferAxfrHandler(txt_data="EDNS NOTIMP")), + ), + "goodaxfr": ( + SignResponses(SoaHandler()), + SignResponses(XferAxfrHandler(txt_data="initial AXFR")), + ), + "ixfrnotimp": ( + SignResponses(SoaHandler(serial := 2)), + SignResponses(IxfrNotimpHandler()), + SignResponses( + XferAxfrHandler(soa_serial=serial, txt_data="IXFR NOTIMP") + ), + ), + "partial": ( + SignResponses(SoaHandler(serial := 4)), + SignFirstResponse( + XferAxfrHandler( + soa_serial=serial, + txt_data="partially signed AXFR", + extra_a_record=True, + ), + ), + ), + "soamismatch": ( + SignResponses(SoaHandler()), + SignResponses( + XferAxfrHandler( + txt_data="SOA mismatch AXFR", + final_soa_mismatch=True, + ) + ), + ), + "unknownkey": ( + SignResponses(SoaHandler(serial := 5), BAD_KEY), + SignResponses( + XferAxfrHandler( + soa_serial=serial, + txt_data="unknown key AXFR", + extra_a_record=True, + ), + BAD_KEY, + ), + ), + "unsigned": ( + SignResponses(SoaHandler(serial := 2)), + ClearTsig( + XferAxfrHandler( + soa_serial=serial, + txt_data="unsigned AXFR", + extra_a_record=True, + ) + ), + ), + "wrongkey": ( + SignResponses(SoaHandler(serial := 6), UNUSED_KEY), + SignResponses( + XferAxfrHandler( + soa_serial=serial, + txt_data="incorrect key AXFR", + extra_a_record=True, + ), + UNUSED_KEY, + ), + ), + "wrongname": ( + SignResponses(SoaHandler()), + SignResponses( + WrongQnameInFinalSoa( + XferAxfrHandler(txt_data="wrong question AXFR") + ) + ), + ), + } + ) + server.install_control_command(switch_command) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/xfer/ans5/badkeydata b/bin/tests/system/xfer/ans5/badkeydata deleted file mode 100644 index 8dc80fb1442..00000000000 --- a/bin/tests/system/xfer/ans5/badkeydata +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300 -/AXFR tsig_key abcd1234ffff/ -nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300 -/AXFR tsig_key abcd1234ffff/ -nil. 300 NS ns.nil. -nil. 300 TXT "bad keydata AXFR" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key abcd1234ffff/ -nil. 300 SOA ns.nil. root.nil. 3 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/badmessageid b/bin/tests/system/xfer/ans5/badmessageid deleted file mode 100644 index e0dc04168b5..00000000000 --- a/bin/tests/system/xfer/ans5/badmessageid +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR bad-id tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "bad message id" -a.nil. 60 A 10.0.0.61 -/AXFR bad-id tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/ednsformerr b/bin/tests/system/xfer/ans5/ednsformerr deleted file mode 100644 index 6ea77bef784..00000000000 --- a/bin/tests/system/xfer/ans5/ednsformerr +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR EDNS=FORMERR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR EDNS=FORMERR tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "EDNS FORMERR" -a.nil. 60 A 10.0.0.61 -/AXFR EDNS=FORMERR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/ednsnotimp b/bin/tests/system/xfer/ans5/ednsnotimp deleted file mode 100644 index a1df16b4069..00000000000 --- a/bin/tests/system/xfer/ans5/ednsnotimp +++ /dev/null @@ -1,12 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR EDNS=NOTIMP tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "EDNS NOTIMP" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/goodaxfr b/bin/tests/system/xfer/ans5/goodaxfr deleted file mode 100644 index e5ccd434240..00000000000 --- a/bin/tests/system/xfer/ans5/goodaxfr +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "initial AXFR" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/ixfrnotimp b/bin/tests/system/xfer/ans5/ixfrnotimp deleted file mode 100644 index a947a6346f0..00000000000 --- a/bin/tests/system/xfer/ans5/ixfrnotimp +++ /dev/null @@ -1,11 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300 -/IXFR NOTIMP tsig_key LSAnCU+Z/ -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "IXFR NOTIMP" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/partial b/bin/tests/system/xfer/ans5/partial deleted file mode 100644 index e7eff8e6474..00000000000 --- a/bin/tests/system/xfer/ans5/partial +++ /dev/null @@ -1,11 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 4 300 300 604800 300 -/AXFR/ -nil. 300 NS ns.nil. -nil. 300 TXT "partially signed 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. 4 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/soamismatch b/bin/tests/system/xfer/ans5/soamismatch deleted file mode 100644 index 14cfa4165c6..00000000000 --- a/bin/tests/system/xfer/ans5/soamismatch +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "SOA mismatch AXFR" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA whatever. other. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/unknownkey b/bin/tests/system/xfer/ans5/unknownkey deleted file mode 100644 index da7889bc05a..00000000000 --- a/bin/tests/system/xfer/ans5/unknownkey +++ /dev/null @@ -1,11 +0,0 @@ -/SOA bad_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 5 300 300 604800 300 -/AXFR bad_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 5 300 300 604800 300 -/AXFR bad_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "unknown key AXFR" -a.nil. 60 A 10.0.0.61 -b.nil. 60 A 10.0.0.62 -/AXFR bad_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 5 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/unsigned b/bin/tests/system/xfer/ans5/unsigned deleted file mode 100644 index 3fe04dbaf79..00000000000 --- a/bin/tests/system/xfer/ans5/unsigned +++ /dev/null @@ -1,11 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300 -/AXFR/ -nil. 300 SOA ns.nil. root.nil. 2 300 300 604800 300 -/AXFR/ -nil. 300 NS ns.nil. -nil. 300 TXT "unsigned 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. 2 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/wrongkey b/bin/tests/system/xfer/ans5/wrongkey deleted file mode 100644 index af120b07330..00000000000 --- a/bin/tests/system/xfer/ans5/wrongkey +++ /dev/null @@ -1,11 +0,0 @@ -/SOA unused_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 6 300 300 604800 300 -/AXFR unused_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 6 300 300 604800 300 -/AXFR unused_key LSAnCU+Z/ -nil. 300 NS ns.nil. -nil. 300 TXT "incorrect key AXFR" -a.nil. 60 A 10.0.0.61 -b.nil. 60 A 10.0.0.62 -/AXFR unused_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 6 300 300 604800 300 diff --git a/bin/tests/system/xfer/ans5/wrongname b/bin/tests/system/xfer/ans5/wrongname deleted file mode 100644 index 346ac3df621..00000000000 --- a/bin/tests/system/xfer/ans5/wrongname +++ /dev/null @@ -1,10 +0,0 @@ -/SOA tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 -/AXFR tsig_key LSAnCU+Z ns.wrong./ -nil. 300 NS ns.nil. -nil. 300 TXT "wrong question AXFR" -a.nil. 60 A 10.0.0.61 -/AXFR tsig_key LSAnCU+Z/ -nil. 300 SOA ns.nil. root.nil. 1 300 300 604800 300 diff --git a/bin/tests/system/xfer/tests_xfer.py b/bin/tests/system/xfer/tests_xfer.py index cbe49ce9edf..6fcfcef78d6 100644 --- a/bin/tests/system/xfer/tests_xfer.py +++ b/bin/tests/system/xfer/tests_xfer.py @@ -14,7 +14,6 @@ from re import compile as Re import fileinput import os -import socket import time import dns.message @@ -33,23 +32,17 @@ NEW_SOA_SERIAL = 1397051953 OLD_SOA_SERIAL = 1397051952 -def sendcmd(cmdfile): - host = "10.53.0.5" - port = int(isctest.vars.ALL["EXTRAPORT1"]) - cmdfile = f"ans5/{cmdfile}" - assert os.path.exists(cmdfile) - - sock = socket.create_connection((host, port)) - with open(cmdfile, "r", encoding="utf-8") as f: - for line in f: - sock.sendall(line.encode()) - sock.close() +def send_switch_control_command(command): + control_query = isctest.query.create( + f"{command}.switch._control.", dns.rdatatype.TXT + ) + isctest.query.tcp(control_query, "10.53.0.5") @pytest.fixture(scope="module", autouse=True) def after_servers_start(templates, ns4): # initial correctly-signed transfer should succeed - sendcmd("goodaxfr") + send_switch_control_command("goodaxfr") with ns4.watch_log_from_here() as watcher: templates.render("ns4/named.conf", {"ns4_as_secondary_for_nil": True}) @@ -299,13 +292,13 @@ def test_make_ns4_secondary_for_nil(): isctest.run.retry_with_timeout(_wait_for_soa, timeout=10) return True - sendcmd("goodaxfr") + send_switch_control_command("goodaxfr") assert wait_for_soa(), "SOA not found in the response" check_rdata_in_txt_record("initial AXFR") def test_handle_ixfr_notimp(ns4): - sendcmd("ixfrnotimp") + send_switch_control_command("ixfrnotimp") with ns4.watch_log_from_here() as watcher_transfer_success: with ns4.watch_log_from_here() as watcher_requesting_ixfr: ns4.rndc("refresh nil.") @@ -363,7 +356,7 @@ def test_handle_ixfr_notimp(ns4): ], ) def test_under_signed_transfer(command_file, expected_rdata, named_log_line, ns4): - sendcmd(command_file) + send_switch_control_command(command_file) with ns4.watch_log_from_here() as watcher: ns4.rndc("retransfer nil.") watcher.wait_for_line(named_log_line) @@ -371,14 +364,14 @@ def test_under_signed_transfer(command_file, expected_rdata, named_log_line, ns4 def test_handle_edns_notimp(ns4): - sendcmd("ednsnotimp") + send_switch_control_command("ednsnotimp") with ns4.watch_log_from_here() as watcher: ns4.rndc("retransfer nil.") watcher.wait_for_line("Transfer status: NOTIMP") def test_handle_edns_formerr(ns4): - sendcmd("ednsformerr") + send_switch_control_command("ednsformerr") with ns4.watch_log_from_here() as watcher: ns4.rndc("retransfer nil.") watcher.wait_for_line("Transfer status: success")