Reimplement 'resolver/ans2' server using AsyncDnsServer

Ensure packet-for-packet compatibility with the old server including
bugs.

(cherry picked from commit d203a39314)
This commit is contained in:
Štěpán Balážik 2025-12-25 17:03:20 +01:00
parent c11a7877b9
commit aa658f80d7
3 changed files with 225 additions and 185 deletions

View file

@ -1,184 +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;
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"));
}
} 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,225 @@
"""
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.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,
)
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 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)
# XXX: This handler is here to provide bug-for-bug compatibility with the old server.
class XXXBuggyTldNsHandler(QnameQtypeHandler, FallbackHandler):
qnames = [
"tld1.",
"tld2.",
]
qtypes = [dns.rdatatype.NS]
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(),
XXXBuggyTldNsHandler(),
)
# 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

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