From: Evan Hunt Date: Tue, 30 Sep 2025 05:42:44 +0000 (-0700) Subject: Retry lookups with unsigned DNAME over TCP X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0e4cd87bed5efc61443337034a9d96287b4885dc;p=thirdparty%2Fbind9.git Retry lookups with unsigned DNAME over TCP To prevent spoofed unsigned DNAME responses being accepted retry response with unsigned DNAMEs over TCP if the response is not TSIG signed or there isn't a good DNS CLIENT COOKIE. To prevent test failures, this required adding TCP support to the ans3 and ans4 servers in the chain system test. (cherry picked from commit 2e40705c06831988106335ed77db3cf924d431f6) --- diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl deleted file mode 100644 index 8afe71096dd..00000000000 --- a/bin/tests/system/chain/ans3/ans.pl +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# 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 00000000000..d6d4eea6d99 --- /dev/null +++ b/bin/tests/system/chain/ans3/ans.py @@ -0,0 +1,216 @@ +# 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 = "ns3." + zone + synth = "synth-then-dname." + zone + synth2 = "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("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 == "cname-to-" + synth2: + r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name." + synth2)) + r.answer.append(dns.rrset.from_text("name." + synth2, ttl, IN, CNAME, "name.")) + r.answer.append(dns.rrset.from_text(synth2, ttl, IN, DNAME, ".")) + elif qname == synth or qname == synth2: + if qtype == "DNAME": + r.answer.append(dns.rrset.from_text(qname, ttl, IN, DNAME, ".")) + elif qname == "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 == "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 45d650417f0..e4fc15a280e 100755 --- a/bin/tests/system/chain/ans4/ans.py +++ b/bin/tests/system/chain/ans4/ans.py @@ -276,16 +276,30 @@ except: port=5300 try: ctrlport=int(os.environ['EXTRAPORT1']) 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_udp.bind((ip6, port)) + except: + query6_udp.close() + havev6 = False + + query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - query6_socket.bind((ip6, port)) + query6_tcp.bind((ip4, port)) + query6_tcp.listen(1) + query6_tcp.settimeout(1) except: - query6_socket.close() + query6_tcp.close() havev6 = False except: havev6 = False @@ -310,9 +324,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: @@ -335,13 +349,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/bin/tests/system/cookie/ans9/ans.py b/bin/tests/system/cookie/ans9/ans.py index b454fc8c91a..dd48bdb0950 100644 --- a/bin/tests/system/cookie/ans9/ans.py +++ b/bin/tests/system/cookie/ans9/ans.py @@ -1,13 +1,13 @@ -############################################################################ # 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 +# 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 __future__ import print_function import os @@ -35,28 +35,27 @@ from dns.rdataclass import * from dns.rdatatype import * from dns.tsig import * + # Log query to file def logquery(type, qname): with open("qlog", "a") as f: f.write("%s %s\n", type, qname) + # DNS 2.0 keyring specifies the algorithm try: - keyring = dns.tsigkeyring.from_text({ "foo" : { - "hmac-sha256", - "aaaaaaaaaaaa" - } , - "fake" : { - "hmac-sha256", - "aaaaaaaaaaaa" - } - }) + keyring = dns.tsigkeyring.from_text( + { + "foo": {"hmac-sha256", "aaaaaaaaaaaa"}, + "fake": {"hmac-sha256", "aaaaaaaaaaaa"}, + } + ) except: - keyring = dns.tsigkeyring.from_text({ "foo" : "aaaaaaaaaaaa", - "fake" : "aaaaaaaaaaaa" }) + keyring = dns.tsigkeyring.from_text({"foo": "aaaaaaaaaaaa", "fake": "aaaaaaaaaaaa"}) dopass2 = False + ############################################################################ # # This server will serve valid and spoofed answers. A spoofed answer will @@ -81,7 +80,7 @@ def create_response(msg, tcp, first, ns10): m = dns.message.from_wire(msg, keyring=keyring) qname = m.question[0].name.to_text() lqname = qname.lower() - labels = lqname.split('.') + labels = lqname.split(".") rrtype = m.question[0].rdtype typename = dns.rdatatype.to_text(rrtype) @@ -113,27 +112,35 @@ def create_response(msg, tcp, first, ns10): # Add a server cookie to the response if labels[0] != "nocookie": for o in m.options: - if o.otype == 10: # Use 10 instead of COOKIE - if first and labels[0] == "withtsig" and not tcp: - r.use_tsig(keyring = keyring, - keyname = dns.name.from_text("fake"), - algorithm = HMAC_SHA256) - elif labels[0] != "tcponly" or tcp: - cookie = o - if len(o.data) == 8: - cookie.data = o.data + o.data - else: - cookie.data = o.data - r.use_edns(options=[cookie]) + if o.otype == 10: # Use 10 instead of COOKIE + if first and labels[0] == "withtsig" and not tcp: + r.use_tsig( + keyring=keyring, + keyname=dns.name.from_text("fake"), + algorithm=HMAC_SHA256, + ) + elif labels[0] != "tcponly" or tcp: + cookie = o + try: + if len(o.server) == 0: + cookie.server = o.client + except AttributeError: # dnspython<2.7.0 compat + if len(o.data) == 8: + cookie.data = o.data + o.data + else: + cookie.data = o.data + r.use_edns(options=[cookie]) r.flags |= dns.flags.AA return r + def sigterm(signum, frame): - print ("Shutting down now...") - os.remove('ans.pid') + print("Shutting down now...") + os.remove("ans.pid") running = False sys.exit(0) + ############################################################################ # Main # @@ -146,8 +153,10 @@ ip4_addr2 = "10.53.0.10" ip6_addr1 = "fd92:7065:b8e:ffff::9" ip6_addr2 = "fd92:7065:b8e:ffff::10" -try: port=int(os.environ['PORT']) -except: port=5300 +try: + port = int(os.environ["PORT"]) +except: + port = 5300 query4_udp1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) query4_udp1.bind((ip4_addr1, port)) @@ -195,24 +204,32 @@ except: signal.signal(signal.SIGTERM, sigterm) -f = open('ans.pid', 'w') +f = open("ans.pid", "w") pid = os.getpid() -print (pid, file=f) +print(pid, file=f) f.close() running = True -print ("Using DNS version %s" % dns.version.version) -print ("Listening on %s port %d" % (ip4_addr1, port)) -print ("Listening on %s port %d" % (ip4_addr2, port)) +print("Using DNS version %s" % dns.version.version) +print("Listening on %s port %d" % (ip4_addr1, port)) +print("Listening on %s port %d" % (ip4_addr2, port)) if havev6: - print ("Listening on %s port %d" % (ip6_addr1, port)) - print ("Listening on %s port %d" % (ip6_addr2, port)) -print ("Ctrl-c to quit") + print("Listening on %s port %d" % (ip6_addr1, port)) + print("Listening on %s port %d" % (ip6_addr2, port)) +print("Ctrl-c to quit") if havev6: - input = [query4_udp1, query6_udp1, query4_tcp1, query6_tcp1, - query4_udp2, query6_udp2, query4_tcp2, query6_tcp2] + input = [ + query4_udp1, + query6_udp1, + query4_tcp1, + query6_tcp1, + query4_udp2, + query6_udp2, + query4_tcp2, + query6_tcp2, + ] else: input = [query4_udp1, query4_tcp1, query4_udp2, query4_tcp2] @@ -228,14 +245,19 @@ while running: for s in inputready: ns10 = False - if s == query4_udp1 or s == query6_udp1 or \ - s == query4_udp2 or s == query6_udp2: + if s == query4_udp1 or s == query6_udp1 or s == query4_udp2 or s == query6_udp2: if s == query4_udp1 or s == query6_udp1: - print ("UDP Query received on %s" % - (ip4_addr1 if s == query4_udp1 else ip6_addr1), end=" ") + print( + "UDP Query received on %s" + % (ip4_addr1 if s == query4_udp1 else ip6_addr1), + end=" ", + ) if s == query4_udp2 or s == query6_udp2: - print ("UDP Query received on %s" % - (ip4_addr2 if s == query4_udp2 else ip6_addr2), end=" ") + print( + "UDP Query received on %s" + % (ip4_addr2 if s == query4_udp2 else ip6_addr2), + end=" ", + ) ns10 = True # Handle incoming queries msg = s.recvfrom(65535) @@ -244,31 +266,36 @@ while running: print(dns.rcode.to_text(rsp.rcode())) s.sendto(rsp.to_wire(), msg[1]) if dopass2: - print ("Sending second UDP response without TSIG", end=" ") + print("Sending second UDP response without TSIG", end=" ") rsp = create_response(msg[0], False, False, ns10) s.sendto(rsp.to_wire(), msg[1]) print(dns.rcode.to_text(rsp.rcode())) - if s == query4_tcp1 or s == query6_tcp1 or \ - s == query4_tcp2 or s == query6_tcp2: + if s == query4_tcp1 or s == query6_tcp1 or s == query4_tcp2 or s == query6_tcp2: try: (cs, _) = s.accept() if s == query4_tcp1 or s == query6_tcp1: - print ("TCP Query received on %s" % - (ip4_addr1 if s == query4_tcp1 else ip6_addr1), end=" ") + print( + "TCP Query received on %s" + % (ip4_addr1 if s == query4_tcp1 else ip6_addr1), + end=" ", + ) if s == query4_tcp2 or s == query6_tcp2: - print ("TCP Query received on %s" % - (ip4_addr2 if s == query4_tcp2 else ip6_addr2), end=" ") + print( + "TCP Query received on %s" + % (ip4_addr2 if s == query4_tcp2 else ip6_addr2), + end=" ", + ) ns10 = True # get TCP message length buf = cs.recv(2) - length = struct.unpack('>H', buf[:2])[0] + length = struct.unpack(">H", buf[:2])[0] # grep DNS message msg = cs.recv(length) rsp = create_response(msg, True, True, ns10) print(dns.rcode.to_text(rsp.rcode())) wire = rsp.to_wire() - cs.send(struct.pack('>H', len(wire))) + cs.send(struct.pack(">H", len(wire))) cs.send(wire) cs.close() except s.timeout: diff --git a/lib/dns/include/dns/message.h b/lib/dns/include/dns/message.h index 96c5ef1f79c..54208a49e61 100644 --- a/lib/dns/include/dns/message.h +++ b/lib/dns/include/dns/message.h @@ -226,6 +226,7 @@ struct dns_message { unsigned int cc_bad : 1; unsigned int tkey : 1; unsigned int rdclass_set : 1; + unsigned int has_dname : 1; unsigned int opt_reserved; unsigned int sig_reserved; @@ -1408,6 +1409,13 @@ dns_message_setclass(dns_message_t *msg, dns_rdataclass_t rdclass); * \li msg be a valid message with parsing intent. */ +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 #endif /* DNS_MESSAGE_H */ diff --git a/lib/dns/message.c b/lib/dns/message.c index 03ed2a178dc..8a010a8e9d7 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -459,6 +459,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 = dns_master_indentstr; m->indent.count = dns_master_indent; @@ -1829,6 +1830,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; @@ -4701,3 +4707,9 @@ dns_message_setclass(dns_message_t *msg, dns_rdataclass_t rdclass) { msg->rdclass = rdclass; msg->rdclass_set = 1; } + +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 e48899b3263..2b57b8ebb4b 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -7209,8 +7209,9 @@ validinanswer(dns_rdataset_t *rdataset, fetchctx_t *fctx) { } static isc_result_t -answer_response(fetchctx_t *fctx, dns_message_t *message) { +answer_response(resquery_t *query, dns_message_t *message) { isc_result_t result; + fetchctx_t *fctx = NULL; dns_name_t *name = NULL, *qname = NULL, *ns_name = NULL; dns_name_t *aname = NULL, *cname = NULL, *dname = NULL; dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; @@ -7223,6 +7224,8 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { dns_view_t *view = NULL; dns_trust_t trust; + REQUIRE(VALID_QUERY(query)); + fctx = query->fctx; REQUIRE(VALID_FCTX(fctx)); FCTXTRACE("answer_response"); @@ -7527,8 +7530,17 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { * * We expect there to be only one owner name for all the rdatasets * in this section, and we expect that it is not external. + * + * If the message was not sent over TCP or otherwise secured, + * skip this. */ - result = dns_message_firstname(message, DNS_SECTION_AUTHORITY); + if (message->cc_ok || message->tsig != NULL || message->sig0 != NULL || + (query->options & DNS_FETCHOPT_TCP) != 0) + { + result = dns_message_firstname(message, DNS_SECTION_AUTHORITY); + } else { + done = true; + } while (!done && result == ISC_R_SUCCESS) { name = NULL; dns_message_currentname(message, DNS_SECTION_AUTHORITY, &name); @@ -8011,6 +8023,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { unsigned int bucketnum; dns_resolver_t *res; bool bucket_empty; + bool secured = false; #ifdef HAVE_DNSTAP isc_socket_t *sock = NULL; isc_sockaddr_t localaddr, *la = NULL; @@ -8321,6 +8334,17 @@ resquery_response(isc_task_t *task, isc_event_t *event) { goto done; } + /* + * Remember whether this message was signed or had a + * valid client cookie; if not, we may need to retry over + * TCP later. + */ + if (rmessage->cc_ok || rmessage->tsig != NULL || + rmessage->sig0 != NULL) + { + secured = true; + } + /* * The dispatcher should ensure we only get responses with QR set. */ @@ -8338,10 +8362,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { * 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(rmessage, NULL) == NULL && - !rmessage->cc_ok && !rmessage->cc_bad && - (options & DNS_FETCHOPT_TCP) == 0) - { + if (!secured && (options & DNS_FETCHOPT_TCP) == 0) { unsigned char cookie[COOKIE_BUFFER_SIZE]; if (dns_adb_getcookie(fctx->adb, query->addrinfo, cookie, sizeof(cookie)) > CLIENT_COOKIE_SIZE) @@ -8364,6 +8385,16 @@ resquery_response(isc_task_t *task, isc_event_t *event) { * XXXMPA When support for DNS COOKIE becomes ubiquitous, fall * back to TCP for all non-COOKIE responses. */ + + /* + * Check whether we need to retry over TCP for some other + * reason. + */ + if (dns_message_hasdname(rmessage)) { + options |= DNS_FETCHOPT_TCP; + resend = true; + goto done; + } } /* @@ -8720,7 +8751,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { if ((rmessage->flags & DNS_MESSAGEFLAG_AA) != 0 || ISFORWARDER(query->addrinfo)) { - result = answer_response(fctx, rmessage); + result = answer_response(query, rmessage); if (result != ISC_R_SUCCESS) FCTXTRACE3("answer_response (AA/fwd)", result); } else if (iscname(fctx, rmessage) && @@ -8732,7 +8763,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { * answer when a CNAME is followed. We should treat * it as a valid answer. */ - result = answer_response(fctx, rmessage); + result = answer_response(query, rmessage); if (result != ISC_R_SUCCESS) FCTXTRACE3("answer_response (!ANY/!CNAME)", result); @@ -8741,7 +8772,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { /* * Lame response !!!. */ - result = answer_response(fctx, rmessage); + result = answer_response(query, rmessage); if (result != ISC_R_SUCCESS) FCTXTRACE3("answer_response (!NS)", result); } else { diff --git a/lib/dns/win32/libdns.def.in b/lib/dns/win32/libdns.def.in index a0a8f625563..3cac2ec7da1 100644 --- a/lib/dns/win32/libdns.def.in +++ b/lib/dns/win32/libdns.def.in @@ -537,6 +537,7 @@ dns_message_gettemprdataset dns_message_gettimeadjust dns_message_gettsig dns_message_gettsigkey +dns_message_hasdname dns_message_logfmtpacket dns_message_logfmtpacket2 dns_message_logpacket