]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Retry lookups with unsigned DNAME over TCP
authorEvan Hunt <each@isc.org>
Tue, 30 Sep 2025 04:57:48 +0000 (21:57 -0700)
committerMichał Kępień <michal@isc.org>
Fri, 3 Oct 2025 13:50:34 +0000 (15:50 +0200)
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)

bin/tests/system/chain/ans3/ans.pl [deleted file]
bin/tests/system/chain/ans3/ans.py [new file with mode: 0644]
bin/tests/system/chain/ans4/ans.py
lib/dns/include/dns/message.h
lib/dns/message.c
lib/dns/resolver.c

diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl
deleted file mode 100644 (file)
index e42240b..0000000
+++ /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 (file)
index 0000000..0a031c1
--- /dev/null
@@ -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
index 839067faa5e685504f92d19213748b235447013d..66f0193caca62a96bcd7f31a6364185e85d8a71d 100755 (executable)
@@ -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_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
@@ -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
index fe51fcfe24ba4a2742aa376da21fcd93e993164b..280d872d2087d111415536ebc1095682d05ae7f6 100644 (file)
@@ -236,6 +236,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;
@@ -1457,4 +1458,11 @@ dns_message_clonebuffer(dns_message_t *msg);
  * \li   msg be a valid message.
  */
 
+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
index e23baf7e09e6b3f912ae9f8293051b28e54bb038..225c9d7576ff2db438f8d4e9eefe53f062ddb840 100644 (file)
@@ -442,6 +442,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;
@@ -1727,6 +1728,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;
 
@@ -4773,3 +4779,9 @@ dns_message_clonebuffer(dns_message_t *msg) {
                msg->free_query = 1;
        }
 }
+
+bool
+dns_message_hasdname(dns_message_t *msg) {
+       REQUIRE(DNS_MESSAGE_VALID(msg));
+       return msg->has_dname;
+}
index 168fcc5e808b0b6cc55b70ce1f11de42011a6787..a823f5a70512014c1d44c874a8ffdede23a0670c 100644 (file)
@@ -758,6 +758,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 */
@@ -7900,6 +7901,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
@@ -8058,6 +8100,11 @@ resquery_response(isc_task_t *task, isc_event_t *event) {
                break;
        }
 
+       /*
+        * The dispatcher should ensure we only get responses with QR set.
+        */
+       INSIST((query->rmessage->flags & DNS_MESSAGEFLAG_QR) != 0);
+
        /*
         * If the message is signed, check the signature.  If not, this
         * returns success anyway.
@@ -8075,9 +8122,16 @@ resquery_response(isc_task_t *task, isc_event_t *event) {
        }
 
        /*
-        * The dispatcher should ensure we only get responses with QR set.
+        * Remember whether this message was signed or had a
+        * valid client cookie; if not, we may need to retry over
+        * TCP later.
         */
-       INSIST((query->rmessage->flags & DNS_MESSAGEFLAG_QR) != 0);
+       if (query->rmessage->cc_ok || query->rmessage->tsig != NULL ||
+           query->rmessage->sig0 != NULL)
+       {
+               rctx.secured = true;
+       }
+
        /*
         * INSIST() that the message comes from the place we sent it to,
         * since the dispatch code should ensure this.
@@ -8091,10 +8145,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(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)
@@ -8120,6 +8171,17 @@ resquery_response(isc_task_t *task, isc_event_t *event) {
                 */
        }
 
+       /*
+        * 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);
 
        /*
@@ -8848,8 +8910,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);
 
@@ -9221,14 +9283,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) {
@@ -9236,6 +9298,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) {