chg: test: Use isctest.asyncserver in the "resolver" system test

Depends on !11403, !11419

Merge branch 'stepan/resolver-asyncserver' into 'main'

See merge request isc-projects/bind9!11411
This commit is contained in:
Štěpán Balážik 2026-01-24 15:23:35 +00:00
commit 11e6d1c0b9
11 changed files with 917 additions and 875 deletions

View file

@ -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

View file

@ -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()

View file

@ -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";
}

View file

@ -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()

View file

@ -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;
}
}

View file

@ -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()

View file

@ -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;
}
}

View file

@ -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()

View file

@ -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)

View file

@ -11,7 +11,6 @@
import time
import isctest

View file

@ -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()