]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Reimplement 'resolver/ans8' server using AsyncDnsServer
authorŠtěpán Balážik <stepan@isc.org>
Thu, 25 Dec 2025 16:03:20 +0000 (17:03 +0100)
committerŠtěpán Balážik <stepan@isc.org>
Sat, 24 Jan 2026 12:04:09 +0000 (13:04 +0100)
Ensure packet-for-packet compatibility with the old server.

bin/tests/system/resolver/ans8/ans.pl [deleted file]
bin/tests/system/resolver/ans8/ans.py [new file with mode: 0644]

diff --git a/bin/tests/system/resolver/ans8/ans.pl b/bin/tests/system/resolver/ans8/ans.pl
deleted file mode 100644 (file)
index a3d06b6..0000000
+++ /dev/null
@@ -1,177 +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.
-
-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.8";
-
-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;
-
-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 $response = new Net::DNS::Packet($qname, $qtype, $qclass);
-       $response->header->qr(1);
-       $response->header->aa(1);
-       $response->header->tc(0);
-       $response->header->id($id);
-
-       # Responses to queries for no-questions/NS and ns.no-questions/A are
-       # _not_ malformed or truncated.
-       if ($qname eq "no-questions" && $qtype eq "NS") {
-               $response->push("answer", new Net::DNS::RR($qname . " 300 NS ns.no-questions"));
-               $response->push("additional", new Net::DNS::RR("ns.no-questions. 300 A 10.53.0.8"));
-               return $response->data;
-       } elsif ($qname eq "ns.no-questions") {
-               $response->push("answer", new Net::DNS::RR($qname . " 300 A 10.53.0.8"))
-                       if ($qtype eq "A");
-               return $response->data;
-       } elsif ($qname =~ /\.formerr-to-all$/) {
-               $response->header->rcode("FORMERR");
-               return $response->data;
-       }
-
-       # don't use Net::DNS to construct the header only reply as early
-       # versions just get it completely wrong.
-
-       if ($qname eq "truncated.no-questions") {
-               # QR, AA, TC: forces TCP retry
-               return (pack("nnnnnn", $id, 0x8600, 0, 0, 0, 0));
-       } elsif ($qname eq "tcpalso.no-questions") {
-               # QR, REFUSED: forces TCP retry
-               return (pack("nnnnnn", $id, 0x8205, 0, 0, 0, 0));
-       }
-       # QR, AA
-       return (pack("nnnnnn", $id, 0x8400, 0, 0, 0, 0));
-}
-
-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 @results = ();
-       my $response = new Net::DNS::Packet($qname, $qtype, $qclass);
-
-       $response->header->qr(1);
-       $response->header->aa(1);
-       $response->header->id($id);
-       $response->push("answer", new Net::DNS::RR("$qname 300 A 1.2.3.4"));
-
-       if ($qname eq "tcpalso.no-questions") {
-               # for this qname we also return a bad reply over TCP
-               # QR, REFUSED, no question section
-               push (@results, pack("nnnnnn", $id, 0x8005, 0, 0, 0, 0));
-       } else {
-               push(@results, $response->data);
-       }
-
-       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);
-               my $result = handleUDP($buf);
-               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 = handleTCP($buf);
-                       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/resolver/ans8/ans.py b/bin/tests/system/resolver/ans8/ans.py
new file mode 100644 (file)
index 0000000..25eac42
--- /dev/null
@@ -0,0 +1,144 @@
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0.  If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+import abc
+
+from typing import AsyncGenerator
+
+import dns.flags
+import dns.message
+import dns.rcode
+import dns.rdatatype
+
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsResponseSend,
+    DnsProtocol,
+    DomainHandler,
+    QnameHandler,
+    QnameQtypeHandler,
+    QueryContext,
+    ResponseHandler,
+    StaticResponseHandler,
+)
+
+from resolver_ans import rrset
+
+
+class HeaderOnlyHandler(ResponseHandler):
+    """
+    Return an empty DNS message with only header flags set.
+    """
+
+    @property
+    @abc.abstractmethod
+    def flags(self) -> dns.flags.Flag:
+        raise NotImplementedError
+
+    @property
+    def rcode(self) -> dns.rcode.Rcode:
+        return dns.rcode.NOERROR
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        message = dns.message.Message(id=qctx.query.id)
+        message.use_edns(False)
+        message.flags = self.flags
+        message.set_rcode(self.rcode)
+        yield DnsResponseSend(message, acknowledge_hand_rolled_response=True)
+
+
+class RefusedOnTcpHandler(QnameHandler, HeaderOnlyHandler):
+    qnames = ["tcpalso.no-questions."]
+    flags = dns.flags.QR
+    rcode = dns.rcode.REFUSED
+
+    def match(self, qctx: QueryContext) -> bool:
+        return qctx.protocol == DnsProtocol.TCP and super().match(qctx)
+
+
+class TcpFallbackHandler(ResponseHandler):
+    def match(self, qctx: QueryContext) -> bool:
+        return qctx.protocol == DnsProtocol.TCP
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        qctx.response.answer.append(rrset(qctx.qname, dns.rdatatype.A, "1.2.3.4"))
+        yield DnsResponseSend(qctx.response)
+
+
+class FormerrToAllHandler(DomainHandler, StaticResponseHandler):
+    domains = ["formerr-to-all."]
+    rcode = dns.rcode.FORMERR
+
+
+class NoQuestionsNSHandler(QnameQtypeHandler, StaticResponseHandler):
+    qnames = ["no-questions."]
+    qtypes = [dns.rdatatype.NS]
+    answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")]
+    additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.8")]
+
+
+class NsNoQuestionsAHandler(QnameHandler):
+    qnames = ["ns.no-questions."]
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        if qctx.qtype == dns.rdatatype.A:
+            a_rrset = rrset(qctx.qname, dns.rdatatype.A, "10.53.0.8")
+            qctx.response.answer.append(a_rrset)
+        yield DnsResponseSend(qctx.response)
+
+
+class TcpalsoNoQuestionsHandler(QnameHandler, HeaderOnlyHandler):
+    qnames = ["tcpalso.no-questions."]
+    flags = dns.flags.QR | dns.flags.TC
+    rcode = dns.rcode.REFUSED
+
+
+class TruncatedNoQuestionsHandler(QnameHandler, HeaderOnlyHandler):
+    qnames = ["truncated.no-questions."]
+    flags = dns.flags.QR | dns.flags.AA | dns.flags.TC
+
+
+class FallbackHandler(HeaderOnlyHandler):
+    flags = dns.flags.QR | dns.flags.AA
+
+
+def main() -> None:
+    server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
+
+    # Install TCP handlers first so they take precedence
+    server.install_response_handlers(
+        RefusedOnTcpHandler(),
+        TcpFallbackHandler(),
+    )
+
+    # Install UDP handlers
+    server.install_response_handlers(
+        FormerrToAllHandler(),
+        NoQuestionsNSHandler(),
+        NsNoQuestionsAHandler(),
+        TcpalsoNoQuestionsHandler(),
+        TruncatedNoQuestionsHandler(),
+    )
+    server.install_response_handler(FallbackHandler())
+
+    server.run()
+
+
+if __name__ == "__main__":
+    main()