diff --git a/bin/tests/system/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index 165a6db9de..dd784ef5b6 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -642,6 +642,105 @@ class QnameHandler(ResponseHandler): return qctx.qname in self._qnames +class QnameQtypeHandler(QnameHandler): + """ + Handle queries for which both of the following conditions are true: + + - the query's QNAME is present in `self.qnames`, + - the query's QTYPE is present in `self.qtypes`. + """ + + @property + @abc.abstractmethod + def qtypes(self) -> List[dns.rdatatype.RdataType]: + """ + A list of QTYPEs handled by this class. + """ + raise NotImplementedError + + def __init__(self) -> None: + super().__init__() + self._qtypes: List[dns.rdatatype.RdataType] = self.qtypes + + def __str__(self) -> str: + return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)}; QTYPEs: {', '.join(map(str, self.qtypes))})" + + def match(self, qctx: QueryContext) -> bool: + """ + Handle queries whose QNAME and QTYPE match any of the QNAMEs and + QTYPEs handled by this class. + """ + return qctx.qtype in self._qtypes and super().match(qctx) + + +class StaticResponseHandler(ResponseHandler): + """ + Base class used for deriving custom static response handlers. + + The derived class can specify the RRsets to be included in the answer, + authority, and additional sections of the response, whether to set the AA + bit in the response, and a delay before sending the response. + + The default implementation of `get_responses()` uses these properties to + prepare and yield a single response. + """ + + @property + def rcode(self) -> Optional[dns.rcode.Rcode]: + """ + Optional RCODE to be set in the response. + """ + return None + + @property + def answer(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the answer section of the response. + """ + return [] + + @property + def authority(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the authority section of the response. + """ + return [] + + @property + def additional(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the additional section of the response. + """ + return [] + + @property + def authoritative(self) -> Optional[bool]: + """ + Whether to set the AA bit in the response. + """ + return None + + @property + def delay(self) -> float: + """ + Delay before sending the response. + """ + return 0.0 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.prepare_new_response(with_zone_data=False) + qctx.response.answer.extend(self.answer) + qctx.response.authority.extend(self.authority) + qctx.response.additional.extend(self.additional) + if self.rcode is not None: + qctx.response.set_rcode(self.rcode) + yield DnsResponseSend( + qctx.response, authoritative=self.authoritative, delay=self.delay + ) + + class DomainHandler(ResponseHandler): """ Base class used for deriving custom domain handlers. @@ -649,6 +748,8 @@ class DomainHandler(ResponseHandler): The derived class must specify a list of `domains` that it wants to handle. Queries for any of these domains (and their subdomains) will then be passed to the `get_response()` method in the derived class. + + The most specific matching domain is stored in the `matched_domain` attribute. """ @property @@ -660,9 +761,15 @@ class DomainHandler(ResponseHandler): raise NotImplementedError def __init__(self) -> None: - self._domains: List[dns.name.Name] = [ - dns.name.from_text(d) for d in self.domains - ] + self._domains: List[dns.name.Name] = sorted( + [dns.name.from_text(d) for d in self.domains], reverse=True + ) + self._matched_domain: Optional[dns.name.Name] = None + + @property + def matched_domain(self) -> dns.name.Name: + assert self._matched_domain is not None + return self._matched_domain def __str__(self) -> str: return f"{self.__class__.__name__}(domains: {', '.join(self.domains)})" @@ -672,8 +779,10 @@ class DomainHandler(ResponseHandler): Handle queries whose QNAME matches any of the domains handled by this class. """ + self._matched_domain = None for domain in self._domains: if qctx.qname.is_subdomain(domain): + self._matched_domain = domain return True return False diff --git a/bin/tests/system/resolver/ans10/ans.py b/bin/tests/system/resolver/ans10/ans.py index 6e95dbbfc6..1edb75db9e 100644 --- a/bin/tests/system/resolver/ans10/ans.py +++ b/bin/tests/system/resolver/ans10/ans.py @@ -9,144 +9,60 @@ # 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 -from datetime import datetime, timedelta -import time -import functools +from typing import AsyncGenerator -import dns, dns.message, dns.query, dns.flags -from dns.rdatatype import * -from dns.rdataclass import * -from dns.rcode import * -from dns.name import * +import dns.rcode +import dns.rdatatype + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + +from resolver_ans import rrset, soa_rrset -# Log query to file -def logquery(type, qname): - with open("qlog", "a") as f: - f.write("%s %s\n", type, qname) +class EdnsWithOptionsFormerrHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + return qctx.query.edns > -1 and qctx.query.options + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_rcode(dns.rcode.FORMERR) + # The test requires that the server echoes back the client cookie + qctx.response.opt = qctx.query.opt + yield DnsResponseSend(qctx.response, authoritative=False) -############################################################################ -# Respond to a DNS query. -# If there are EDNS options present return FORMERR copying the OPT record. -# Otherwise: -# SOA gets a unsigned response. -# NS gets a unsigned response. -# A gets a unsigned response. -# All other types get a unsigned NODATA response. -############################################################################ -def create_response(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) - - with open("query.log", "a") as f: - f.write("%s %s\n" % (typename, qname)) - print("%s %s" % (typename, qname), end=" ") - - if m.edns != -1 and len(m.options) != 0: - r = dns.message.make_response(m) - r.use_edns( - edns=m.edns, ednsflags=m.ednsflags, payload=m.payload, options=m.options - ) - r.set_rcode(FORMERR) - else: - r = dns.message.make_response(m) - r.set_rcode(NOERROR) - if rrtype == A: - r.answer.append(dns.rrset.from_text(qname, 1, IN, A, "10.53.0.10")) - elif rrtype == NS: - r.answer.append(dns.rrset.from_text(qname, 1, IN, NS, ".")) - elif rrtype == SOA: - r.answer.append(dns.rrset.from_text(qname, 1, IN, SOA, ". . 0 0 0 0 0")) +class FallbackHandler(ResponseHandler): + 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.10") + qctx.response.answer.append(a_rrset) + elif qctx.qtype == dns.rdatatype.NS: + ns_rrset = rrset(qctx.qname, dns.rdatatype.NS, ".") + qctx.response.answer.append(ns_rrset) + elif qctx.qtype == dns.rdatatype.SOA: + qctx.response.answer.append(soa_rrset(qctx.qname)) else: - r.authority.append(dns.rrset.from_text(qname, 1, IN, SOA, ". . 0 0 0 0 0")) - r.flags |= dns.flags.AA - return r + qctx.response.authority.append(soa_rrset(qctx.qname)) + + yield DnsResponseSend(qctx.response, authoritative=True) -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +def main() -> None: + server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + EdnsWithOptionsFormerrHandler(), + FallbackHandler(), + ) + server.run() -############################################################################ -# Main -# -# Set up responder and control channel, open the pid file, and start -# the main loop, listening for queries on the query channel or commands -# on the control channel and acting on them. -############################################################################ -ip4 = "10.53.0.10" -ip6 = "fd92:7065:b8e:ffff::10" - -try: - port = int(os.environ["PORT"]) -except: - port = 5300 - -query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -query4_socket.bind((ip4, port)) -havev6 = True -try: - query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) - try: - query6_socket.bind((ip6, port)) - except: - query6_socket.close() - havev6 = False -except: - havev6 = False -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)) -if havev6: - print("Listening on %s port %d" % (ip6, port)) -print("Ctrl-c to quit") - -if havev6: - input = [query4_socket, query6_socket] -else: - input = [query4_socket] - -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_socket or s == query6_socket: - print( - "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" " - ) - # Handle incoming queries - msg = s.recvfrom(65535) - rsp = create_response(msg[0]) - if rsp: - print(dns.rcode.to_text(rsp.rcode())) - s.sendto(rsp.to_wire(), msg[1]) - else: - print("NO RESPONSE") - if not running: - break +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resolver/ans2/ans.pl b/bin/tests/system/resolver/ans2/ans.pl deleted file mode 100644 index 8ec4b0a59b..0000000000 --- a/bin/tests/system/resolver/ans2/ans.pl +++ /dev/null @@ -1,195 +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. - -# -# Ad hoc name server -# - -use IO::File; -use IO::Socket; -use Net::DNS; -use Net::DNS::Packet; - -print "Using Net::DNS $Net::DNS::VERSION\n"; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $sock = IO::Socket::INET->new(LocalAddr => "10.53.0.2", - 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; - - if ($qname eq "com" && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("com 300 NS a.root-servers.nil.")); - } elsif ($qname eq "example.com" && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("example.com 300 NS a.root-servers.nil.")); - } elsif ($qname eq "cname1.example.com") { - # Data for the "cname + other data / 1" test - $packet->push("answer", new Net::DNS::RR("cname1.example.com 300 CNAME cname1.example.com")); - $packet->push("answer", new Net::DNS::RR("cname1.example.com 300 A 1.2.3.4")); - } elsif ($qname eq "cname2.example.com") { - # Data for the "cname + other data / 2" test: same RRs in opposite order - $packet->push("answer", new Net::DNS::RR("cname2.example.com 300 A 1.2.3.4")); - $packet->push("answer", new Net::DNS::RR("cname2.example.com 300 CNAME cname2.example.com")); - } elsif ($qname =~ /redirect\.com/) { - $packet->push("authority", new Net::DNS::RR("redirect.com 300 NS ns.redirect.com")); - $packet->push("additional", new Net::DNS::RR("ns.redirect.com 300 A 10.53.0.6")); - } elsif ($qname =~ /\.tld1/) { - $packet->push("authority", new Net::DNS::RR("tld1 300 NS ns.tld1")); - $packet->push("additional", new Net::DNS::RR("ns.tld1 300 A 10.53.0.6")); - } elsif ($qname =~ /\.tld2/) { - $packet->push("authority", new Net::DNS::RR("tld2 300 NS ns.tld2")); - $packet->push("additional", new Net::DNS::RR("ns.tld2 300 A 10.53.0.7")); - } elsif ($qname eq "org" && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("org 300 NS a.root-servers.nil.")); - } elsif ($qname eq "example.org" && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("example.org 300 NS a.root-servers.nil.")); - } elsif (($qname eq "baddname.example.org" || $qname eq "gooddname.example.org") && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("example.org 300 NS a.root-servers.nil.")); - } elsif ($qname eq "www.example.org" || - $qname eq "badcname.example.org" || - $qname eq "goodcname.example.org" || - $qname eq "foo.baddname.example.org" || - $qname eq "foo.gooddname.example.org") { - # Data for address/alias filtering. - $packet->header->aa(1); - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 A 192.0.2.1")); - } elsif ($qtype eq "AAAA") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 AAAA 2001:db8:beef::1")); - } - } elsif ($qname eq "net" && $qtype eq "NS") { - $packet->header->aa(1); - $packet->push("answer", new Net::DNS::RR("net 300 NS a.root-servers.nil.")); - } elsif ($qname eq "noresponse.exampleudp.net") { - next; - } elsif ($qname =~ /example\.net/) { - $packet->push("authority", new Net::DNS::RR("example.net 300 NS ns.example.net")); - $packet->push("additional", new Net::DNS::RR("ns.example.net 300 A 10.53.0.3")); - } elsif ($qname =~ /exampleudp\.net/) { - $packet->push("authority", new Net::DNS::RR("exampleudp.net 300 NS ns.exampleudp.net")); - $packet->push("additional", new Net::DNS::RR("ns.exampleudp.net 300 A 10.53.0.2")); - } elsif ($qname =~ /lame\.example\.org/) { - $packet->header->ad(0); - $packet->header->aa(0); - $packet->push("authority", new Net::DNS::RR("lame.example.org 300 NS ns.lame.example.org")); - $packet->push("additional", new Net::DNS::RR("ns.lame.example.org 300 A 10.53.0.3")); - } elsif ($qname =~ /sub\.example\.org/) { - # Data for CNAME/DNAME filtering. The final answers are - # expected to be accepted regardless of the filter setting. - $packet->push("authority", new Net::DNS::RR("sub.example.org 300 NS ns.sub.example.org")); - $packet->push("additional", new Net::DNS::RR("ns.sub.example.org 300 A 10.53.0.3")); - } elsif ($qname =~ /glue-in-answer\.example\.org/) { - $packet->push("answer", new Net::DNS::RR("ns.glue-in-answer.example.org 300 A 10.53.0.3")); - $packet->push("authority", new Net::DNS::RR("glue-in-answer.example.org 300 NS ns.glue-in-answer.example.org")); - $packet->push("additional", new Net::DNS::RR("ns.glue-in-answer.example.org 300 A 10.53.0.3")); - } elsif ($qname =~ /\.broken/ || $qname =~ /^broken/) { - # Delegation to broken TLD. - $packet->push("authority", new Net::DNS::RR("broken 300 NS ns.broken")); - $packet->push("additional", new Net::DNS::RR("ns.broken 300 A 10.53.0.4")); - } elsif ($qname =~ /\.partial-formerr/) { - $packet->header->rcode("FORMERR"); - } elsif ($qname eq "gl6412") { - if ($qtype eq "SOA") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } elsif ($qtype eq "NS") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 NS ns2" . $qname)); - $packet->push("answer", - new Net::DNS::RR($qname . " 300 NS ns3" . $qname)); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } elsif ($qname eq "a.gl6412" || $qname eq "a.a.gl6412") { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } elsif ($qname eq "ns2.gl6412") { - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 10.53.0.2")); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } elsif ($qname eq "ns3.gl6412") { - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 10.53.0.3")); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } elsif ($qname eq "zoneversion") { - $packet->push("authority", new Net::DNS::RR(". 300 SOA . . 0 0 0 0 0")); - if ($Net::DNS::VERSION >= 1.49) { - $packet->edns->option('ZONEVERSION' => [0, 1, '01022304'] ) - } elsif ($Net::DNS::VERSION >= 1.35) { - $packet->edns->option('19' => {'BASE16' => '000101022304'} ) - } else { - $packet->edns->option('19' => pack 'H*', '000101022304') - } - } else { - # Data for the "bogus referrals" test - $packet->push("authority", new Net::DNS::RR("below.www.example.com 300 NS ns.below.www.example.com")); - $packet->push("additional", new Net::DNS::RR("ns.below.www.example.com 300 A 10.53.0.3")); - } - - $sock->send($packet->data); - - print "RESPONSE:\n"; - $packet->print; - print "\n"; -} diff --git a/bin/tests/system/resolver/ans2/ans.py b/bin/tests/system/resolver/ans2/ans.py new file mode 100644 index 0000000000..74195b00cc --- /dev/null +++ b/bin/tests/system/resolver/ans2/ans.py @@ -0,0 +1,229 @@ +""" +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, Tuple, Union + +import dns.edns +import dns.name +import dns.rcode +import dns.rrset +import dns.rdatatype + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + DomainHandler, + IgnoreAllQueries, + QnameHandler, + QnameQtypeHandler, + QueryContext, + StaticResponseHandler, + ResponseHandler, +) + +from resolver_ans import ( + DelegationHandler, + Gl6412AHandler, + Gl6412Handler, + Gl6412Ns2Handler, + Gl6412Ns3Handler, + rrset, + setup_delegation, + soa_rrset, +) + + +class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler): + qnames = [ + "baddname.example.org.", + "gooddname.example.org.", + ] + qtypes = [dns.rdatatype.NS] + answer = [rrset("example.org.", dns.rdatatype.NS, "a.root-servers.nil.")] + authoritative = True + + +def _cname_rrsets( + qname: Union[dns.name.Name, str], +) -> Tuple[dns.rrset.RRset, dns.rrset.RRset]: + return ( + rrset(qname, dns.rdatatype.CNAME, f"{qname}"), + rrset(qname, dns.rdatatype.A, "1.2.3.4"), + ) + + +class Cname1Handler(QnameHandler, StaticResponseHandler): + qnames = ["cname1.example.com."] + # Data for the "cname + other data / 1" test + answer = _cname_rrsets(qnames[0]) + authoritative = False + + +class Cname2Handler(QnameHandler, StaticResponseHandler): + qnames = ["cname2.example.com."] + # Data for the "cname + other data / 2" test: same RRs in opposite order + answer = tuple(reversed(_cname_rrsets(qnames[0]))) + authoritative = False + + +class ExampleOrgHandler(QnameHandler): + qnames = [ + "www.example.org", + "badcname.example.org", + "goodcname.example.org", + "foo.baddname.example.org", + "foo.gooddname.example.org", + ] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + # Data for address/alias filtering. + if qctx.qtype == dns.rdatatype.A: + a_rrset = rrset(qctx.qname, dns.rdatatype.A, "192.0.2.1") + qctx.response.answer.append(a_rrset) + elif qctx.qtype == dns.rdatatype.AAAA: + aaaa_rrset = rrset(qctx.qname, dns.rdatatype.AAAA, "2001:db8:beef::1") + qctx.response.answer.append(aaaa_rrset) + yield DnsResponseSend(qctx.response, authoritative=True) + + +class NoResponseExampleUdpHandler(QnameHandler, IgnoreAllQueries): + qnames = ["noresponse.exampleudp.net."] + + +class RootNsHandler(QnameQtypeHandler): + qnames = [ + "example.com.", + "com.", + "example.org.", + "org.", + "net.", + ] + + qtypes = [dns.rdatatype.NS] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + root_ns = rrset(qctx.qname, dns.rdatatype.NS, "a.root-servers.nil.") + qctx.response.answer.append(root_ns) + yield DnsResponseSend(qctx.response, authoritative=True) + + +class ZoneVersionHandler(QnameHandler): + qnames = ["zoneversion."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.authority.append(soa_rrset(".")) + zoneversion_opt = dns.edns.GenericOption(19, bytes.fromhex("000101022304")) # type: ignore + qctx.response.use_edns(edns=0, options=[zoneversion_opt]) + yield DnsResponseSend(qctx.response, authoritative=False) + + +class Ns2Delegation(DelegationHandler): + domains = ["exampleudp.net."] + server_number = 2 + + +class Ns3Delegation(DelegationHandler): + domains = [ + "example.net.", + "lame.example.org.", + "sub.example.org.", + ] + server_number = 3 + + +class Ns3GlueInAnswerDelegation(DelegationHandler): + domains = ["glue-in-answer.example.org."] + server_number = 3 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + async for dns_response in super().get_responses(qctx): + dns_response.response.answer += dns_response.response.additional + yield dns_response + + +class Ns4Delegation(DelegationHandler): + domains = ["broken."] + server_number = 4 + + +class Ns6Delegation(DelegationHandler): + domains = [ + "redirect.com.", + "tld1.", + ] + server_number = 6 + + +class Ns7Delegation(DelegationHandler): + domains = ["tld2."] + server_number = 7 + + +class PartialFormerrHandler(DomainHandler, StaticResponseHandler): + domains = ["partial-formerr."] + authoritative = False + rcode = dns.rcode.FORMERR + + +class FallbackHandler(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + setup_delegation(qctx, "below.www.example.com.", 3) + yield DnsResponseSend(qctx.response, authoritative=False) + + +def main() -> None: + server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR) + + # Install QnameHandlers first + server.install_response_handlers( + BadGoodDnameNsHandler(), + Cname1Handler(), + Cname2Handler(), + ExampleOrgHandler(), + Gl6412AHandler(), + Gl6412Handler(), + Gl6412Ns2Handler(), + Gl6412Ns3Handler(), + NoResponseExampleUdpHandler(), + RootNsHandler(), + ZoneVersionHandler(), + ) + + # Then install DomainHandlers + server.install_response_handlers( + Ns2Delegation(), + Ns3Delegation(), + Ns3GlueInAnswerDelegation(), + Ns4Delegation(), + Ns6Delegation(), + Ns7Delegation(), + PartialFormerrHandler(), + ) + + # Finally, install the fallback handler + server.install_response_handler(FallbackHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resolver/ans3/ans.pl b/bin/tests/system/resolver/ans3/ans.pl deleted file mode 100644 index 02a8c1d6a6..0000000000 --- a/bin/tests/system/resolver/ans3/ans.pl +++ /dev/null @@ -1,234 +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. - -# -# Ad hoc name server -# - -use IO::File; -use IO::Socket; -use Net::DNS; -use Net::DNS::Packet; - -# 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 $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $server_addr = "10.53.0.3"; - -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 "$!"; - -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 handleQuery { - my $buf = shift; - 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->aa(1); - - my @questions = $packet->question; - my $qname = $questions[0]->qname; - my $qtype = $questions[0]->qtype; - - if ($qname eq "example.net" && $qtype eq "NS") { - $packet->push("answer", new Net::DNS::RR($qname . " 300 NS ns.example.net")); - $packet->push("additional", new Net::DNS::RR("ns.example.net 300 A 10.53.0.3")); - } elsif ($qname eq "ns.example.net") { - $packet->push("answer", new Net::DNS::RR($qname . " 300 A 10.53.0.3")); - } elsif ($qname eq "nodata.example.net") { - # Do not add a SOA RRset. - } elsif ($qname eq "noresponse.example.net") { - # Do not response. - print "RESPONSE:\n"; - return ""; - } elsif ($qname eq "nxdomain.example.net") { - # Do not add a SOA RRset. - $packet->header->rcode(NXDOMAIN); - } elsif ($qname eq "www.example.net") { - # Data for address/alias filtering. - if ($qtype eq "A") { - $packet->push("answer", new Net::DNS::RR($qname . " 300 A 192.0.2.1")); - } elsif ($qtype eq "AAAA") { - $packet->push("answer", new Net::DNS::RR($qname . " 300 AAAA 2001:db8:beef::1")); - } - } elsif ($qname eq "badcname.example.net") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 CNAME badcname.example.org")); - } elsif (($qname eq "baddname.example.net" || $qname eq "gooddname.example.net") && $qtype eq "NS") { - $packet->push("authority", new Net::DNS::RR("example.net IN SOA (1 2 3 4 5)")) - } elsif ($qname eq "foo.baddname.example.net") { - $packet->push("answer", - new Net::DNS::RR("baddname.example.net" . - " 300 DNAME baddname.example.org")); - } elsif ($qname eq "foo.gooddname.example.net") { - $packet->push("answer", - new Net::DNS::RR("gooddname.example.net" . - " 300 DNAME gooddname.example.org")); - } elsif ($qname eq "goodcname.example.net") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 CNAME goodcname.example.org")); - } elsif ($qname =~ /^longcname/) { - $cname = $qname =~ s/longcname/longcnamex/r; - $packet->push("answer", new Net::DNS::RR($qname . " 300 CNAME " . $cname)); - } elsif ($qname =~ /^nodata\.example\.net$/i) { - $packet->header->aa(1); - } elsif ($qname =~ /^nxdomain\.example\.net$/i) { - $packet->header->aa(1); - $packet->header->rcode(NXDOMAIN); - } elsif ($qname =~ /lame\.example\.org/) { - $packet->header->ad(0); - $packet->header->aa(0); - $packet->push("authority", new Net::DNS::RR("lame.example.org 300 NS ns.lame.example.org")); - $packet->push("additional", new Net::DNS::RR("ns.lame.example.org 300 A 10.53.0.3")); - } elsif ($qname eq "large-referral.example.net") { - for (my $i = 1; $i < 1000; $i++) { - $packet->push("authority", new Net::DNS::RR("large-referral.example.net 300 NS ns" . $i . ".fake.redirect.com")); - } - # No glue records - } elsif ($qname eq "foo.bar.sub.tld1") { - $packet->push("answer", new Net::DNS::RR("$qname 300 TXT baz")); - } elsif ($qname eq "cname.sub.example.org") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 CNAME ok.sub.example.org")); - } elsif ($qname eq "ok.sub.example.org") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 192.0.2.1")); - } elsif ($qname eq "www.dname.sub.example.org") { - $packet->push("answer", - new Net::DNS::RR("dname.sub.example.org" . - " 300 DNAME ok.sub.example.org")); - } elsif ($qname eq "www.ok.sub.example.org") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 192.0.2.1")); - } elsif ($qname eq "foo.glue-in-answer.example.org") { - $packet->push("answer", new Net::DNS::RR($qname . " 300 A 192.0.2.1")); - } elsif ($qname eq "ns.example.net") { - $packet->push("answer", - new Net::DNS::RR($qname . - " 300 A 10.53.0.3")); - } elsif ($qname =~ /\.partial-formerr/) { - $packet->push("answer", - new Net::DNS::RR($qname . " 1 A 10.53.0.3")); - } elsif ($qname eq "gl6412") { - if ($qtype eq "SOA") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } elsif ($qtype eq "NS") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 NS ns2" . $qname)); - $packet->push("answer", - new Net::DNS::RR($qname . " 300 NS ns3" . $qname)); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } elsif ($qname eq "a.gl6412" || $qname eq "a.a.gl6412") { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } elsif ($qname eq "ns2.gl6412") { - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 10.53.0.2")); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } elsif ($qname eq "ns3.gl6412") { - if ($qtype eq "A") { - $packet->push("answer", - new Net::DNS::RR($qname . " 300 A 10.53.0.3")); - } else { - $packet->push("authority", - new Net::DNS::RR($qname . " 300 SOA . . 0 0 0 0 0")); - } - } else { - $packet->push("answer", new Net::DNS::RR("www.example.com 300 A 1.2.3.4")); - } - - print "RESPONSE:\n"; - $packet->print; - - return $packet->data; -} - -# 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 = handleQuery($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 = handleQuery($buf); - $len = length($result); - if ($len != 0) { - $conn->syswrite(pack("n", $len), 2); - $n = $conn->syswrite($result, $len); - } else { - $n = 0; - } - print " Sent: $n chars via TCP\n"; - } - $conn->close; - } -} diff --git a/bin/tests/system/resolver/ans3/ans.py b/bin/tests/system/resolver/ans3/ans.py new file mode 100644 index 0000000000..bd21c9d4f7 --- /dev/null +++ b/bin/tests/system/resolver/ans3/ans.py @@ -0,0 +1,228 @@ +""" +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.name +import dns.rcode +import dns.rdatatype + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + DomainHandler, + IgnoreAllQueries, + QnameHandler, + QnameQtypeHandler, + QueryContext, + StaticResponseHandler, + ResponseHandler, +) + +from resolver_ans import ( + DelegationHandler, + Gl6412AHandler, + Gl6412Handler, + Gl6412Ns2Handler, + Gl6412Ns3Handler, + rrset, + rrset_from_list, + soa_rrset, +) + + +class ApexNSHandler(QnameHandler, StaticResponseHandler): + qnames = ["example.net."] + 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.3")] + + +class BadCnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["badcname.example.net."] + answer = [rrset(qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")] + + +class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler): + qnames = ["baddname.example.net.", "gooddname.example.net."] + qtypes = [dns.rdatatype.NS] + authority = [soa_rrset("example.net.")] + + +class CnameSubHandler(QnameHandler, StaticResponseHandler): + qnames = ["cname.sub.example.org."] + answer = [rrset(qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")] + + +class FooBadDnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["foo.baddname.example.net."] + answer = [ + rrset("baddname.example.net.", dns.rdatatype.DNAME, "baddname.example.org.") + ] + + +class FooBarSubTld1Handler(QnameHandler, StaticResponseHandler): + qnames = ["foo.bar.sub.tld1."] + answer = [rrset(qnames[0], dns.rdatatype.TXT, "baz")] + + +class FooGlueInAnswerHandler(QnameHandler, StaticResponseHandler): + qnames = ["foo.glue-in-answer.example.org."] + answer = [rrset(qnames[0], dns.rdatatype.A, "192.0.2.1")] + + +class FooGoodDnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["foo.gooddname.example.net."] + answer = [ + rrset("gooddname.example.net.", dns.rdatatype.DNAME, "gooddname.example.org.") + ] + + +class GoodCnameHandler(QnameHandler, StaticResponseHandler): + qnames = ["goodcname.example.net."] + answer = [rrset(qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")] + + +class LameExampleOrgDelegation(DelegationHandler): + domains = ["lame.example.org."] + server_number = 3 + + +class LargeReferralHandler(QnameHandler, StaticResponseHandler): + qnames = ["large-referral.example.net."] + qtypes = [dns.rdatatype.NS] + authority = [ + rrset_from_list( + qnames[0], + dns.rdatatype.NS, + [f"ns{i}.fake.redirect.com." for i in range(1, 1000)], + ) + ] + + +class LongCnameHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + return qctx.qname.labels[0].startswith(b"longcname") + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + first_label = qctx.qname.labels[0].replace(b"longcname", b"longcnamex") + cname_target = f"{dns.name.Name((first_label,) + qctx.qname.labels[1:])}" + qctx.response.answer.append( + rrset(qctx.qname, dns.rdatatype.CNAME, cname_target) + ) + yield DnsResponseSend(qctx.response) + + +class NodataHandler(QnameHandler, StaticResponseHandler): + qnames = ["nodata.example.net."] + + +class NoresponseHandler(QnameHandler, IgnoreAllQueries): + qnames = ["noresponse.example.net."] + + +class NsHandler(QnameHandler, StaticResponseHandler): + qnames = ["ns.example.net."] + answer = [rrset(qnames[0], dns.rdatatype.A, "10.53.0.3")] + + +class NxdomainHandler(QnameHandler, StaticResponseHandler): + qnames = ["nxdomain.example.net."] + rcode = dns.rcode.NXDOMAIN + + +class OkSubHandler(QnameHandler): + qnames = ["ok.sub.example.org.", "www.ok.sub.example.org."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.answer.append(rrset(qctx.qname, dns.rdatatype.A, "192.0.2.1")) + yield DnsResponseSend(qctx.response) + + +class PartialFormerrHandler(DomainHandler): + domains = ["partial-formerr."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.answer.append( + rrset(qctx.qname, dns.rdatatype.A, "10.53.0.3", ttl=1) + ) + yield DnsResponseSend(qctx.response) + + +class WwwDnameSubHandler(QnameHandler, StaticResponseHandler): + qnames = ["www.dname.sub.example.org."] + answer = [ + rrset("dname.sub.example.org.", dns.rdatatype.DNAME, "ok.sub.example.org.") + ] + + +class WwwHandler(QnameHandler): + qnames = ["www.example.net."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + if qctx.qtype == dns.rdatatype.A: + qctx.response.answer.append(rrset(qctx.qname, dns.rdatatype.A, "192.0.2.1")) + elif qctx.qtype == dns.rdatatype.AAAA: + qctx.response.answer.append( + rrset(qctx.qname, dns.rdatatype.AAAA, "2001:db8:beef::1") + ) + yield DnsResponseSend(qctx.response) + + +class FallbackHandler(StaticResponseHandler): + answer = [rrset("www.example.com.", dns.rdatatype.A, "1.2.3.4")] + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + ApexNSHandler(), + BadCnameHandler(), + BadGoodDnameNsHandler(), + CnameSubHandler(), + FooBadDnameHandler(), + FooBarSubTld1Handler(), + FooGoodDnameHandler(), + FooGlueInAnswerHandler(), + Gl6412AHandler(), + Gl6412Handler(), + Gl6412Ns2Handler(), + Gl6412Ns3Handler(), + GoodCnameHandler(), + LameExampleOrgDelegation(), + LargeReferralHandler(), + LongCnameHandler(), + NodataHandler(), + NoresponseHandler(), + NsHandler(), + NxdomainHandler(), + OkSubHandler(), + PartialFormerrHandler(), + WwwDnameSubHandler(), + WwwHandler(), + ) + + server.install_response_handler(FallbackHandler()) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resolver/ans8/ans.pl b/bin/tests/system/resolver/ans8/ans.pl deleted file mode 100644 index a3d06b67d6..0000000000 --- a/bin/tests/system/resolver/ans8/ans.pl +++ /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 index 0000000000..25eac4247f --- /dev/null +++ b/bin/tests/system/resolver/ans8/ans.py @@ -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() diff --git a/bin/tests/system/resolver/resolver_ans.py b/bin/tests/system/resolver/resolver_ans.py new file mode 100644 index 0000000000..e5d7854fd9 --- /dev/null +++ b/bin/tests/system/resolver/resolver_ans.py @@ -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. +""" + +from typing import AsyncGenerator, List, NamedTuple, Union + +import abc + +import dns.name +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + DnsResponseSend, + DomainHandler, + QnameHandler, + QueryContext, +) + + +def rrset( + qname: Union[dns.name.Name, str], + rtype: dns.rdatatype.RdataType, + rdata: str, + ttl: int = 300, +) -> dns.rrset.RRset: + return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) + + +def rrset_from_list( + qname: Union[dns.name.Name, str], + rtype: dns.rdatatype.RdataType, + rdata_list: List[str], + ttl: int = 300, +) -> dns.rrset.RRset: + return dns.rrset.from_text_list(qname, ttl, dns.rdataclass.IN, rtype, rdata_list) + + +def soa_rrset(qname: Union[dns.name.Name, str]) -> dns.rrset.RRset: + return rrset(qname, dns.rdatatype.SOA, ". . 0 0 0 0 0") + + +class DelegationRRsets(NamedTuple): + ns_rrset: dns.rrset.RRset + a_rrset: dns.rrset.RRset + + +def delegation_rrsets( + owner: Union[dns.name.Name, str], + server_number: int, + ns_name: Union[dns.name.Name, str, None] = None, +) -> DelegationRRsets: + if ns_name is None: + ns_name = f"ns.{owner}" + ns_rrset = rrset(owner, dns.rdatatype.NS, f"{ns_name}") + a_rrset = rrset(ns_name, dns.rdatatype.A, f"10.53.0.{server_number}") + return DelegationRRsets(ns_rrset, a_rrset) + + +def setup_delegation( + qctx: QueryContext, owner: Union[dns.name.Name, str], server_number: int +) -> None: + delegation = delegation_rrsets(owner, server_number) + qctx.response.authority.append(delegation.ns_rrset) + qctx.response.additional.append(delegation.a_rrset) + + +class DelegationHandler(DomainHandler): + @property + @abc.abstractmethod + def server_number(self) -> int: + raise NotImplementedError + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + setup_delegation(qctx, self.matched_domain, self.server_number) + yield DnsResponseSend(qctx.response, authoritative=False) + + +class Gl6412AHandler(QnameHandler): + qnames = ["a.gl6412.", "a.a.gl6412."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.authority.append(soa_rrset(qctx.qname)) + yield DnsResponseSend(qctx.response) + + +class Gl6412Handler(QnameHandler): + qnames = ["gl6412."] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + if qctx.qtype == dns.rdatatype.SOA: + qctx.response.answer.append(soa_rrset(qctx.qname)) + elif qctx.qtype == dns.rdatatype.NS: + ns2_rrset = rrset(qctx.qname, dns.rdatatype.NS, f"ns2.{qctx.qname}") + ns3_rrset = rrset(qctx.qname, dns.rdatatype.NS, f"ns3.{qctx.qname}") + qctx.response.answer.append(ns2_rrset) + qctx.response.answer.append(ns3_rrset) + else: + qctx.response.authority.append(soa_rrset(qctx.qname)) + yield DnsResponseSend(qctx.response) + + +class Gl6412Ns2Handler(QnameHandler): + qnames = ["ns2.gl6412."] + + 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.2") + qctx.response.answer.append(a_rrset) + else: + qctx.response.authority.append(soa_rrset(qctx.qname)) + yield DnsResponseSend(qctx.response) + + +class Gl6412Ns3Handler(QnameHandler): + qnames = ["ns3.gl6412."] + + 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.3") + qctx.response.answer.append(a_rrset) + else: + qctx.response.authority.append(soa_rrset(qctx.qname)) + yield DnsResponseSend(qctx.response) diff --git a/bin/tests/system/resolver/tests_resolver.py b/bin/tests/system/resolver/tests_resolver.py index 1465269ebd..a7d1cdef43 100644 --- a/bin/tests/system/resolver/tests_resolver.py +++ b/bin/tests/system/resolver/tests_resolver.py @@ -11,7 +11,6 @@ import time - import isctest diff --git a/bin/tests/system/statistics/ans4/ans.py b/bin/tests/system/statistics/ans4/ans.py index 0a9d05e1b8..52e0a00d50 100644 --- a/bin/tests/system/statistics/ans4/ans.py +++ b/bin/tests/system/statistics/ans4/ans.py @@ -20,7 +20,6 @@ import dns.rrset from isctest.asyncserver import ( AsyncDnsServer, DnsResponseSend, - DomainHandler, IgnoreAllQueries, QnameHandler, QueryContext, @@ -28,150 +27,30 @@ from isctest.asyncserver import ( ) -def setup_delegation(qctx: QueryContext, owner: str) -> None: - ns_name = f"ns.{owner}" - ns_rrset = dns.rrset.from_text(owner, 300, qctx.qclass, dns.rdatatype.NS, ns_name) - a_rrset = dns.rrset.from_text( - ns_name, 300, qctx.qclass, dns.rdatatype.A, "10.53.0.3" - ) - qctx.response.authority.append(ns_rrset) - qctx.response.additional.append(a_rrset) - - -class BadGoodCnameHandler(QnameHandler): - qnames = [ - "badcname.example.net.", - "goodcname.example.net.", - ] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - # Data for CNAME/DNAME filtering. We need to make one-level - # delegation to avoid automatic acceptance for subdomain aliases - setup_delegation(qctx, "example.net.") - yield DnsResponseSend(qctx.response, authoritative=False) - - -class Cname1Handler(QnameHandler): - qnames = ["cname1.example.com."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - # Data for the "cname + other data / 1" test - cname_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, dns.rdatatype.CNAME, "cname1.example.com." - ) - a_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, dns.rdatatype.A, "1.2.3.4" - ) - qctx.response.answer.append(cname_rrset) - qctx.response.answer.append(a_rrset) - yield DnsResponseSend(qctx.response, authoritative=False) - - -class Cname2Handler(QnameHandler): - qnames = ["cname2.example.com."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - # Data for the "cname + other data / 2" test: same RRs in opposite order - a_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, dns.rdatatype.A, "1.2.3.4" - ) - cname_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, dns.rdatatype.CNAME, "cname2.example.com." - ) - qctx.response.answer.append(a_rrset) - qctx.response.answer.append(cname_rrset) - yield DnsResponseSend(qctx.response, authoritative=False) - - -class ExampleHandler(QnameHandler): - qnames = [ - "www.example.com.", - "www.example.net.", - "badcname.example.org.", - "goodcname.example.org.", - "foo.badcname.example.org.", - "foo.goodcname.example.org.", - ] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - # Data for address/alias filtering. - if qctx.qtype == dns.rdatatype.A: - a_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, qctx.qtype, "192.0.2.1" - ) - qctx.response.answer.append(a_rrset) - elif qctx.qtype == dns.rdatatype.AAAA: - aaaa_rrset = dns.rrset.from_text( - qctx.qname, 300, qctx.qclass, qctx.qtype, "2001:db8:beef::1" - ) - qctx.response.answer.append(aaaa_rrset) - yield DnsResponseSend(qctx.response, authoritative=True) - - class FooInfoHandler(QnameHandler, IgnoreAllQueries): qnames = ["foo.info."] -class NoDataHandler(DomainHandler): - domains = ["nodata.example.net."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - yield DnsResponseSend(qctx.response, authoritative=True) - - -class NxdomainHandler(DomainHandler): - domains = ["nxdomain.example.net."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.response.set_rcode(dns.rcode.NXDOMAIN) - yield DnsResponseSend(qctx.response, authoritative=True) - - -class SubHandler(DomainHandler): - domains = ["sub.example.org."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - # Data for CNAME/DNAME filtering. The final answers are - # expected to be accepted regardless of the filter setting. - setup_delegation(qctx, "sub.example.org.") - yield DnsResponseSend(qctx.response, authoritative=False) - - class FallbackHandler(ResponseHandler): async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - setup_delegation(qctx, "below.www.example.com.") - yield DnsResponseSend(qctx.response, authoritative=False) + name = "below.www.example.com." + ns_name = f"ns.{name}" + ns_rrset = dns.rrset.from_text( + name, 300, qctx.qclass, dns.rdatatype.NS, ns_name + ) + a_rrset = dns.rrset.from_text( + ns_name, 300, qctx.qclass, dns.rdatatype.A, "10.53.0.3" + ) + qctx.response.authority.append(ns_rrset) + qctx.response.additional.append(a_rrset) + yield DnsResponseSend(qctx.response) def main() -> None: - server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR) - server.install_response_handlers( - BadGoodCnameHandler(), - Cname1Handler(), - Cname2Handler(), - ExampleHandler(), - FooInfoHandler(), - NoDataHandler(), - NxdomainHandler(), - SubHandler(), - ) - server.install_response_handler(FallbackHandler()) + server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR, default_aa=False) + server.install_response_handlers(FooInfoHandler(), FallbackHandler()) server.run()