diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl deleted file mode 100644 index e42240be63..0000000000 --- a/bin/tests/system/chain/ans3/ans.pl +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env 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 strict; -use warnings; - -use IO::File; -use Getopt::Long; -use Net::DNS::Nameserver; - -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; }; -sub term { }; - -$SIG{INT} = \&rmpid; -if ($Net::DNS::VERSION > 1.41) { - $SIG{TERM} = \&term; -} else { - $SIG{TERM} = \&rmpid; -} - -my $localaddr = "10.53.0.3"; - -my $localport = int($ENV{'PORT'}); -if (!$localport) { $localport = 5300; } - -my $verbose = 0; -my $ttl = 60; -my $zone = "example.broken"; -my $nsname = "ns3.$zone"; -my $synth = "synth-then-dname.$zone"; -my $synth2 = "synth2-then-dname.$zone"; - -sub reply_handler { - my ($qname, $qclass, $qtype, $peerhost, $query, $conn) = @_; - my ($rcode, @ans, @auth, @add); - - print ("request: $qname/$qtype\n"); - STDOUT->flush(); - - if ($qname eq "example.broken") { - if ($qtype eq "SOA") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass SOA . . 0 0 0 0 0"); - push @ans, $rr; - } elsif ($qtype eq "NS") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass NS $nsname"); - push @ans, $rr; - $rr = new Net::DNS::RR("$nsname $ttl $qclass A $localaddr"); - push @add, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "cname-to-$synth2") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.$synth2"); - push @ans, $rr; - $rr = new Net::DNS::RR("name.$synth2 $ttl $qclass CNAME name"); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "$synth" || $qname eq "$synth2") { - if ($qtype eq "DNAME") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME ."); - push @ans, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "name.$synth") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "name.$synth2") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); - push @ans, $rr; - $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); - push @ans, $rr; - $rcode = "NOERROR"; - # The following three code branches referring to the "example.dname" - # zone are necessary for the resolver variant of the CVE-2021-25215 - # regression test to work. A named instance cannot be used for - # serving the DNAME records below as a version of BIND vulnerable to - # CVE-2021-25215 would crash while answering the queries asked by - # the tested resolver. - } elsif ($qname eq "ns3.example.dname") { - if ($qtype eq "A") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass A 10.53.0.3"); - push @ans, $rr; - } - if ($qtype eq "AAAA") { - my $rr = new Net::DNS::RR("example.dname. $ttl $qclass SOA . . 0 0 0 0 $ttl"); - push @auth, $rr; - } - $rcode = "NOERROR"; - } elsif ($qname eq "self.example.self.example.dname") { - my $rr = new Net::DNS::RR("self.example.dname. $ttl $qclass DNAME dname."); - push @ans, $rr; - $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME self.example.dname."); - push @ans, $rr; - $rcode = "NOERROR"; - } elsif ($qname eq "self.example.dname") { - if ($qtype eq "DNAME") { - my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME dname."); - push @ans, $rr; - } - $rcode = "NOERROR"; - } else { - $rcode = "REFUSED"; - } - return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); -} - -GetOptions( - 'port=i' => \$localport, - 'verbose!' => \$verbose, -); - -my $ns = Net::DNS::Nameserver->new( - LocalAddr => $localaddr, - LocalPort => $localport, - ReplyHandler => \&reply_handler, - Verbose => $verbose, -); - -if ($Net::DNS::VERSION >= 1.42) { - $ns->start_server(); - select(undef, undef, undef, undef); - $ns->stop_server(); - unlink "ans.pid"; -} else { - $ns->main_loop; -} diff --git a/bin/tests/system/chain/ans3/ans.py b/bin/tests/system/chain/ans3/ans.py new file mode 100644 index 0000000000..0a031c1145 --- /dev/null +++ b/bin/tests/system/chain/ans3/ans.py @@ -0,0 +1,217 @@ +# 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. + +############################################################################ +# ans.py: See README.anspy for details. +############################################################################ + +from __future__ import print_function +import os +import sys +import signal +import socket +import select +from datetime import datetime, timedelta +import functools + +import dns, dns.message, dns.query +from dns.rdatatype import * +from dns.rdataclass import * +from dns.rcode import * +from dns.name import * + + +############################################################################ +# Respond to a DNS query. +############################################################################ +def create_response(msg): + ttl = 60 + zone = "example.broken." + nsname = f"ns3.{zone}" + synth = f"synth-then-dname.{zone}" + synth2 = f"synth2-then-dname.{zone}" + + m = dns.message.from_wire(msg) + qname = m.question[0].name.to_text() + + # prepare the response and convert to wire format + r = dns.message.make_response(m) + + # get qtype + rrtype = m.question[0].rdtype + qtype = dns.rdatatype.to_text(rrtype) + print(f"request: {qname}/{qtype}") + + rcode = "NOERROR" + if qname == zone: + if qtype == "SOA": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, SOA, ". . 0 0 0 0 0")) + elif qtype == "NS": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, NS, nsname)) + r.additional.append(dns.rrset.from_text(nsname, ttl, IN, A, ip4)) + elif qname == f"cname-to-{synth2}": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, f"name.{synth2}")) + r.answer.append(dns.rrset.from_text(f"name.{synth2}", ttl, IN, CNAME, "name.")) + r.answer.append(dns.rrset.from_text(synth2, ttl, IN, DNAME, ".")) + elif qname == f"{synth}" or qname == f"{synth2}": + if qtype == "DNAME": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, DNAME, ".")) + elif qname == f"name.{synth}": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name.")) + r.answer.append(dns.rrset.from_text(synth, ttl, IN, DNAME, ".")) + elif qname == f"name.{synth2}": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name.")) + r.answer.append(dns.rrset.from_text(synth2, ttl, IN, DNAME, ".")) + elif qname == "ns3.example.dname.": + # This and the next two code branches referring to the "example.dname" + # zone are necessary for the resolver variant of the CVE-2021-25215 + # regression test to work. A named instance cannot be used for + # serving the DNAME records below as a version of BIND vulnerable to + # CVE-2021-25215 would crash while answering the queries asked by + # the tested resolver. + if qtype == "A": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, A, ip4)) + elif qtype == "AAAA": + r.authority.append( + dns.rrset.from_text("example.dname.", ttl, IN, SOA, ". . 0 0 0 0 0") + ) + elif qname == "self.example.self..example.dname.": + r.answer.append( + dns.rrset.from_text("self.example.dname.", ttl, IN, DNAME, "dname.") + ) + r.answer.append( + dns.rrset.from_text(qname, ttl, IN, CNAME, "self.example.dname.") + ) + elif qname == "self.example.dname.": + if qtype == "DNAME": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, DNAME, "dname.")) + else: + rcode = "REFUSED" + + r.flags |= dns.flags.AA + r.use_edns() + return r.to_wire() + + +def sigterm(signum, frame): + print("Shutting down now...") + os.remove("ans.pid") + running = False + sys.exit(0) + + +############################################################################ +# 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.3" +ip6 = "fd92:7065:b8e:ffff::3" + +try: + port = int(os.environ["PORT"]) +except: + port = 5300 + +query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +query4_udp.bind((ip4, port)) + +query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +query4_tcp.bind((ip4, port)) +query4_tcp.listen(1) +query4_tcp.settimeout(1) + +havev6 = True +try: + query6_udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + try: + query6_udp.bind((ip6, port)) + except: + query6_udp.close() + havev6 = False + + query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + query6_tcp.bind((ip4, port)) + query6_tcp.listen(1) + query6_tcp.settimeout(1) + except: + query6_tcp.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_udp, query4_tcp, query6_udp, query6_tcp] +else: + input = [query4_udp, query4_tcp] + +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_udp or s == query6_udp: + print("Query received on %s" % (ip4 if s == query4_udp else ip6)) + # Handle incoming queries + msg = s.recvfrom(65535) + rsp = create_response(msg[0]) + if rsp: + s.sendto(rsp, msg[1]) + elif s == query4_tcp or s == query6_tcp: + try: + conn, _ = s.accept() + if s == query4_tcp or s == query6_tcp: + print( + "TCP Query received on %s" % (ip4 if s == query4_tcp else ip6), + end=" ", + ) + # get TCP message length + msg = conn.recv(2) + if len(msg) != 2: + print("couldn't read TCP message length") + continue + length = struct.unpack(">H", msg[:2])[0] + msg = conn.recv(length) + if len(msg) != length: + print("couldn't read TCP message") + continue + rsp = create_response(msg) + if rsp: + conn.send(struct.pack(">H", len(rsp))) + conn.send(rsp) + conn.close() + except socket.error as e: + print("error: %s" % str(e)) + if not running: + break diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py index 839067faa5..66f0193cac 100755 --- a/bin/tests/system/chain/ans4/ans.py +++ b/bin/tests/system/chain/ans4/ans.py @@ -316,16 +316,30 @@ try: except: ctrlport = 5300 -query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -query4_socket.bind((ip4, port)) +query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +query4_udp.bind((ip4, port)) + +query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +query4_tcp.bind((ip4, port)) +query4_tcp.listen(1) +query4_tcp.settimeout(1) havev6 = True try: - query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + query6_udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) try: - query6_socket.bind((ip6, port)) + query6_udp.bind((ip6, port)) except: - query6_socket.close() + query6_udp.close() + havev6 = False + + query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + query6_tcp.bind((ip4, port)) + query6_tcp.listen(1) + query6_tcp.settimeout(1) + except: + query6_tcp.close() havev6 = False except: havev6 = False @@ -350,9 +364,9 @@ print("Control channel on %s port %d" % (ip4, ctrlport)) print("Ctrl-c to quit") if havev6: - input = [query4_socket, query6_socket, ctrl_socket] + input = [query4_udp, query4_tcp, query6_udp, query6_tcp, ctrl_socket] else: - input = [query4_socket, ctrl_socket] + input = [query4_udp, query4_tcp, ctrl_socket] while running: try: @@ -375,12 +389,37 @@ while running: break ctl_channel(msg) conn.close() - if s == query4_socket or s == query6_socket: - print("Query received on %s" % (ip4 if s == query4_socket else ip6)) + elif s == query4_udp or s == query6_udp: + print("Query received on %s" % (ip4 if s == query4_udp else ip6)) # Handle incoming queries msg = s.recvfrom(65535) rsp = create_response(msg[0]) if rsp: s.sendto(rsp, msg[1]) + elif s == query4_tcp or s == query6_tcp: + try: + conn, _ = s.accept() + if s == query4_tcp or s == query6_tcp: + print( + "TCP Query received on %s" % (ip4 if s == query4_tcp else ip6), + end=" ", + ) + # get TCP message length + msg = conn.recv(2) + if len(msg) != 2: + print("couldn't read TCP message length") + continue + length = struct.unpack(">H", msg[:2])[0] + msg = conn.recv(length) + if len(msg) != length: + print("couldn't read TCP message") + continue + rsp = create_response(msg) + if rsp: + conn.send(struct.pack(">H", len(rsp))) + conn.send(rsp) + conn.close() + except socket.error as e: + print("error: %s" % str(e)) if not running: break diff --git a/lib/dns/include/dns/message.h b/lib/dns/include/dns/message.h index b4c2c8ee9c..d90a0837df 100644 --- a/lib/dns/include/dns/message.h +++ b/lib/dns/include/dns/message.h @@ -284,6 +284,7 @@ struct dns_message { unsigned int tkey : 1; unsigned int rdclass_set : 1; unsigned int fuzzing : 1; + unsigned int has_dname : 1; unsigned int opt_reserved; unsigned int sig_reserved; @@ -1527,4 +1528,11 @@ dns_message_response_minttl(dns_message_t *msg, dns_ttl_t *pttl); * \li 'pttl != NULL'. */ +bool +dns_message_hasdname(dns_message_t *msg); +/*%< + * Return whether a DNAME was detected in the ANSWER section of a QUERY + * message when it was parsed. + */ + ISC_LANG_ENDDECLS diff --git a/lib/dns/message.c b/lib/dns/message.c index 22d328c0f1..541a854db0 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -428,6 +428,7 @@ msginit(dns_message_t *m) { m->cc_bad = 0; m->tkey = 0; m->rdclass_set = 0; + m->has_dname = 0; m->querytsig = NULL; m->indent.string = "\t"; m->indent.count = 0; @@ -1654,6 +1655,11 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t *dctx, */ msg->tsigname->attributes |= DNS_NAMEATTR_NOCOMPRESS; free_name = false; + } else if (rdtype == dns_rdatatype_dname && + sectionid == DNS_SECTION_ANSWER && + msg->opcode == dns_opcode_query) + { + msg->has_dname = 1; } rdataset = NULL; @@ -4798,3 +4804,9 @@ dns_message_response_minttl(dns_message_t *msg, dns_ttl_t *pttl) { return ISC_R_SUCCESS; } + +bool +dns_message_hasdname(dns_message_t *msg) { + REQUIRE(DNS_MESSAGE_VALID(msg)); + return msg->has_dname; +} diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 4364f0ac19..1bacec6470 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -808,6 +808,7 @@ typedef struct respctx { bool get_nameservers; /* get a new NS rrset at * zone cut? */ bool resend; /* resend this query? */ + bool secured; /* message was signed or had a valid cookie */ bool nextitem; /* invalid response; keep * listening for the correct one */ bool truncated; /* response was truncated */ @@ -7894,6 +7895,47 @@ betterreferral(respctx_t *rctx) { return false; } +static bool +rctx_need_tcpretry(respctx_t *rctx) { + resquery_t *query = rctx->query; + if ((rctx->retryopts & DNS_FETCHOPT_TCP) != 0) { + /* TCP is already in the retry flags */ + return false; + } + + /* + * If the message was secured, no need to continue. + */ + if (rctx->secured) { + return false; + } + + /* + * Currently the only extra reason why we might need to + * retry a UDP response over TCP is a DNAME in the message. + */ + if (dns_message_hasdname(query->rmessage)) { + return true; + } + + return false; +} + +static isc_result_t +rctx_tcpretry(respctx_t *rctx) { + /* + * Do we need to retry a UDP response over TCP? + */ + if (rctx_need_tcpretry(rctx)) { + rctx->retryopts |= DNS_FETCHOPT_TCP; + rctx->resend = true; + rctx_done(rctx, ISC_R_SUCCESS); + return ISC_R_COMPLETE; + } + + return ISC_R_SUCCESS; +} + /* * resquery_response(): * Handles responses received in response to iterative queries sent by @@ -8083,6 +8125,17 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { return; } + /* + * Remember whether this message was signed or had a + * valid client cookie; if not, we may need to retry over + * TCP later. + */ + if (query->rmessage->cc_ok || query->rmessage->tsig != NULL || + query->rmessage->sig0 != NULL) + { + rctx.secured = true; + } + /* * The dispatcher should ensure we only get responses with QR * set. @@ -8094,10 +8147,7 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { * TCP. This may be a misconfigured anycast server or an attempt * to send a spoofed response. Skip if we have a valid tsig. */ - if (dns_message_gettsig(query->rmessage, NULL) == NULL && - !query->rmessage->cc_ok && !query->rmessage->cc_bad && - (rctx.retryopts & DNS_FETCHOPT_TCP) == 0) - { + if (!rctx.secured && (rctx.retryopts & DNS_FETCHOPT_TCP) == 0) { unsigned char cookie[COOKIE_BUFFER_SIZE]; if (dns_adb_getcookie(fctx->adb, query->addrinfo, cookie, sizeof(cookie)) > CLIENT_COOKIE_SIZE) @@ -8109,8 +8159,7 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { isc_log_write( dns_lctx, DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, ISC_LOG_INFO, - "missing expected cookie " - "from %s", + "missing expected cookie from %s", addrbuf); } rctx.retryopts |= DNS_FETCHOPT_TCP; @@ -8120,6 +8169,17 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { } } + /* + * Check whether we need to retry over TCP for some other reason. + */ + result = rctx_tcpretry(&rctx); + if (result == ISC_R_COMPLETE) { + return; + } + + /* + * Check for EDNS issues. + */ rctx_edns(&rctx); /* @@ -8851,8 +8911,8 @@ rctx_answer_positive(respctx_t *rctx) { } /* - * Cache records in the authority section, if - * there are any suitable for caching. + * Cache records in the authority section, if there are + * any suitable for caching. */ rctx_authority_positive(rctx); @@ -9225,14 +9285,14 @@ rctx_answer_dname(respctx_t *rctx) { /* * rctx_authority_positive(): - * Examine the records in the authority section (if there are any) for a - * positive answer. We expect the names for all rdatasets in this - * section to be subdomains of the domain being queried; any that are - * not are skipped. We expect to find only *one* owner name; any names - * after the first one processed are ignored. We expect to find only - * rdatasets of type NS, RRSIG, or SIG; all others are ignored. Whatever - * remains can be cached at trust level authauthority or additional - * (depending on whether the AA bit was set on the answer). + * If a positive answer was received over TCP or secured with a cookie + * or TSIG, examine the authority section. We expect names for all + * rdatasets in this section to be subdomains of the domain being queried; + * any that are not are skipped. We expect to find only *one* owner name; + * any names after the first one processed are ignored. We expect to find + * only rdatasets of type NS; all others are ignored. Whatever remains can + * be cached at trust level authauthority or additional (depending on + * whether the AA bit was set on the answer). */ static void rctx_authority_positive(respctx_t *rctx) { @@ -9240,6 +9300,11 @@ rctx_authority_positive(respctx_t *rctx) { bool done = false; isc_result_t result; + /* If it's spoofable, don't cache it. */ + if (!rctx->secured && (rctx->query->options & DNS_FETCHOPT_TCP) == 0) { + return; + } + result = dns_message_firstname(rctx->query->rmessage, DNS_SECTION_AUTHORITY); while (!done && result == ISC_R_SUCCESS) {