+++ /dev/null
-#!/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;
- }
-}
--- /dev/null
+"""
+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()