diff --git a/bin/named/server.c b/bin/named/server.c index e9f2329b7c..7f3f0162a5 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1715,6 +1715,7 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) { dns_name_t *origin = dns_zone_getorigin(zone); dns_rdataclass_t zclass = view->rdclass; + dns_zone_setclass(zone, zclass); RETERR(dns_zonemgr_managezone(named_g_server->zonemgr, zone)); dns_zone_setstats(zone, named_g_server->zonestats); @@ -3926,7 +3927,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, obj = NULL; result = named_config_get(maps, "recursion", &obj); INSIST(result == ISC_R_SUCCESS); - view->recursion = cfg_obj_asboolean(obj); + view->recursion = (view->rdclass == dns_rdataclass_in && + cfg_obj_asboolean(obj)); max_cache_size = configure_max_cache_size(view, maps); @@ -4543,35 +4545,13 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, /* * We have default root hints for class IN if we need them. * Each view gets its own rootdb so a priming response only - * writes into that view's copy. + * writes into that view's copy. Other classes don't support + * recursion and don't need hints. */ if (view->rdclass == dns_rdataclass_in && view->rootdb == NULL) { CHECK(configure_rootdb(view, NULL)); } - /* - * If we still have no root hints, this is a non-IN view with no - * "hints zone" configured. Issue a warning, except if this - * is a root server. Root servers never need to consult - * their hints, so it's no point requiring users to configure - * them. - */ - if (view->rootdb == NULL) { - dns_zone_t *rootzone = NULL; - (void)dns_view_findzone(view, dns_rootname, DNS_ZTFIND_EXACT, - &rootzone); - if (rootzone != NULL) { - dns_zone_detach(&rootzone); - } else if (strcmp(view->name, "_bind") != 0 || - view->rdclass != dns_rdataclass_chaos) - { - isc_log_write(NAMED_LOGCATEGORY_GENERAL, - NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, - "no root hints for view '%s'", - view->name); - } - } - /* * Configure the view's transports (DoT/DoH) */ @@ -4794,9 +4774,10 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, CHECK(configure_view_acl(vconfig, config, "allow-proxy-on", NULL, aclctx, isc_g_mctx, &view->proxyonacl)); - if (strcmp(view->name, "_bind") != 0 && - view->rdclass != dns_rdataclass_chaos) - { + if (view->rdclass != dns_rdataclass_in) { + dns_acl_none(isc_g_mctx, &view->recursionacl); + dns_acl_none(isc_g_mctx, &view->recursiononacl); + } else { CHECK(configure_view_acl(vconfig, config, "allow-recursion", NULL, aclctx, isc_g_mctx, &view->recursionacl)); diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 69d96b7ea8..330da510e3 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -515,6 +515,7 @@ $CHECKCONF -l good.conf \ | grep -v "is not implemented" \ | grep -v "is not recommended" \ | grep -v "no longer exists" \ + | grep -v "recursion will be disabled" \ | grep -v "is obsolete" >checkconf.out$n || ret=1 diff good.zonelist checkconf.out$n >diff.out$n || ret=1 if [ $ret -ne 0 ]; then @@ -738,5 +739,16 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check 'recursion yes;' is warned and disabled in a non-IN view ($n)" +ret=0 +$CHECKCONF warn-chaos-recursion.conf >checkconf.out$n 2>&1 || ret=1 +grep -F "recursion will be disabled" checkconf.out$n >/dev/null || ret=1 +if [ $ret != 0 ]; then + echo_i "failed" + ret=1 +fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/checkconf/warn-chaos-recursion.conf b/bin/tests/system/checkconf/warn-chaos-recursion.conf new file mode 100644 index 0000000000..01965102a4 --- /dev/null +++ b/bin/tests/system/checkconf/warn-chaos-recursion.conf @@ -0,0 +1,12 @@ +options { + directory "."; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.hints"; + }; +}; diff --git a/bin/tests/system/class/ns1/chaos.db.in b/bin/tests/system/class/ns1/chaos.db.in new file mode 100644 index 0000000000..43ca58ffa8 --- /dev/null +++ b/bin/tests/system/class/ns1/chaos.db.in @@ -0,0 +1,4 @@ +. CH NS ns.root. +ns.root. CH A ns.root. 1 +ns.root. CH AAAA \# 1 00 + diff --git a/bin/tests/system/class/ns1/named.conf.j2 b/bin/tests/system/class/ns1/named.conf.j2 new file mode 100644 index 0000000000..76f85fc6c9 --- /dev/null +++ b/bin/tests/system/class/ns1/named.conf.j2 @@ -0,0 +1,31 @@ +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.db"; + }; + zone "version.bind" { + type primary; + database "_builtin version"; + }; +}; diff --git a/bin/tests/system/class/ns2/example.db.in b/bin/tests/system/class/ns2/example.db.in new file mode 100644 index 0000000000..a658ddbd89 --- /dev/null +++ b/bin/tests/system/class/ns2/example.db.in @@ -0,0 +1,6 @@ +$TTL 300 +@ CH SOA ns.example. hostmaster.example. 1 3600 1200 604800 300 +@ CH NS ns.example. +ns CH TXT "ns" +a CH A target.example. 1 +target CH TXT "target" diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in new file mode 100644 index 0000000000..a50e5167a9 --- /dev/null +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -0,0 +1,11 @@ +$ORIGIN 1.0.0.127.in-addr.arpa. +$TTL 300 +@ IN SOA ns hostmaster 1 3600 900 604800 300 +@ IN NS ns +ns IN A 127.0.0.1 + +@ IN KX 10 target.example. +@ IN PX 10 map822.example. mapx400.example. +@ IN NSAP 0x47000580ffff0000000001e133ffffff00016200 +@ IN NSAP-PTR target.example. +@ in EID \# 01 aa diff --git a/bin/tests/system/class/ns2/named.conf.j2 b/bin/tests/system/class/ns2/named.conf.j2 new file mode 100644 index 0000000000..5618c15216 --- /dev/null +++ b/bin/tests/system/class/ns2/named.conf.j2 @@ -0,0 +1,42 @@ +options { + directory "."; + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view default { + match-clients { any; }; + recursion no; + dnssec-validation no; + zone "1.0.0.127.in-addr.arpa." { + type primary; + file "localhost.db"; + update-policy { + grant * tcp-self . ANY; + }; + }; +}; + +view chaos ch { + match-clients { any; }; + recursion no; + zone example { + type primary; + file "example.db"; + allow-update { any; }; + }; +}; diff --git a/bin/tests/system/class/ns3/named.conf.j2 b/bin/tests/system/class/ns3/named.conf.j2 new file mode 100644 index 0000000000..3016333aad --- /dev/null +++ b/bin/tests/system/class/ns3/named.conf.j2 @@ -0,0 +1,28 @@ +options { + directory "."; + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + dnssec-validation no; + forward only; + forwarders port @PORT@ { 10.53.0.2; }; + deny-answer-addresses { 0.0.0.0/0; ::/0; }; +}; diff --git a/bin/tests/system/class/setup.sh b/bin/tests/system/class/setup.sh new file mode 100644 index 0000000000..c70a2f8290 --- /dev/null +++ b/bin/tests/system/class/setup.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e + +# 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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/chaos.db.in ns1/chaos.db +cp ns2/example.db.in ns2/example.db +cp ns2/localhost.db.in ns2/localhost.db diff --git a/bin/tests/system/class/tests_class_chaos.py b/bin/tests/system/class/tests_class_chaos.py new file mode 100644 index 0000000000..5b4fef9ae4 --- /dev/null +++ b/bin/tests/system/class/tests_class_chaos.py @@ -0,0 +1,54 @@ +# 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 dns.opcode +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def test_chaos_recursion(): + msg = isctest.query.create("foo.example.", "TXT", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.refused(res) + + +def test_chaos_auth(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + + +def test_chaos_forward(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.3") + isctest.check.refused(res) + + +def test_chaos_notify(): + msg = isctest.query.create("example.", "SOA", qclass="CH", rd=False, dnssec=False) + msg.set_opcode(dns.opcode.NOTIFY) + msg.flags = dns.opcode.to_flags(dns.opcode.NOTIFY) + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.notimp(res) + + +def test_query_class_none(): + msg = isctest.query.create("example.", "A", qclass="NONE") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.formerr(res) diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py new file mode 100644 index 0000000000..30e3ba6d2a --- /dev/null +++ b/bin/tests/system/class/tests_class_update.py @@ -0,0 +1,137 @@ +# 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 socket +import struct + +from dns import message, rdataclass, rdatatype, update + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def encode_name(name: str) -> bytes: + out = b"" + for label in name.rstrip(".").split("."): + out += bytes([len(label)]) + label.encode("ascii") + return out + b"\x00" + + +@pytest.mark.parametrize( + "rdtype,rdclass,ttl,rdata", + [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00\x00\x00\x00\x00\x00\x01"), + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), + (rdatatype.KX, rdataclass.NONE, 0, b""), + (rdatatype.PX, rdataclass.NONE, 0, b""), + (rdatatype.NSAP, rdataclass.NONE, 0, b""), + (rdatatype.NSAP_PTR, rdataclass.NONE, 0, b""), + (31, rdataclass.NONE, 0, b""), # dnspython doesn't define type EID + ], +) +def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): + # these update messages are badly formatted, so we construct + # them manually instead of using dnspython. + + # opcode=UPDATE, 1 RRset in ZONE, 1 RRset in UPDATE + header = struct.pack("!HHHHHH", 0, 0x2800, 1, 0, 1, 0) + + # ZONE section: QNAME=, QTYPE=SOA, QCLASS=ANY + zone_q = encode_name("1.0.0.127.in-addr.arpa") + struct.pack("!HH", 6, 255) + + # UPDATE section RR: + update_rr = ( + encode_name("1.0.0.127.in-addr.arpa") + + struct.pack("!HHIH", rdtype, rdclass, ttl, len(rdata)) + + rdata + ) + + m = header + zone_q + update_rr + packet = struct.pack("!H", len(m)) + m + + with socket.create_connection( + ("10.53.0.2", named_port), source_address=("127.0.0.1", 0), timeout=2.0 + ) as s: + s.sendall(packet) + try: + rwire = s.recv(4096) + res = message.from_wire(rwire) + isctest.check.formerr(res) + except Exception: # pylint: disable=broad-except + pass + + # check the server is answering + msg = isctest.query.create("1.0.0.127.in-addr.arpa", "SRV") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 0) + + +@pytest.mark.parametrize( + "rdtype,rdata", + [ + (rdatatype.SVCB, "\\# 02 0000"), + (rdatatype.WKS, "\\# 02 4142"), + (rdatatype.WKS, "\\# 02 4344"), + ], +) +def test_class_chaosupdate(rdtype, rdata): + up = update.UpdateMessage("example.", rdclass=rdataclass.CHAOS) + up.add("foo.example.", 300, rdtype, rdata) + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) + + +def test_class_undefined(ns2): + up = update.UpdateMessage(".", rdclass=257) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) + watcher.wait_for_line("invalid message class: CLASS257") + + +def test_class_zero(ns2): + up = update.UpdateMessage(".", rdclass=0) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message class could not be determined") + + +def test_class_any(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.ANY) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") + + +def test_class_none(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.NONE) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") diff --git a/bin/tests/system/doth/tests_malicious.py b/bin/tests/system/doth/tests_malicious.py new file mode 100644 index 0000000000..7529f2b7e1 --- /dev/null +++ b/bin/tests/system/doth/tests_malicious.py @@ -0,0 +1,73 @@ +# 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 socket +import ssl + +from h2.config import H2Configuration +from h2.connection import H2Connection +from h2.settings import SettingCodes + +import dns.message + + +def test_settings_frame_flood(ns1, named_httpsport): + msg = dns.message.make_query(".", "SOA") + wire = msg.to_wire() + + with socket.create_connection((ns1.ip, named_httpsport), timeout=10) as sock: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.set_alpn_protocols(["h2"]) + + with ctx.wrap_socket(sock, server_hostname=ns1.ip) as tls: + config = H2Configuration(client_side=True, header_encoding="utf-8") + conn = H2Connection(config=config) + conn.initiate_connection() + tls.sendall(conn.data_to_send()) + + stream_id = conn.get_next_available_stream_id() + conn.send_headers( + stream_id, + [ + (":method", "POST"), + (":path", "/dns-query"), + (":scheme", "https"), + (":authority", f"{ns1.ip}:{named_httpsport}"), + ("content-type", "application/dns-message"), + ("accept", "application/dns-message"), + ("content-length", str(len(wire))), + ], + ) + conn.send_data(stream_id, wire, end_stream=True) + tls.sendall(conn.data_to_send()) + + for i in range(4096): + try: + conn.update_settings( + { + SettingCodes.MAX_CONCURRENT_STREAMS: (i % 100) + 1, + SettingCodes.INITIAL_WINDOW_SIZE: i + 1, + } + ) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + break + + if i % 500 == 0: + tls.settimeout(0.05) + try: + while (data := tls.recv(65535)) != b"": + conn.receive_data(data) + tls.sendall(conn.data_to_send()) + except Exception: # pylint: disable=broad-except + pass diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index 7c5e30c4e1..f723dd95c0 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -47,6 +47,10 @@ def servfail(message: dns.message.Message) -> None: rcode(message, dns.rcode.SERVFAIL) +def formerr(message: dns.message.Message) -> None: + rcode(message, dns.rcode.FORMERR) + + def adflag(message: dns.message.Message) -> None: assert (message.flags & dns.flags.AD) != 0, str(message) diff --git a/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 b/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 deleted file mode 100644 index 57389c0521..0000000000 --- a/bin/tests/system/nohintswarn_bindchaos/ns1/named.conf.j2 +++ /dev/null @@ -1,23 +0,0 @@ -options { - port @PORT@; - pid-file "named.pid"; - listen-on { 10.53.0.1; }; -}; - -key rndc_key { - secret "1234abcd8765"; - algorithm @DEFAULT_HMAC@; -}; - -controls { - inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; -}; - -view _bind { -}; - -view foo { -}; - -view bar ch { -}; diff --git a/bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 new file mode 100644 index 0000000000..fd83fc3c19 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns1/named.conf.j2 @@ -0,0 +1,28 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + recursion no; + dnssec-validation no; +}; + +zone "." { + type primary; + file "root.db"; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns1/root.db b/bin/tests/system/nslimit_outdomain/ns1/root.db new file mode 100644 index 0000000000..bfbf049b80 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns1/root.db @@ -0,0 +1,24 @@ +; 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. + +$TTL 300 +. IN SOA owner.root-servers.nil. a.root.servers.nil. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +tld. NS ns.tld. +ns.tld. A 10.53.0.2 diff --git a/bin/tests/system/nslimit_outdomain/ns2/dnshoster.tld.db b/bin/tests/system/nslimit_outdomain/ns2/dnshoster.tld.db new file mode 100644 index 0000000000..9540da4743 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/dnshoster.tld.db @@ -0,0 +1,33 @@ +; 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. + +$TTL 300 +dnshoster.tld. IN SOA owner.tld. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +dnshoster.tld. NS ns1.dnshoster.tld. +ns1.dnshoster.tld. A 10.53.0.5 +ns1.dnshoster.tld. A 10.53.0.6 + +dnshoster.tld. NS ns2.dnshoster.tld. +ns2.dnshoster.tld. A 10.53.1.1 +ns2.dnshoster.tld. A 10.53.1.2 + +dnshoster.tld. NS ns3.dnshoster.tld. +ns3.dnshoster.tld. A 10.53.2.1 +ns3.dnshoster.tld. A 10.53.2.2 + + diff --git a/bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 new file mode 100644 index 0000000000..037ac60fe0 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/named.conf.j2 @@ -0,0 +1,41 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { + 10.53.0.2; + 10.53.0.5; + 10.53.0.6; + 10.53.0.7; + 10.53.1.1; + 10.53.1.2; + 10.53.2.2; + }; + recursion no; + dnssec-validation no; +}; + +zone "tld." { + type primary; + file "tld.db"; +}; + +zone "dnshoster.tld." { + type primary; + file "dnshoster.tld.db"; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns2/tld.db b/bin/tests/system/nslimit_outdomain/ns2/tld.db new file mode 100644 index 0000000000..e29bf91f7d --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns2/tld.db @@ -0,0 +1,36 @@ +; 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. + +$TTL 300 +tld. IN SOA owner.tld. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +tld. NS ns.tld. +ns.tld. A 10.53.0.2 + +example4.tld. NS ns.example4.tld. +ns.example4.tld. A 10.53.0.3 + +dnshoster.tld. NS ns1.dnshoster.tld. +ns1.dnshoster.tld. A 10.53.0.5 +ns1.dnshoster.tld. A 10.53.0.6 + +dnshoster.tld. NS ns2.dnshoster.tld. +ns2.dnshoster.tld. A 10.53.1.1 +ns2.dnshoster.tld. A 10.53.1.2 + +dnshoster.tld. NS ns3.dnshoster.tld. +ns3.dnshoster.tld. A 10.53.2.1 +ns3.dnshoster.tld. A 10.53.2.2 diff --git a/bin/tests/system/nslimit_outdomain/ns3/example.tld.db b/bin/tests/system/nslimit_outdomain/ns3/example.tld.db new file mode 100644 index 0000000000..2a599ee876 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/example.tld.db @@ -0,0 +1,184 @@ +; 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. + +$TTL 300 +example.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + +sub.example.tld. NS ns01.sub.example.tld. +sub.example.tld. NS ns02.sub.example.tld. +sub.example.tld. NS ns03.sub.example.tld. +sub.example.tld. NS ns04.sub.example.tld. +sub.example.tld. NS ns05.sub.example.tld. +sub.example.tld. NS ns06.sub.example.tld. +sub.example.tld. NS ns07.sub.example.tld. +sub.example.tld. NS ns08.sub.example.tld. +sub.example.tld. NS ns09.sub.example.tld. +sub.example.tld. NS ns10.sub.example.tld. + +ns01.sub.example.tld. A 10.53.0.5 +ns01.sub.example.tld. A 10.53.0.6 +ns01.sub.example.tld. A 10.53.0.7 +ns01.sub.example.tld. A 10.53.0.8 +ns01.sub.example.tld. A 10.53.0.9 +ns01.sub.example.tld. A 10.53.0.10 +ns01.sub.example.tld. A 10.53.1.1 +ns01.sub.example.tld. A 10.53.1.2 +ns01.sub.example.tld. A 10.53.2.1 +ns01.sub.example.tld. A 10.53.0.3 +ns01.sub.example.tld. A 127.0.0.1 +ns01.sub.example.tld. A 127.0.0.2 +; Those addresses won't be used (exceed the max-delegation-servers). +ns01.sub.example.tld. A 127.0.0.3 +ns01.sub.example.tld. A 127.0.0.4 + +ns02.sub.example.tld. A 10.53.0.5 +ns02.sub.example.tld. A 10.53.0.6 +ns02.sub.example.tld. A 10.53.0.7 +ns02.sub.example.tld. A 10.53.0.8 +ns02.sub.example.tld. A 10.53.0.9 +ns02.sub.example.tld. A 10.53.0.10 +ns02.sub.example.tld. A 10.53.1.1 +ns02.sub.example.tld. A 10.53.1.2 +ns02.sub.example.tld. A 10.53.2.1 +ns02.sub.example.tld. A 10.53.0.3 +ns02.sub.example.tld. A 127.0.0.1 +ns02.sub.example.tld. A 127.0.0.2 +ns02.sub.example.tld. A 127.0.0.3 +ns02.sub.example.tld. A 127.0.0.4 + +ns03.sub.example.tld. A 10.53.0.5 +ns03.sub.example.tld. A 10.53.0.6 +ns03.sub.example.tld. A 10.53.0.7 +ns03.sub.example.tld. A 10.53.0.8 +ns03.sub.example.tld. A 10.53.0.9 +ns03.sub.example.tld. A 10.53.0.10 +ns03.sub.example.tld. A 10.53.1.1 +ns03.sub.example.tld. A 10.53.1.2 +ns03.sub.example.tld. A 10.53.2.1 +ns03.sub.example.tld. A 10.53.0.3 +ns03.sub.example.tld. A 127.0.0.1 +ns03.sub.example.tld. A 127.0.0.2 +ns03.sub.example.tld. A 127.0.0.3 +ns03.sub.example.tld. A 127.0.0.4 + +ns04.sub.example.tld. A 10.53.0.5 +ns04.sub.example.tld. A 10.53.0.6 +ns04.sub.example.tld. A 10.53.0.7 +ns04.sub.example.tld. A 10.53.0.8 +ns04.sub.example.tld. A 10.53.0.9 +ns04.sub.example.tld. A 10.53.0.10 +ns04.sub.example.tld. A 10.53.1.1 +ns04.sub.example.tld. A 10.53.1.2 +ns04.sub.example.tld. A 10.53.2.1 +ns04.sub.example.tld. A 10.53.0.3 +ns04.sub.example.tld. A 127.0.0.1 +ns04.sub.example.tld. A 127.0.0.2 +ns04.sub.example.tld. A 127.0.0.3 +ns04.sub.example.tld. A 127.0.0.4 + +ns05.sub.example.tld. A 10.53.0.5 +ns05.sub.example.tld. A 10.53.0.6 +ns05.sub.example.tld. A 10.53.0.7 +ns05.sub.example.tld. A 10.53.0.8 +ns05.sub.example.tld. A 10.53.0.9 +ns05.sub.example.tld. A 10.53.0.10 +ns05.sub.example.tld. A 10.53.1.1 +ns05.sub.example.tld. A 10.53.1.2 +ns05.sub.example.tld. A 10.53.2.1 +ns05.sub.example.tld. A 10.53.0.3 +ns05.sub.example.tld. A 127.0.0.1 +ns05.sub.example.tld. A 127.0.0.2 +ns05.sub.example.tld. A 127.0.0.3 +ns05.sub.example.tld. A 127.0.0.4 + +ns06.sub.example.tld. A 10.53.0.5 +ns06.sub.example.tld. A 10.53.0.6 +ns06.sub.example.tld. A 10.53.0.7 +ns06.sub.example.tld. A 10.53.0.8 +ns06.sub.example.tld. A 10.53.0.9 +ns06.sub.example.tld. A 10.53.0.10 +ns06.sub.example.tld. A 10.53.1.1 +ns06.sub.example.tld. A 10.53.1.2 +ns06.sub.example.tld. A 10.53.2.1 +ns06.sub.example.tld. A 10.53.0.3 +ns06.sub.example.tld. A 127.0.0.1 +ns06.sub.example.tld. A 127.0.0.2 +ns06.sub.example.tld. A 127.0.0.3 +ns06.sub.example.tld. A 127.0.0.4 + +ns07.sub.example.tld. A 10.53.0.5 +ns07.sub.example.tld. A 10.53.0.6 +ns07.sub.example.tld. A 10.53.0.7 +ns07.sub.example.tld. A 10.53.0.8 +ns07.sub.example.tld. A 10.53.0.9 +ns07.sub.example.tld. A 10.53.0.10 +ns07.sub.example.tld. A 10.53.1.1 +ns07.sub.example.tld. A 10.53.1.2 +ns07.sub.example.tld. A 10.53.2.1 +ns07.sub.example.tld. A 10.53.0.3 +ns07.sub.example.tld. A 127.0.0.1 +ns07.sub.example.tld. A 127.0.0.2 +ns07.sub.example.tld. A 127.0.0.3 +ns07.sub.example.tld. A 127.0.0.4 + +ns08.sub.example.tld. A 10.53.0.5 +ns08.sub.example.tld. A 10.53.0.6 +ns08.sub.example.tld. A 10.53.0.7 +ns08.sub.example.tld. A 10.53.0.8 +ns08.sub.example.tld. A 10.53.0.9 +ns08.sub.example.tld. A 10.53.0.10 +ns08.sub.example.tld. A 10.53.1.1 +ns08.sub.example.tld. A 10.53.1.2 +ns08.sub.example.tld. A 10.53.2.1 +ns08.sub.example.tld. A 10.53.0.3 +ns08.sub.example.tld. A 127.0.0.1 +ns08.sub.example.tld. A 127.0.0.2 +ns08.sub.example.tld. A 127.0.0.3 +ns08.sub.example.tld. A 127.0.0.4 + +ns09.sub.example.tld. A 10.53.0.5 +ns09.sub.example.tld. A 10.53.0.6 +ns09.sub.example.tld. A 10.53.0.7 +ns09.sub.example.tld. A 10.53.0.8 +ns09.sub.example.tld. A 10.53.0.9 +ns09.sub.example.tld. A 10.53.0.10 +ns09.sub.example.tld. A 10.53.1.1 +ns09.sub.example.tld. A 10.53.1.2 +ns09.sub.example.tld. A 10.53.2.1 +ns09.sub.example.tld. A 10.53.0.3 +ns09.sub.example.tld. A 127.0.0.1 +ns09.sub.example.tld. A 127.0.0.2 +ns09.sub.example.tld. A 127.0.0.3 +ns09.sub.example.tld. A 127.0.0.4 + +ns10.sub.example.tld. A 10.53.0.5 +ns10.sub.example.tld. A 10.53.0.6 +ns10.sub.example.tld. A 10.53.0.7 +ns10.sub.example.tld. A 10.53.0.8 +ns10.sub.example.tld. A 10.53.0.9 +ns10.sub.example.tld. A 10.53.0.10 +ns10.sub.example.tld. A 10.53.1.1 +ns10.sub.example.tld. A 10.53.1.2 +ns10.sub.example.tld. A 10.53.2.1 +ns10.sub.example.tld. A 10.53.0.3 +ns10.sub.example.tld. A 127.0.0.1 +ns10.sub.example.tld. A 127.0.0.2 +ns10.sub.example.tld. A 127.0.0.3 +ns10.sub.example.tld. A 127.0.0.4 diff --git a/bin/tests/system/nslimit_outdomain/ns3/example4.tld.db b/bin/tests/system/nslimit_outdomain/ns3/example4.tld.db new file mode 100644 index 0000000000..f1c64be066 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/example4.tld.db @@ -0,0 +1,24 @@ +; 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. + +$TTL 300 +example4.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example4.tld. NS ns.example4.tld. +ns.example4.tld. A 10.53.0.3 +sub.example4.tld. NS ns1.dnshoster.tld. +sub.example4.tld. NS ns2.dnshoster.tld. diff --git a/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 new file mode 100644 index 0000000000..f7e03a82e7 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns3/named.conf.j2 @@ -0,0 +1,30 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { + 10.53.0.3; + }; + recursion no; + dnssec-validation no; +}; + +zone "example4.tld." { + type primary; + file "example4.tld.db"; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 b/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 new file mode 100644 index 0000000000..68f1511c7c --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns4/named.args.j2 @@ -0,0 +1 @@ +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 b/bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 new file mode 100644 index 0000000000..09fbdd4e70 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns4/named.conf.j2 @@ -0,0 +1,59 @@ +/* + * 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. + */ +{% set maxdelegationservers = maxdelegationservers | default(None) %} + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; + {% if maxdelegationservers %} + @maxdelegationservers@ + {% endif %} +}; + +/* + * Forcing TCP ensures that ADDITIONAL won't be truncated (responses won't have + * the TC flag, hence the resolver won't retry using TCP by itself, see + * https://datatracker.ietf.org/doc/html/rfc2181#section-9) + */ +server 10.53.0.3 { tcp-only true; }; +server 10.53.0.5 { tcp-only true; }; +server 10.53.0.6 { tcp-only true; }; +server 10.53.0.7 { tcp-only true; }; +server 10.53.0.8 { tcp-only true; }; +server 10.53.0.9 { tcp-only true; }; +server 10.53.0.10 { tcp-only true; }; +server 10.53.1.1 { tcp-only true; }; +server 10.53.1.2 { tcp-only true; }; +server 10.53.2.1 { tcp-only true; }; + +zone "." { + type hint; + file "root.hint"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/nslimit_outdomain/ns4/root.hint b/bin/tests/system/nslimit_outdomain/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/ns4/root.hint @@ -0,0 +1,14 @@ +; 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. + +$TTL 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.1 diff --git a/bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py b/bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py new file mode 100644 index 0000000000..228fb07918 --- /dev/null +++ b/bin/tests/system/nslimit_outdomain/tests_nslimit_outdomain.py @@ -0,0 +1,120 @@ +# 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 os + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_ips_and_queries(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.4:38507 -> 10.53.0.3:22047 TCP 56b sub.example.tld/IN/NS + _, _, _, _, _, dst, _, _, query = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return (ip, query) + + +def extract_dnstap(ns, expectedlen): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + assert expectedlen == len(lines) + return list(map(line_to_ips_and_queries, lines)) + + +# Because DNSTAP doesn't have ordering guarantee, the order doesn't matter here. +def has_ip_and_query(expected_ips_and_queries, ips_and_queries): + found_count = 0 + for expected_ip, expected_query in expected_ips_and_queries: + for ip, query in ips_and_queries: + if ip == expected_ip and query == expected_query: + found_count += 1 + break + return found_count == len(expected_ips_and_queries) + + +# Test the max-delegation-servers limit on flow where ADB attempt +# a lookup from an NS name rather than directly with the NS addresses. +def test_nslimit_outdomain(ns4, templates): + templates.render( + "ns4/named.conf", {"maxdelegationservers": "max-delegation-servers 2;"} + ) + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + + msg = isctest.query.create("sub.example4.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 9) + + # The resolver first resolve example4.tld. and gets the NS for sub.example.tld. + # which is out-domain. So it resolves it. + assert has_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example4.tld/IN/NS"), + ("10.53.0.3", "sub.example4.tld/IN/A"), + ("10.53.0.2", "dnshoster.tld/IN/NS"), + ], + ips_and_queries, + ) + + # Then, because max-delegation-servers is 2, the resolver will try to use either + # the NS ns1.dnshoster.tld or the NS ns2.dnshoster.tld. or the NS ns3.dnshoster.tld. + # + # What is important here, is that the NS of sub.example4.tld are _names_, so + # this is going through the dns_adb_createfind() flow, and it does stop after 2 + # queries (on the two IPs of one of the NS server above) and _won't_ try another + # NS name (becuse max-delegation-servers will be reached). + # + # Note that the sum of all the queries checked here is 8 and not 9. This is because + # when dnshoster.tld has been resolved, the resolver resolved 2 names. But the IPs + # of only one of the two names has been used. (This is checked below). + + used_ns1 = has_ip_and_query( + [ + ("10.53.0.2", "ns1.dnshoster.tld/IN/A"), + ("10.53.0.5", "sub.example4.tld/IN/A"), + ("10.53.0.6", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + used_ns2 = has_ip_and_query( + [ + ("10.53.0.2", "ns2.dnshoster.tld/IN/A"), + ("10.53.1.1", "sub.example4.tld/IN/A"), + ("10.53.1.2", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + used_ns3 = has_ip_and_query( + [ + ("10.53.0.2", "ns3.dnshoster.tld/IN/A"), + ("10.53.2.1", "sub.example4.tld/IN/A"), + ("10.53.2.2", "sub.example4.tld/IN/A"), + ], + ips_and_queries, + ) + + assert used_ns1 or used_ns2 or used_ns3 diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh index 299330773a..9d27b20a76 100644 --- a/bin/tests/system/nsupdate/setup.sh +++ b/bin/tests/system/nsupdate/setup.sh @@ -35,6 +35,7 @@ update.nil IN SOA ns1.example.nil. hostmaster.example.nil. ( 3600 ; minimum (1 hour) ) update.nil. NS ns1.update.nil. +update.nil. KX 0 . ns1.update.nil. A 10.53.0.2 ns2.update.nil. AAAA ::1 EOF diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh index 7a77faea6b..d71136c563 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -485,8 +485,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 update is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000100000000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { @@ -497,20 +499,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 additional data is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000000010000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 -$DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 -grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 -[ $ret = 0 ] || { - echo_i "failed" - status=1 -} - -n=$((n + 1)) -ret=0 -echo_i "check that update to undefined class is handled ($n)" -echo "a0e4280000010001000000000000060101c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { diff --git a/bin/tests/system/packet.pl b/bin/tests/system/packet.pl index 900a0c071e..afb9f4784d 100644 --- a/bin/tests/system/packet.pl +++ b/bin/tests/system/packet.pl @@ -40,6 +40,7 @@ # -p : specify port # -t : specify UDP or TCP # -r : send packet times +# -b: blocking io # -d: dump response packets # # If not specified, address defaults to 127.0.0.1, port to 53, protocol @@ -51,6 +52,8 @@ use strict; use Getopt::Std; use IO::File; use IO::Socket; +use Net::DNS; +use Net::DNS::Packet; sub usage { print ("Usage: packet.pl [-a address] [-d] [-p port] [-t (tcp|udp)] [-r ] [file]\n"); @@ -61,8 +64,6 @@ my $sock; my $proto; sub dumppacket { - use Net::DNS; - use Net::DNS::Packet; my $rin; my $rout; @@ -96,7 +97,7 @@ sub dumppacket { } my %options={}; -getopts("a:dp:t:r:", \%options); +getopts("a:bdp:t:r:", \%options); my $addr = "127.0.0.1"; $addr = $options{a} if defined $options{a}; @@ -111,6 +112,8 @@ usage if ($proto !~ /^(udp|tcp)$/); my $repeats = 1; $repeats = $options{r} if defined $options{r}; +my $blocking = defined $options{b} ? 1 : 0; + my $file = "STDIN"; if (@ARGV >= 1) { my $filename = shift @ARGV; @@ -132,8 +135,22 @@ my $len = length $data; my $output = unpack("H*", $data); print ("sending $repeats time(s): $output\n"); + +if (defined $options{d}) { + my $request; + if ($Net::DNS::VERSION > 0.68) { + $request = new Net::DNS::Packet(\$data, 0); + $@ and die $@; + } else { + my $err; + ($request, $err) = new Net::DNS::Packet(\$data, 0); + $err and die $err; + } + $request->print; +} + $sock = IO::Socket::INET->new(PeerAddr => $addr, PeerPort => $port, - Blocking => 0, + Blocking => $blocking, Proto => $proto,) or die "$!"; STDOUT->autoflush(1); diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py new file mode 100644 index 0000000000..d0cb6d2935 --- /dev/null +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -0,0 +1,132 @@ +# 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 collections.abc import AsyncGenerator + +import dns.edns +import dns.name +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QnameHandler, + QueryContext, + ResponseHandler, + StaticResponseHandler, +) + + +def _get_cookie(qctx: QueryContext): + for o in qctx.query.options: + if o.otype == dns.edns.OptionType.COOKIE: + cookie = o + try: + if len(cookie.server) == 0: + cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88" + except AttributeError: # dnspython<2.7.0 compat + if len(o.data) == 8: + cookie.data *= 2 + + return cookie + + return None + + +def rrset( + qname: 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) + + +class RootNSHandler(QnameHandler, StaticResponseHandler): + qnames = ["."] + answer = [ + rrset(".", dns.rdatatype.NS, "a.root-servers.nil."), + ] + additional = [ + rrset("a.root-servers.nil.", dns.rdatatype.A, "10.53.0.3"), + ] + + +class ExampleNSHandler(QnameHandler, StaticResponseHandler): + qnames = ["example."] + answer = [ + rrset("example.", dns.rdatatype.NS, "ns.example."), + ] + additional = [ + rrset("ns.example.", dns.rdatatype.A, "10.53.0.3"), + ] + + +class CookieHandler(ResponseHandler): + def match(self, qctx: QueryContext) -> bool: + example = dns.name.from_text("example") + return qctx.qname.is_subdomain(example) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + + # Check for client cookie + cookie = _get_cookie(qctx) + + # If missing cookie entirely, just return SERVFAIL + if cookie is None: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + yield DnsResponseSend(qctx.response, authoritative=True) + + # If there is a client cookie, mock BADCOOKIE to trigger + # the resend loop logic. + qctx.response.use_edns(options=[cookie]) + qctx.response.set_rcode(dns.rcode.BADCOOKIE) + yield DnsResponseSend(qctx.response, authoritative=True) + + +class NoErrorHandler(ResponseHandler): + """ + If the query is NOT a subdomain of example, respond with standard NOERROR empty answer + """ + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + qctx.prepare_new_response() + qctx.response.set_rcode(dns.rcode.NOERROR) + yield DnsResponseSend(qctx.response, authoritative=True) + + +def resend_server() -> AsyncDnsServer: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + RootNSHandler(), + ExampleNSHandler(), + CookieHandler(), + NoErrorHandler(), + ) + return server + + +def main() -> None: + resend_server().run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2 new file mode 100644 index 0000000000..e686eaaa42 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/named.conf.j2 @@ -0,0 +1,17 @@ +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + listen-on-v6 { none; }; + query-source-v6 none; + recursion yes; + dnssec-validation no; +}; + +zone "." IN { + type hint; + file "root.hint"; +}; diff --git a/bin/tests/system/resend_loop/ns4/root.hint b/bin/tests/system/resend_loop/ns4/root.hint new file mode 100644 index 0000000000..3889a8b353 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/root.hint @@ -0,0 +1,14 @@ +; 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. + +$TTL 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.3 diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py new file mode 100644 index 0000000000..a9a236fa58 --- /dev/null +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -0,0 +1,85 @@ +# 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 re import compile as Re + +import dns.message + +import isctest + + +# This test verifies the query pattern when the upstream behaves badly. +# In this scenario, the upstream server (ans3) always responds with a +# BADCOOKIE error for queries within the "example" zone, even on TCP. +# The resolver (ns4), should not resend the same queries over and over +# again, up to the max-query-count threshold. Instead, the expected +# pattern is: +# 1. Priming query, getting the NS for . +# 2. Getting the NS for example. +# 3. Trying to resolve test.example. +# 4. Trying again, but now with the server cookie. +# 5. Trying again, now over TCP. +# +# This means we expect 5 recursion queries trying to resolve test.example. +def test_resend_loop_badcookie(ns4): + sending_packet = Re("sending packet from 10.53.0.4#[0-9]+ to 10.53.0.3#[0-9]+") + received_packet = Re("received packet from 10.53.0.3#[0-9]+ to 10.53.0.4#[0-9]+") + + log_sequence = [ + # 1. Priming query, getting the NS for . + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re(".\\s+IN\\s+NS"), + # 2. Getting the NS for example. + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re("example.\\s+IN\\s+NS"), + # 3. Trying to resolve test.example. + sending_packet, + Re("COOKIE: [0-9a-z]{16}$"), + Re("test.example.\\s+IN\\s+A"), + # Get the first BADCOOKIE error. + "UDP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + # 4. Trying again, but now with the server cookie. + sending_packet, + Re("test.example.\\s+IN\\s+A"), + # Get BADCOOKIE error again. + "UDP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + # 5. Trying again, now over TCP. + sending_packet, + Re("test.example.\\s+IN\\s+A"), + # Fails and give up. + "TCP response", + received_packet, + "BADCOOKIE", + Re("COOKIE: [0-9a-z]{16}1122334455667788"), + Re("test.example.\\s+IN\\s+A"), + ] + + msg = dns.message.make_query("test.example", "A") + with ns4.watch_log_from_here() as watcher: + res = isctest.query.udp(msg, ns4.ip) + watcher.wait_for_sequence(log_sequence) + + assert len(ns4.log.grep(sending_packet)) == 5 + + isctest.check.servfail(res) + + prohibited_log = "query failed (timed out) for test.example/IN/A" + assert prohibited_log not in ns4.log diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh index 18893edd69..06ef98f697 100755 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -783,10 +783,12 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) -echo_i "checking NXDOMAIN is returned when querying non existing domain in CH class ($n)" +echo_i "checking REFUSED is returned when querying non existing domain in CH class ($n)" ret=0 -dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.${n} || ret=1 -grep "status: NXDOMAIN" dig.ns1.out.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 hostname.chaostest txt ch >dig.ns1.out.1.${n} || ret=1 +grep "status: NOERROR" dig.ns1.out.1.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.2.${n} || ret=1 +grep "status: REFUSED" dig.ns1.out.2.${n} >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/bin/tests/system/selfpointedglue/ns1/named.conf.j2 b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 new file mode 100644 index 0000000000..fd83fc3c19 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns1/named.conf.j2 @@ -0,0 +1,28 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + recursion no; + dnssec-validation no; +}; + +zone "." { + type primary; + file "root.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns1/root.db b/bin/tests/system/selfpointedglue/ns1/root.db new file mode 100644 index 0000000000..bfbf049b80 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns1/root.db @@ -0,0 +1,24 @@ +; 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. + +$TTL 300 +. IN SOA owner.root-servers.nil. a.root.servers.nil. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +tld. NS ns.tld. +ns.tld. A 10.53.0.2 diff --git a/bin/tests/system/selfpointedglue/ns2/named.conf.j2 b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 new file mode 100644 index 0000000000..2993832da2 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/named.conf.j2 @@ -0,0 +1,28 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + recursion no; + dnssec-validation no; +}; + +zone "tld." { + type primary; + file "tld.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns2/tld.db b/bin/tests/system/selfpointedglue/ns2/tld.db new file mode 100644 index 0000000000..66f7925d99 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/tld.db @@ -0,0 +1,30 @@ +; 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. + +$TTL 300 +tld. IN SOA owner.tld. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +tld. NS ns.tld. +ns.tld. A 10.53.0.2 + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + +example2.tld. NS ns.example2.tld. +ns.example2.tld. A 10.53.0.3 + +example3.tld. NS ns.example3.tld. +ns.example3.tld. A 10.53.0.3 diff --git a/bin/tests/system/selfpointedglue/ns3/example.tld.db b/bin/tests/system/selfpointedglue/ns3/example.tld.db new file mode 100644 index 0000000000..2a599ee876 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example.tld.db @@ -0,0 +1,184 @@ +; 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. + +$TTL 300 +example.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + +sub.example.tld. NS ns01.sub.example.tld. +sub.example.tld. NS ns02.sub.example.tld. +sub.example.tld. NS ns03.sub.example.tld. +sub.example.tld. NS ns04.sub.example.tld. +sub.example.tld. NS ns05.sub.example.tld. +sub.example.tld. NS ns06.sub.example.tld. +sub.example.tld. NS ns07.sub.example.tld. +sub.example.tld. NS ns08.sub.example.tld. +sub.example.tld. NS ns09.sub.example.tld. +sub.example.tld. NS ns10.sub.example.tld. + +ns01.sub.example.tld. A 10.53.0.5 +ns01.sub.example.tld. A 10.53.0.6 +ns01.sub.example.tld. A 10.53.0.7 +ns01.sub.example.tld. A 10.53.0.8 +ns01.sub.example.tld. A 10.53.0.9 +ns01.sub.example.tld. A 10.53.0.10 +ns01.sub.example.tld. A 10.53.1.1 +ns01.sub.example.tld. A 10.53.1.2 +ns01.sub.example.tld. A 10.53.2.1 +ns01.sub.example.tld. A 10.53.0.3 +ns01.sub.example.tld. A 127.0.0.1 +ns01.sub.example.tld. A 127.0.0.2 +; Those addresses won't be used (exceed the max-delegation-servers). +ns01.sub.example.tld. A 127.0.0.3 +ns01.sub.example.tld. A 127.0.0.4 + +ns02.sub.example.tld. A 10.53.0.5 +ns02.sub.example.tld. A 10.53.0.6 +ns02.sub.example.tld. A 10.53.0.7 +ns02.sub.example.tld. A 10.53.0.8 +ns02.sub.example.tld. A 10.53.0.9 +ns02.sub.example.tld. A 10.53.0.10 +ns02.sub.example.tld. A 10.53.1.1 +ns02.sub.example.tld. A 10.53.1.2 +ns02.sub.example.tld. A 10.53.2.1 +ns02.sub.example.tld. A 10.53.0.3 +ns02.sub.example.tld. A 127.0.0.1 +ns02.sub.example.tld. A 127.0.0.2 +ns02.sub.example.tld. A 127.0.0.3 +ns02.sub.example.tld. A 127.0.0.4 + +ns03.sub.example.tld. A 10.53.0.5 +ns03.sub.example.tld. A 10.53.0.6 +ns03.sub.example.tld. A 10.53.0.7 +ns03.sub.example.tld. A 10.53.0.8 +ns03.sub.example.tld. A 10.53.0.9 +ns03.sub.example.tld. A 10.53.0.10 +ns03.sub.example.tld. A 10.53.1.1 +ns03.sub.example.tld. A 10.53.1.2 +ns03.sub.example.tld. A 10.53.2.1 +ns03.sub.example.tld. A 10.53.0.3 +ns03.sub.example.tld. A 127.0.0.1 +ns03.sub.example.tld. A 127.0.0.2 +ns03.sub.example.tld. A 127.0.0.3 +ns03.sub.example.tld. A 127.0.0.4 + +ns04.sub.example.tld. A 10.53.0.5 +ns04.sub.example.tld. A 10.53.0.6 +ns04.sub.example.tld. A 10.53.0.7 +ns04.sub.example.tld. A 10.53.0.8 +ns04.sub.example.tld. A 10.53.0.9 +ns04.sub.example.tld. A 10.53.0.10 +ns04.sub.example.tld. A 10.53.1.1 +ns04.sub.example.tld. A 10.53.1.2 +ns04.sub.example.tld. A 10.53.2.1 +ns04.sub.example.tld. A 10.53.0.3 +ns04.sub.example.tld. A 127.0.0.1 +ns04.sub.example.tld. A 127.0.0.2 +ns04.sub.example.tld. A 127.0.0.3 +ns04.sub.example.tld. A 127.0.0.4 + +ns05.sub.example.tld. A 10.53.0.5 +ns05.sub.example.tld. A 10.53.0.6 +ns05.sub.example.tld. A 10.53.0.7 +ns05.sub.example.tld. A 10.53.0.8 +ns05.sub.example.tld. A 10.53.0.9 +ns05.sub.example.tld. A 10.53.0.10 +ns05.sub.example.tld. A 10.53.1.1 +ns05.sub.example.tld. A 10.53.1.2 +ns05.sub.example.tld. A 10.53.2.1 +ns05.sub.example.tld. A 10.53.0.3 +ns05.sub.example.tld. A 127.0.0.1 +ns05.sub.example.tld. A 127.0.0.2 +ns05.sub.example.tld. A 127.0.0.3 +ns05.sub.example.tld. A 127.0.0.4 + +ns06.sub.example.tld. A 10.53.0.5 +ns06.sub.example.tld. A 10.53.0.6 +ns06.sub.example.tld. A 10.53.0.7 +ns06.sub.example.tld. A 10.53.0.8 +ns06.sub.example.tld. A 10.53.0.9 +ns06.sub.example.tld. A 10.53.0.10 +ns06.sub.example.tld. A 10.53.1.1 +ns06.sub.example.tld. A 10.53.1.2 +ns06.sub.example.tld. A 10.53.2.1 +ns06.sub.example.tld. A 10.53.0.3 +ns06.sub.example.tld. A 127.0.0.1 +ns06.sub.example.tld. A 127.0.0.2 +ns06.sub.example.tld. A 127.0.0.3 +ns06.sub.example.tld. A 127.0.0.4 + +ns07.sub.example.tld. A 10.53.0.5 +ns07.sub.example.tld. A 10.53.0.6 +ns07.sub.example.tld. A 10.53.0.7 +ns07.sub.example.tld. A 10.53.0.8 +ns07.sub.example.tld. A 10.53.0.9 +ns07.sub.example.tld. A 10.53.0.10 +ns07.sub.example.tld. A 10.53.1.1 +ns07.sub.example.tld. A 10.53.1.2 +ns07.sub.example.tld. A 10.53.2.1 +ns07.sub.example.tld. A 10.53.0.3 +ns07.sub.example.tld. A 127.0.0.1 +ns07.sub.example.tld. A 127.0.0.2 +ns07.sub.example.tld. A 127.0.0.3 +ns07.sub.example.tld. A 127.0.0.4 + +ns08.sub.example.tld. A 10.53.0.5 +ns08.sub.example.tld. A 10.53.0.6 +ns08.sub.example.tld. A 10.53.0.7 +ns08.sub.example.tld. A 10.53.0.8 +ns08.sub.example.tld. A 10.53.0.9 +ns08.sub.example.tld. A 10.53.0.10 +ns08.sub.example.tld. A 10.53.1.1 +ns08.sub.example.tld. A 10.53.1.2 +ns08.sub.example.tld. A 10.53.2.1 +ns08.sub.example.tld. A 10.53.0.3 +ns08.sub.example.tld. A 127.0.0.1 +ns08.sub.example.tld. A 127.0.0.2 +ns08.sub.example.tld. A 127.0.0.3 +ns08.sub.example.tld. A 127.0.0.4 + +ns09.sub.example.tld. A 10.53.0.5 +ns09.sub.example.tld. A 10.53.0.6 +ns09.sub.example.tld. A 10.53.0.7 +ns09.sub.example.tld. A 10.53.0.8 +ns09.sub.example.tld. A 10.53.0.9 +ns09.sub.example.tld. A 10.53.0.10 +ns09.sub.example.tld. A 10.53.1.1 +ns09.sub.example.tld. A 10.53.1.2 +ns09.sub.example.tld. A 10.53.2.1 +ns09.sub.example.tld. A 10.53.0.3 +ns09.sub.example.tld. A 127.0.0.1 +ns09.sub.example.tld. A 127.0.0.2 +ns09.sub.example.tld. A 127.0.0.3 +ns09.sub.example.tld. A 127.0.0.4 + +ns10.sub.example.tld. A 10.53.0.5 +ns10.sub.example.tld. A 10.53.0.6 +ns10.sub.example.tld. A 10.53.0.7 +ns10.sub.example.tld. A 10.53.0.8 +ns10.sub.example.tld. A 10.53.0.9 +ns10.sub.example.tld. A 10.53.0.10 +ns10.sub.example.tld. A 10.53.1.1 +ns10.sub.example.tld. A 10.53.1.2 +ns10.sub.example.tld. A 10.53.2.1 +ns10.sub.example.tld. A 10.53.0.3 +ns10.sub.example.tld. A 127.0.0.1 +ns10.sub.example.tld. A 127.0.0.2 +ns10.sub.example.tld. A 127.0.0.3 +ns10.sub.example.tld. A 127.0.0.4 diff --git a/bin/tests/system/selfpointedglue/ns3/example2.tld.db b/bin/tests/system/selfpointedglue/ns3/example2.tld.db new file mode 100644 index 0000000000..bcab6e38c1 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example2.tld.db @@ -0,0 +1,33 @@ +; 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. + +$TTL 300 +example2.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example2.tld. NS ns.example2.tld. +ns.example2.tld. A 10.53.0.3 + +sub.example2.tld. NS ns01.sub.example2.tld. +sub.example2.tld. NS ns02.sub.example2.tld. +sub.example2.tld. NS ns03.sub.example2.tld. + +ns01.sub.example2.tld. A 10.53.1.1 +ns01.sub.example2.tld. A 10.53.0.5 +ns02.sub.example2.tld. A 10.53.1.2 +ns02.sub.example2.tld. A 10.53.0.6 +ns03.sub.example2.tld. A 10.53.2.1 +ns03.sub.example2.tld. A 10.53.0.7 diff --git a/bin/tests/system/selfpointedglue/ns3/example3.tld.db b/bin/tests/system/selfpointedglue/ns3/example3.tld.db new file mode 100644 index 0000000000..e2c522cc3e --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example3.tld.db @@ -0,0 +1,63 @@ +; 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. + +$TTL 300 +example3.tld. IN SOA owner.dnshoster.tld. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) + +example3.tld. NS ns.example3.tld. +ns.example3.tld. A 10.53.0.3 + +sub.example3.tld. NS ns01.sub.example3.tld. +sub.example3.tld. NS ns02.sub.example3.tld. +sub.example3.tld. NS ns03.sub.example3.tld. +sub.example3.tld. NS ns04.sub.example3.tld. +sub.example3.tld. NS ns05.sub.example3.tld. +sub.example3.tld. NS ns06.sub.example3.tld. +sub.example3.tld. NS ns07.sub.example3.tld. +sub.example3.tld. NS ns08.sub.example3.tld. +sub.example3.tld. NS ns09.sub.example3.tld. +sub.example3.tld. NS ns10.sub.example3.tld. + +ns01.sub.example3.tld. A 10.53.0.5 +ns01.sub.example3.tld. A 10.53.0.6 + +ns02.sub.example3.tld. A 10.53.0.5 +ns02.sub.example3.tld. A 10.53.0.6 + +ns03.sub.example3.tld. A 10.53.0.5 +ns03.sub.example3.tld. A 10.53.0.6 + +ns04.sub.example3.tld. A 10.53.0.5 +ns04.sub.example3.tld. A 10.53.0.6 + +ns05.sub.example3.tld. A 10.53.0.5 +ns05.sub.example3.tld. A 10.53.0.6 + +ns06.sub.example3.tld. A 10.53.0.5 +ns06.sub.example3.tld. A 10.53.0.6 + +ns07.sub.example3.tld. A 10.53.0.5 +ns07.sub.example3.tld. A 10.53.0.6 + +ns08.sub.example3.tld. A 10.53.0.5 +ns08.sub.example3.tld. A 10.53.0.6 + +ns09.sub.example3.tld. A 10.53.0.5 +ns09.sub.example3.tld. A 10.53.0.6 + +ns10.sub.example3.tld. A 10.53.0.5 +ns10.sub.example3.tld. A 10.53.0.6 diff --git a/bin/tests/system/selfpointedglue/ns3/named.conf.j2 b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 new file mode 100644 index 0000000000..0c50ca8658 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 @@ -0,0 +1,49 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { + 10.53.0.3; + 10.53.0.5; + 10.53.0.6; + 10.53.0.7; + 10.53.0.8; + 10.53.0.9; + 10.53.0.10; + 10.53.1.1; + 10.53.1.2; + 10.53.2.1; + }; + recursion no; + dnssec-validation no; +}; + +zone "example.tld." { + type primary; + file "example.tld.db"; +}; + +zone "example2.tld." { + type primary; + file "example2.tld.db"; +}; + +zone "example3.tld." { + type primary; + file "example3.tld.db"; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/named.args.j2 b/bin/tests/system/selfpointedglue/ns4/named.args.j2 new file mode 100644 index 0000000000..68f1511c7c --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.args.j2 @@ -0,0 +1 @@ +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/selfpointedglue/ns4/named.conf.j2 b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 new file mode 100644 index 0000000000..09fbdd4e70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.conf.j2 @@ -0,0 +1,59 @@ +/* + * 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. + */ +{% set maxdelegationservers = maxdelegationservers | default(None) %} + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; + {% if maxdelegationservers %} + @maxdelegationservers@ + {% endif %} +}; + +/* + * Forcing TCP ensures that ADDITIONAL won't be truncated (responses won't have + * the TC flag, hence the resolver won't retry using TCP by itself, see + * https://datatracker.ietf.org/doc/html/rfc2181#section-9) + */ +server 10.53.0.3 { tcp-only true; }; +server 10.53.0.5 { tcp-only true; }; +server 10.53.0.6 { tcp-only true; }; +server 10.53.0.7 { tcp-only true; }; +server 10.53.0.8 { tcp-only true; }; +server 10.53.0.9 { tcp-only true; }; +server 10.53.0.10 { tcp-only true; }; +server 10.53.1.1 { tcp-only true; }; +server 10.53.1.2 { tcp-only true; }; +server 10.53.2.1 { tcp-only true; }; + +zone "." { + type hint; + file "root.hint"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/selfpointedglue/ns4/root.hint b/bin/tests/system/selfpointedglue/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/root.hint @@ -0,0 +1,14 @@ +; 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. + +$TTL 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.1 diff --git a/bin/tests/system/selfpointedglue/tests_selfpointedglue.py b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py new file mode 100644 index 0000000000..d559b76ea8 --- /dev/null +++ b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py @@ -0,0 +1,157 @@ +# 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 os + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_ips_and_queries(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.4:38507 -> 10.53.0.3:22047 TCP 56b sub.example.tld/IN/NS + _, _, _, _, _, dst, _, _, query = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return (ip, query) + + +def extract_dnstap(ns, expectedlen): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + assert expectedlen == len(lines) + return list(map(line_to_ips_and_queries, lines)) + + +# Because DNSTAP doesn't have ordering guarantee, the order doesn't matter here. +def expect_ip_and_query(expected_ips_and_queries, ips_and_queries): + found_count = 0 + for expected_ip, expected_query in expected_ips_and_queries: + found = False + for ip, query in ips_and_queries: + if ip == expected_ip and query == expected_query: + found = True + found_count += 1 + break + assert found + assert found_count == len(expected_ips_and_queries) + + +def expect_query(expected_query, expected_query_count, ips_and_queries): + count = 0 + for _, query in ips_and_queries: + if query == expected_query: + count += 1 + assert count == expected_query_count + + +def test_selfpointedglue1(ns4): + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + # 4 queries to get to the delegation. + # 13 queries to delegation NS servers. + ips_and_queries = extract_dnstap(ns4, 17) + + # Thanks to the de-duplication, only the first 13 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 13*10 + # (13 per NS, with 10 NS). + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example.tld/IN/NS"), + ("10.53.0.3", "sub.example.tld/IN/NS"), + ("10.53.0.3", "a.sub.example.tld/IN/A"), + ("10.53.0.5", "a.sub.example.tld/IN/A"), + ("10.53.0.6", "a.sub.example.tld/IN/A"), + ("10.53.0.7", "a.sub.example.tld/IN/A"), + ("10.53.0.8", "a.sub.example.tld/IN/A"), + ("10.53.0.9", "a.sub.example.tld/IN/A"), + ("10.53.0.10", "a.sub.example.tld/IN/A"), + ("10.53.1.1", "a.sub.example.tld/IN/A"), + ("10.53.1.2", "a.sub.example.tld/IN/A"), + ("10.53.2.1", "a.sub.example.tld/IN/A"), + ("10.53.0.3", "a.sub.example.tld/IN/A"), + ("127.0.0.1", "a.sub.example.tld/IN/A"), + ("127.0.0.2", "a.sub.example.tld/IN/A"), + ], + ips_and_queries, + ) + + +# This test is useful because the one above hits the max-delegation-servers +# from the first NS name lookup. This one doesn't, because there is only 2 +# addresses per NS, but the deduplication avoid the explosion of duplicate +# addresses. +def test_selfpointedglue2(ns4): + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + msg = isctest.query.create("a.sub.example3.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + # 4 queries to get to the delegation. + # 2 queries to delegation NS servers. + ips_and_queries = extract_dnstap(ns4, 6) + + # Thanks to the de-duplication, only the first 2 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 2*10 + # (2 per NS with 10 NS). + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example3.tld/IN/NS"), + ("10.53.0.3", "sub.example3.tld/IN/NS"), + ("10.53.0.5", "a.sub.example3.tld/IN/A"), + ("10.53.0.6", "a.sub.example3.tld/IN/A"), + ], + ips_and_queries, + ) + + +def test_selfpointedglue_nslimit(ns4, templates): + templates.render( + "ns4/named.conf", {"maxdelegationservers": "max-delegation-servers 2;"} + ) + with ns4.watch_log_from_here() as watcher: + ns4.rndc("flush") + ns4.rndc("reload") + watcher.wait_for_line("running") + + msg = isctest.query.create("a.sub.example2.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, 6) + + # Checking the beginning of the resolution + expect_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example2.tld/IN/NS"), + ("10.53.0.3", "sub.example2.tld/IN/NS"), + ], + ips_and_queries, + ) + + expect_query("a.sub.example2.tld/IN/A", 2, ips_and_queries) diff --git a/bin/tests/system/srtt/README b/bin/tests/system/srtt/README new file mode 100644 index 0000000000..c86a697931 --- /dev/null +++ b/bin/tests/system/srtt/README @@ -0,0 +1,18 @@ +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. + +ns1 is root + +ans{2-5} simulates four NS servers making authority on the same domain +`example.`. ans2 is the quickest to answer, followed by ans3, then ans4, with +ans5 being the slowest. + +ns6 is a resolver diff --git a/bin/tests/system/srtt/ans2/ans.py b/bin/tests/system/srtt/ans2/ans.py new file mode 100644 index 0000000000..147a65f828 --- /dev/null +++ b/bin/tests/system/srtt/ans2/ans.py @@ -0,0 +1,36 @@ +""" +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 dns.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo99Handler(DelayedQnameRangeHandler): + max_qname = 99 + delay = 0.0 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo99Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans3/ans.py b/bin/tests/system/srtt/ans3/ans.py new file mode 100644 index 0000000000..ecd590afd0 --- /dev/null +++ b/bin/tests/system/srtt/ans3/ans.py @@ -0,0 +1,36 @@ +""" +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 dns.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo199Handler(DelayedQnameRangeHandler): + max_qname = 199 + delay = 0.03 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo199Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans4/ans.py b/bin/tests/system/srtt/ans4/ans.py new file mode 100644 index 0000000000..af337c27fb --- /dev/null +++ b/bin/tests/system/srtt/ans4/ans.py @@ -0,0 +1,36 @@ +""" +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 dns.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo299Handler(DelayedQnameRangeHandler): + max_qname = 299 + delay = 0.08 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo299Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ans5/ans.py b/bin/tests/system/srtt/ans5/ans.py new file mode 100644 index 0000000000..8bac83a798 --- /dev/null +++ b/bin/tests/system/srtt/ans5/ans.py @@ -0,0 +1,36 @@ +""" +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 dns.rcode + +from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries + +from ..srtt_ans import DelayedQnameRangeHandler + + +class Foo1ToFoo399Handler(DelayedQnameRangeHandler): + max_qname = 399 + delay = 0.15 + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + Foo1ToFoo399Handler(), + IgnoreAllQueries(), + ) + server.run() + + +if __name__ == "__main__": + main() diff --git a/bin/tests/system/srtt/ns1/named.conf.j2 b/bin/tests/system/srtt/ns1/named.conf.j2 new file mode 100644 index 0000000000..eb079c95ab --- /dev/null +++ b/bin/tests/system/srtt/ns1/named.conf.j2 @@ -0,0 +1,29 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion no; + notify yes; +}; + +zone "." { + type primary; + file "root.db"; +}; diff --git a/bin/tests/system/srtt/ns1/root.db b/bin/tests/system/srtt/ns1/root.db new file mode 100644 index 0000000000..29ecd1d89d --- /dev/null +++ b/bin/tests/system/srtt/ns1/root.db @@ -0,0 +1,36 @@ +; 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. + +$TTL 300 +. IN SOA owner.root-servers.nil. a.root-servers.nil. ( + 2000042100 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +; The idea is that the resolver would do 2 ADB lookups, so there would be 2 +; find list, both with 2 IPs in it. ns1 (which is actually ans2 and ans5) would +; have both the slowest and fastest addresses. ns2 (which is actually ans3 and +; ans4) would have two addresses in the middle. + +example. NS ns1.example. +example. NS ns1.example. +example. NS ns2.example. +example. NS ns2.example. + +ns1.example. A 10.53.0.2 ; delay is 0 +ns1.example. A 10.53.0.5 ; delay is 0.15 +ns2.example. A 10.53.0.4 ; delay is 0.08 +ns2.example. A 10.53.0.3 ; delay is 0.03 diff --git a/bin/tests/system/srtt/ns6/named.args b/bin/tests/system/srtt/ns6/named.args new file mode 100644 index 0000000000..b5de5874ec --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.args @@ -0,0 +1 @@ +-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/srtt/ns6/named.conf.j2 b/bin/tests/system/srtt/ns6/named.conf.j2 new file mode 100644 index 0000000000..1d27505a8e --- /dev/null +++ b/bin/tests/system/srtt/ns6/named.conf.j2 @@ -0,0 +1,41 @@ +/* + * 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. + */ + + +options { + query-source address 10.53.0.6; + notify-source 10.53.0.6; + transfer-source 10.53.0.6; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.6; }; + listen-on-v6 { none; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.6 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "." { + type hint; + file "../../_common/root.hint"; +}; diff --git a/bin/tests/system/srtt/srtt_ans.py b/bin/tests/system/srtt/srtt_ans.py new file mode 100644 index 0000000000..9387486993 --- /dev/null +++ b/bin/tests/system/srtt/srtt_ans.py @@ -0,0 +1,59 @@ +""" +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 collections.abc import AsyncGenerator + +import abc + +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import DnsResponseSend, QnameQtypeHandler, QueryContext + + +class DelayedQnameRangeHandler(QnameQtypeHandler): + """ + Respond to queries for QNAMEs "foo1.example." through "foo.example." + with QTYPE=A, where must be defined by the subclass. Every response is + delayed by a fixed amount of time, which must also be defined (in seconds) + by the subclass. + """ + + @property + def qnames(self) -> list[str]: + return [f"foo{x}.example." for x in range(1, self.max_qname + 1)] + + qtypes = [dns.rdatatype.A] + + @property + @abc.abstractmethod + def max_qname(self) -> int: + raise NotImplementedError + + @property + @abc.abstractmethod + def delay(self) -> float: + raise NotImplementedError + + def __str__(self) -> str: + return f"{self.__class__.__name__}(foo[1-{self.max_qname}].example/A)" + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + a_rrset = dns.rrset.from_text( + qctx.qname, 300, dns.rdataclass.IN, dns.rdatatype.A, "10.53.9.9" + ) + qctx.response.answer.append(a_rrset) + yield DnsResponseSend(qctx.response, delay=self.delay) diff --git a/bin/tests/system/srtt/tests_srtt.py b/bin/tests/system/srtt/tests_srtt.py new file mode 100644 index 0000000000..0ce18fcccf --- /dev/null +++ b/bin/tests/system/srtt/tests_srtt.py @@ -0,0 +1,89 @@ +# 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 os + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_dst_ips(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.6:38507 -> 10.53.0.3:22047 TCP 56b fooXXX.example./IN/NS + _, _, _, _, _, dst, _, _, _ = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return ip + + +def extract_dnstap(ns): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + return map(line_to_dst_ips, lines) + + +def assert_used_auth(ns, authip): + ips = extract_dnstap(ns) + queries = 0 + matches = 0 + for ip in ips: + queries += 1 + if ip == authip: + matches += 1 + assert matches > 85 + assert queries <= 115 + + +def test_srtt(ns6): + for i in range(1, 100): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.2") + + for i in range(100, 200): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.3") + + for i in range(200, 300): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + + assert_used_auth(ns6, "10.53.0.4") + + for i in range(300, 400): + msg = isctest.query.create(f"foo{i}.example.", "A") + res = isctest.query.udp(msg, ns6.ip) + isctest.check.noerror(res) + assert len(res.answer[0]) == 1 + res.answer[0].ttl = 300 + assert str(res.answer[0]) == f"foo{i}.example. 300 IN A 10.53.9.9" + assert_used_auth(ns6, "10.53.0.5") diff --git a/bin/tests/system/tkeyleak/ns1/dns.keytab b/bin/tests/system/tkeyleak/ns1/dns.keytab new file mode 100644 index 0000000000..d5a09b060a Binary files /dev/null and b/bin/tests/system/tkeyleak/ns1/dns.keytab differ diff --git a/bin/tests/system/tkeyleak/ns1/example.db.in b/bin/tests/system/tkeyleak/ns1/example.db.in new file mode 100644 index 0000000000..dd200dc9bc --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/example.db.in @@ -0,0 +1,21 @@ +; 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. + +$TTL 300 +@ IN SOA ns.example. admin.example. ( + 1 ; serial + 3600 ; refresh + 900 ; retry + 604800 ; expire + 300 ; minimum + ) +@ IN NS ns.example. +ns IN A 10.53.0.1 diff --git a/bin/tests/system/tkeyleak/ns1/named.conf.j2 b/bin/tests/system/tkeyleak/ns1/named.conf.j2 new file mode 100644 index 0000000000..f16b53414c --- /dev/null +++ b/bin/tests/system/tkeyleak/ns1/named.conf.j2 @@ -0,0 +1,39 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; + recursion no; + dnssec-validation no; + tkey-gssapi-keytab "dns.keytab"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "example" { + type primary; + file "example.db"; +}; diff --git a/bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py b/bin/tests/system/tkeyleak/prereq.sh similarity index 50% rename from bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py rename to bin/tests/system/tkeyleak/prereq.sh index bb788d1708..8a68ae7df1 100644 --- a/bin/tests/system/nohintswarn_bindchaos/tests_nohintswarn_bindchaos.py +++ b/bin/tests/system/tkeyleak/prereq.sh @@ -1,3 +1,5 @@ +#!/bin/sh + # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,17 +11,11 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -import isctest +. ../conf.sh +$FEATURETEST --gssapi || { + echo_i "gssapi not supported - skipping tkeyleak test" + exit 255 +} -def test_nohintswarn_bindchaos(ns1): - found = True - try: - with ns1.watch_log_from_start(timeout=1) as watcher: - watcher.wait_for_line("no root hints for view '_bind'") - except isctest.log.watchlog.WatchLogTimeout: - found = False - assert found is False - - with ns1.watch_log_from_start() as watcher: - watcher.wait_for_line("no root hints for view 'bar'") +exit 0 diff --git a/bin/tests/system/tkeyleak/setup.sh b/bin/tests/system/tkeyleak/setup.sh new file mode 100644 index 0000000000..24a0026665 --- /dev/null +++ b/bin/tests/system/tkeyleak/setup.sh @@ -0,0 +1,17 @@ +#!/bin/sh -e + +# 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. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/example.db.in ns1/example.db diff --git a/bin/tests/system/tkeyleak/tests_tkeyleak.py b/bin/tests/system/tkeyleak/tests_tkeyleak.py new file mode 100644 index 0000000000..fd97c8540c --- /dev/null +++ b/bin/tests/system/tkeyleak/tests_tkeyleak.py @@ -0,0 +1,145 @@ +# 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. + +""" +Regression test for GSS-API context leak via repeated TKEY queries. + +An unauthenticated attacker could exhaust server memory by sending +repeated TKEY queries with crafted SPNEGO NegTokenInit tokens. +Each query triggers gss_accept_sec_context() which returns +GSS_S_CONTINUE_NEEDED and allocates a GSS context. On the unfixed +code path, the context handle in process_gsstkey() is never stored +or freed, leaking ~520 bytes per query. + +The fix rejects GSS_S_CONTINUE_NEEDED in dst_gssapi_acceptctx() and +deletes the context immediately. + +The key distinguishing signal in the TKEY response: + - CONTINUE (vulnerable): error=0, output token present, no TSIG + - BADKEY (fixed): error=17, no output token +""" + +import struct +import time + +import dns.name +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.TKEY +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + +TKEY_NAME = dns.name.from_text("test.key.") +GSSAPI_ALGORITHM = dns.name.from_text("gss-tsig.") +TKEY_MODE_GSSAPI = 3 + +# OID 1.2.840.113554.1.2.2 (Kerberos 5) +KRB5_OID = b"\x06\x09\x2a\x86\x48\x86\xf7\x12\x01\x02\x02" + +# OID 1.3.6.1.5.5.2 (SPNEGO) +SPNEGO_OID = b"\x06\x06\x2b\x06\x01\x05\x05\x02" + + +def der_encode(tag, data): + """Encode data in ASN.1 DER TLV format.""" + length = len(data) + if length < 128: + return tag + bytes([length]) + data + if length < 256: + return tag + b"\x81" + bytes([length]) + data + return tag + b"\x82" + struct.pack(">H", length) + data + + +def spnego_negtokeninit(): + """Build a SPNEGO NegTokenInit proposing krb5 without a mechToken. + + This forces gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED + because the acceptor recognizes the krb5 mechanism but has not + received an actual AP-REQ token yet. + """ + # MechTypeList ::= SEQUENCE OF MechType + mechtype_list = der_encode(b"\x30", KRB5_OID) + # [0] mechTypes + mechtypes = der_encode(b"\xa0", mechtype_list) + # NegTokenInit ::= SEQUENCE { mechTypes, ... } + negtokeninit = der_encode(b"\x30", mechtypes) + # [0] CONSTRUCTED (wrapping NegTokenInit) + wrapped = der_encode(b"\xa0", negtokeninit) + # APPLICATION 0 CONSTRUCTED (SPNEGO OID + body) + return der_encode(b"\x60", SPNEGO_OID + wrapped) + + +def make_tkey_query(token): + """Build a TKEY query with a GSS-API token in the additional section.""" + now = int(time.time()) + tkey_rdata = dns.rdtypes.ANY.TKEY.TKEY( + rdclass=dns.rdataclass.ANY, + rdtype=dns.rdatatype.TKEY, + algorithm=GSSAPI_ALGORITHM, + inception=now, + expiration=now + 86400, + mode=TKEY_MODE_GSSAPI, + error=0, + key=token, + other=b"", + ) + + msg = isctest.query.create(TKEY_NAME, dns.rdatatype.TKEY, dns.rdataclass.ANY) + rrset = msg.find_rrset( + msg.additional, + TKEY_NAME, + dns.rdataclass.ANY, + dns.rdatatype.TKEY, + create=True, + ) + rrset.add(tkey_rdata) + return msg + + +def test_tkey_gssapi_no_continuation(ns1): + """TKEY with a SPNEGO NegTokenInit must be rejected, not continued. + + On unfixed code, gss_accept_sec_context() returns CONTINUE_NEEDED + and the response has error=0 with an output token (the leaked path). + On fixed code, CONTINUE_NEEDED is rejected and the response has + error=BADKEY(17) with no output token. + """ + port = ns1.ports.dns + ip = ns1.ip + + msg = make_tkey_query(spnego_negtokeninit()) + res = dns.query.tcp(msg, ip, port=port, timeout=5) + + assert res is not None + + tkey = get_tkey_answer(res) + assert tkey is not None, "server did not return a TKEY answer" + assert ( + tkey.error != 0 + ), "server returned error=0 (GSS_S_CONTINUE_NEEDED not rejected)" + assert len(tkey.key) == 0, "server returned a continuation token" + + +def get_tkey_answer(response): + """Extract TKEY rdata from a DNS response, or None.""" + for rrset in response.answer: + if rrset.rdtype == dns.rdatatype.TKEY: + for rdata in rrset: + return rdata + return None diff --git a/bin/tests/system/unknown/tests.sh b/bin/tests/system/unknown/tests.sh index eb61f21f28..cbc2943f17 100644 --- a/bin/tests/system/unknown/tests.sh +++ b/bin/tests/system/unknown/tests.sh @@ -25,6 +25,11 @@ dig_cmd() { "$DIG" $DIGOPTS "$@" | grep -v '^;' } +dig_full() { + # shellcheck disable=SC2086 + "$DIG" $DIGOPTS "$@" +} + n=$((n + 1)) echo_i "querying for various representations of an IN A record ($n)" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do @@ -81,8 +86,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE1 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n - echo '\# 4 0A000001' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -93,8 +98,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TXT record ($n)" for i in 1 2 3 4; do ret=0 - dig_cmd +short @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n - echo '"hello"' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -105,8 +110,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE123 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n - echo '\# 1 00' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi diff --git a/bin/tests/system/xferquota/ns1/named.conf.j2 b/bin/tests/system/xferquota/ns1/named.conf.j2 index 9bba994dd4..90e9417c42 100644 --- a/bin/tests/system/xferquota/ns1/named.conf.j2 +++ b/bin/tests/system/xferquota/ns1/named.conf.j2 @@ -10,6 +10,8 @@ options { recursion no; dnssec-validation no; notify yes; + + transfers-out 3; }; key rndc_key { diff --git a/bin/tests/system/xferquota/ns3/named.conf.j2 b/bin/tests/system/xferquota/ns3/named.conf.j2 new file mode 100644 index 0000000000..4a0d70ca55 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/named.conf.j2 @@ -0,0 +1,46 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + recursion no; + dnssec-validation no; + + transfers-out 1; + allow-transfer { 10.53.0.2; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "." { + type primary; + file "root.db"; +}; + +zone "quota." { + type primary; + file "quota.db"; +}; diff --git a/bin/tests/system/xferquota/ns3/quota.db b/bin/tests/system/xferquota/ns3/quota.db new file mode 100644 index 0000000000..12a67d3d2a --- /dev/null +++ b/bin/tests/system/xferquota/ns3/quota.db @@ -0,0 +1,22 @@ +; 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. + +$TTL 300 +@ IN SOA ns1.quota. hostmaster.quota. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) + IN NS ns1.quota. +ns1 IN A 10.53.0.3 +www IN A 10.0.0.1 diff --git a/bin/tests/system/xferquota/ns3/root.db b/bin/tests/system/xferquota/ns3/root.db new file mode 100644 index 0000000000..a5ff0fc697 --- /dev/null +++ b/bin/tests/system/xferquota/ns3/root.db @@ -0,0 +1,21 @@ +; 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. + +$TTL 300 +. IN SOA ns.root. hostmaster.root. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 604800 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.3 diff --git a/bin/tests/system/xferquota/tests_xferquota.py b/bin/tests/system/xferquota/tests_xferquota.py index 2a95a6c1a8..6d127f9543 100644 --- a/bin/tests/system/xferquota/tests_xferquota.py +++ b/bin/tests/system/xferquota/tests_xferquota.py @@ -12,12 +12,15 @@ from re import compile as Re import glob +import multiprocessing import os import re import shutil import signal import time +import dns.message +import dns.query import dns.zone import pytest @@ -60,6 +63,9 @@ def test_xferquota(named_port, ns1, ns2): matching_line_count += 1 return matching_line_count == 300 + # The primary has 'transfers-out 3;', while the secondary has + # 'transfers-in 5; transfer-per-ns 5;'. This will allow all the zones + # to be eventually transferred, hitting the quotas now and then. isctest.run.retry_with_timeout(check_line_count, timeout=360) axfr_msg = isctest.query.create("zone000099.example.", "AXFR") @@ -79,3 +85,39 @@ def test_xferquota(named_port, ns1, ns2): with ns2.watch_log_from_start(timeout=30) as watcher: watcher.wait_for_line(pattern) query_and_compare(a_msg) + + +def _flood_unauthorized_axfrs(port, duration): + """Child process: send unauthorized AXFR requests for `duration` seconds.""" + deadline = time.monotonic() + duration + while time.monotonic() < deadline: + try: + msg = dns.message.make_query("quota.", "AXFR") + dns.query.tcp(msg, "10.53.0.3", port=port, timeout=2, source="10.53.0.1") + except Exception: # pylint: disable=broad-exception-caught + pass + + +def test_xfrquota_unauthorized_no_starve(named_port): + """Unauthorized AXFR clients must not consume XFR-out quota (GL #3859). + + ns3 is configured with transfers-out 1 and allow-transfer { 10.53.0.2; }. + We flood AXFR requests from unauthorized source processes (10.53.0.1) and + verify that an authorized client (10.53.0.2) can still transfer. + """ + with multiprocessing.Pool(10) as pool: + pool.starmap_async(_flood_unauthorized_axfrs, [(named_port, 5)] * 10) + + # Give the flood a moment to saturate + time.sleep(1) + + # Try an authorized AXFR from 10.53.0.2 multiple times to increase + # the chance of hitting the race window where quota is consumed. + zone = dns.zone.Zone("quota.") + dns.query.inbound_xfr( + "10.53.0.3", + zone, + port=named_port, + timeout=10, + source="10.53.0.2", + ) diff --git a/doc/arm/changelog.rst b/doc/arm/changelog.rst index c02a5680d4..742ae801cb 100644 --- a/doc/arm/changelog.rst +++ b/doc/arm/changelog.rst @@ -18,6 +18,7 @@ Changelog development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.21.22.rst .. include:: ../changelog/changelog-9.21.21.rst .. include:: ../changelog/changelog-9.21.20.rst .. include:: ../changelog/changelog-9.21.19.rst diff --git a/doc/arm/notes.rst b/doc/arm/notes.rst index fa13bbe88e..a434d7b276 100644 --- a/doc/arm/notes.rst +++ b/doc/arm/notes.rst @@ -47,6 +47,7 @@ The list of known issues affecting the latest version in the 9.21 branch can be found at https://gitlab.isc.org/isc-projects/bind9/-/wikis/Known-Issues-in-BIND-9.21 +.. include:: ../notes/notes-9.21.22.rst .. include:: ../notes/notes-9.21.21.rst .. include:: ../notes/notes-9.21.20.rst .. include:: ../notes/notes-9.21.19.rst diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index a42a08d326..10ba133ef1 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -4187,14 +4187,11 @@ Tuning .. namedconf:statement:: max-delegation-servers :tags: server - :short: Configure the maximum number of nameserver names considered for a delegation + :short: Configure the maximum number of nameservers considered for a delegation When looking up remote nameservers for a delegation, the list of nameserver names is sorted according to Canonical RR Ordering within an RRset (see - :rfc:`4034` Section 6.3), and the number of names for which :iscman:`named` - looks up IP addresses is capped at :any:`max-delegation-servers`. - - This capped list of nameserver names is then randomly shuffled every time + :rfc:`4034` Section 6.3). This list is then randomly shuffled every time :iscman:`named` needs additional remote addresses for those nameservers. This randomized selection works around situations where the first few nameserver names in the zone are unresponsive. @@ -4207,6 +4204,12 @@ Tuning outgoing DNS query is initiated only if the DNS resolver does not already have existing IP addresses for any of the nameserver names in the cache. + The known NS addresses for an NS name (cached from a previous resolution, or + the NS name has glues, or it is defined from a local zone or hints) are + counted as delegation servers. Thus, the maximum queries the resolver does + to resolve a name at a delegation point is capped at + :any:`max-delegation-servers`. + The default and recommended value is ``13``. This limit prevents excessive resource use while processing large or misconfigured delegations. The default value should only be increased in controlled environments where a remote diff --git a/doc/changelog/changelog-9.21.22.rst b/doc/changelog/changelog-9.21.22.rst new file mode 100644 index 0000000000..8d876fe5e9 --- /dev/null +++ b/doc/changelog/changelog-9.21.22.rst @@ -0,0 +1,768 @@ +.. 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. + +BIND 9.21.22 +------------ + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. ``3ddd7b8695`` + + Unauthorized clients could consume outgoing zone transfers quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + +- [CVE-2026-3592] Limit resolver server list size. ``e249148d75`` + + When resolving a domain with many nameservers that share overlapping + IP addresses (e.g., 10 NS records all pointing at the same set of + addresses), BIND could previously waste time querying duplicate + addresses and build up excessively large server lists. Deduplicate + addresses in the resolver's server list so that each unique IP is only + queried once per resolution attempt, regardless of how many NS records + point to it and cap the number of addresses stored per nameserver name + to 6 (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. :gl:`#5641` + +- [CVE-2026-3039] Fix GSS-API resource leak. ``01bdb7abeb`` + + Fixed a memory leak where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with tkey-gssapi-keytab configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. :gl:`#5752` + +- [CVE-2026-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. ``21c8ba4f0b`` + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with recursion yes in a non-IN view will log a warning at + startup, and named-checkconf flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. :gl:`#5784` + +- [CVE-2026-5950] Avoid unbounded recursion loop. ``5319c21761`` + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- [CVE-2026-5947] Fix crash in resolver when SIG(0)-signed responses are + received under load. ``9831f41894`` + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. :gl:`#5819` + +- Fix race condition in getsigningtime() ``d35a527ffb`` + + Compute qpzone_get_lock(elem->node) into a local variable while the + heap lock is still held, rather than dereferencing the stale elem + pointer after releasing the lock. A concurrent thread running + setsigningtime() (e.g. via IXFR apply on a worker thread) could free + the top-of-heap element between the heap lock release and the + dereference, causing a use-after-free. :gl:`#5883` :gl:`!11875` + +- [CVE-2026-3593] Fix use-after-free in DNS-over-HTTPS when processing + HTTP/2 SETTINGS frames. ``e33ff6bb0a`` + + A use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause named to crash when a client sends a flood of HTTP/2 + SETTINGS frames while a DoH response is being written. This affects + servers with DoH (DNS-over-HTTPS) enabled. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. + + For: #5755 + +New Features +~~~~~~~~~~~~ + +- Add DTRACE probes to the delegation cache. ``780ffe375f`` + + The new delegation cache, which stores NS-based and DELEG-based + delegations per view, is now instrumented with static user-space + tracing probes so that cache hit rate, insertion and lookup latency, + eviction pressure under memory limits, and removals triggered by rndc + flush-delegation can be observed on a running named. :gl:`!11855` + +Removed Features +~~~~~~~~~~~~~~~~ + +- Remove obsolete KEY record flags deprecated by RFC 3445. + ``1535b32dab`` + + KEY resource records originally defined NOAUTH, NOCONF, EXTENDED, and + ENTITY flags that were removed by RFC 3445 back in 2002. BIND still + carried code to parse and emit them, including the additional + two-octet flags field that followed when the EXTENDED bit was set. + That handling has been removed and the affected bit positions are now + reserved. + + Dropping the extended-flags handling also eliminates a possible crash + that could be reached when signing a zone containing an invalid key. + :gl:`#5900` :gl:`!11961` + +Feature Changes +~~~~~~~~~~~~~~~ + +- Embed default sanitizer flags in executables. ``7c60b7da8a`` + + Replicating CI failures requires the developer to piece together the + sanitizer flags by hand, reducing ergonomics. + + Fix this problem by embedding the relevant settings to the + executables. Symbol resolution still needs manual intervention by + setting the env variable `*SAN_SYMBOLIZER_PATH`. However, this doesn't + affect any behavior. + + The flags are passed though a meson-configured `sanitize.c.in` + template file to toggle which flags are included for the executable. + Using the built-in `__SANITIZE_XXX__` or `__has_feature` for this task + is more trouble than it's worth because only one of the two is + available in most GCC/clang versions, alongside the lack of + `__SANITIZE_UNDEFINED__` from GCC. + + Meson's own unit test execution sets its own `ASAN_OPTIONS` etc. To + prevent it from overriding the default options, we also pass the same + options to unit tests environment variables. + + A new script `ci/sanitizer-default-check.py` is used in CI to detect + if a build directory with sanitizers enabled has a meson `executable` + definition that doesn't include the sanitizer flag source file. + :gl:`#5469` :gl:`!10919` + +- Catch rare named crash in recursive resolution earlier for diagnosis. + ``3c9a848be7`` + + A rare crash has been observed in named while it is resolving upstream + nameserver addresses for a recursive query, surfacing as a + segmentation fault with no immediate clue as to the cause. This change + adds internal consistency checks so that a future occurrence of the + same condition aborts named with a diagnostic message at the point the + inconsistency arises, rather than corrupting state and crashing later + in an unrelated location. :gl:`#5602` :gl:`!11943` + +- Revert isdelegation() to return boolean value again. ``69d439cd1b`` + + :gl:`#5838` :gl:`!11792` + +- Fix CPU spikes and slow queries when cache approaches memory limit. + ``2e7e9f51db`` + + Spread cache cleanup probabilistically to avoid CPU usage spikes and a + drop in query throughput. :gl:`#5891` + +- Add a refcount to the vecheaders. ``a743ff1e44`` + + This MR changes the way the ownership of the vecheaders is tracked. + Before this MR, the ownership of the vecheader was implicitely tracked + through a mix of the refcount on the node owning the header, the + external refcount of the same node and the version. This has some + adverse consequences in terms of contention, such as that querying A + and AAAA glue hits the same refcount. + + This MR adds a refcount to the vecheader itself, allowing it to exist + independently of the node it is contained in. On its own, this would + create a cycle, where the node has a reference to the header, which + has a reference to the heap, which in turn has a reference to the + node. + + To break this cycle, this MR also moves from an "intrusive" heap, to a + more traditional one where pointers to the node and vecheader in the + heap are stored in a hashmap. :gl:`!11397` + +- Change NSEC3 and NSEC3PARAM rdata struct fields to use isc_region_t. + ``245c71dfac`` + + Replace the separate pointer+length field pairs in the NSEC3 and + NSEC3PARAM rdata structures (salt/salt_length, next/next_length, + typebits/len) with isc_region_t, making the fields self-describing and + eliminating a class of length-mismatch bugs. :gl:`!11592` + +- Document that named-checkzone must not run on untrusted input. + ``5b164b551f`` + + The zone-file parser implements $INCLUDE by opening whatever local + path the zone text names, and fragments of the included file leak + through parser error messages. There is no safe way to validate + untrusted zone text with named-checkzone or named-compilezone, so the + manual pages for both tools now warn against doing so. :gl:`!11901` + +- Don't set named curves explicitly in pre-3.0 libcrypto. ``2e92389905`` + + The function EC_KEY_set_asn1_flag is deprecated in AWS-LC. Fortunately + calling it to make sure we use named curve keys is entirely + unnecessary. + + More information for pre-3.0 libcrypto and significant forks are as + following: + + OpenSSL: Named curves were the default between 1.1.0 and 3.6.1 [1],[2] + + AWS-LC: Library only supports named curves in the first place [3] + + BoringSSL: Likewise with AWS-LC [4] + + LibreSSL: EC_GROUPs are named by default [5] + + [1]: https://github.com/openssl/openssl/commit/86f300d38540ead85543aee + 0cb30c32145931744 [2]: https://github.com/openssl/openssl/commit/9db6a + f922c48c5cab5398ef9f37e425e382f9440 [3]: https://github.com/aws/aws-lc + /blob/a605df416bc6ddd0a3b79d728770664ce2302e71/include/openssl/ec_key. + h#L442-L445 [4]: https://github.com/google/boringssl/blob/514abb73bb80 + 130000b46cf589190c967c6647cd/include/openssl/ec_key.h#L279-L280 [5]: h + ttps://github.com/libressl/openbsd/blob/c9338745181f31ae01336081edfdb7 + 38c0b76d5f/src/lib/libcrypto/ec/ec_lib.c#L94 :gl:`!11530` + +- Fix off by one error in dnssec-ksr sign. ``ae739daec2`` + + If the inception time of the signature is exactly equal to the + inactive time of the key, add the signature. :gl:`!11791` + +- Harden GSS-API context establishment in TKEY negotiation. + ``9212e1ac50`` + + Implement RFC 3645 Section 3.1.1 client-side check for REPLAY, MUTUAL, + and INTEG flags after gss_init_sec_context() completes. Add + server-side INTEG flag check after gss_accept_sec_context(). Also + fixes an uninitialized gss_name_t on the error path in + dst_gssapi_initctx(). + +- Implement RFC 3645 Section 4.1.1 key expiry check in TKEY. + ``6b6913c83b`` + + Check for existing TSIG keys before accepting a new GSS-API + negotiation and delete the key if it has expired. Previously, an + expired GSS key would permanently block re-negotiation for that name + until the server was restarted. :gl:`!11713` + +- Reduce memory footprint by actively returning unused memory to the OS. + ``460bf794a5`` + + Previously, :iscman:`named` relied on the default allocator settings + for releasing unused memory back to the operating system, which could + result in unnecessarily high resident memory usage. :iscman:`named` + now actively manages memory page purging. On systems using jemalloc, + background cleanup threads are enabled and the dirty page decay time + is reduced from 10 seconds to 5 seconds. Additionally, a volume-based + decay pass is triggered after every 16 MiB of freed memory. On + glibc-based systems, a similar volume-based mechanism using + malloc_trim() is used instead. :gl:`!11761` + +- Split up zone.c (zone manager) ``e99b5f80be`` + + In order to make `zone.c` more readable, split it up in separate + source files. This moves zone manager related code to `zonemgr.c`. + :gl:`!11726` + +- Split up zone.c (zone properties) ``36acc92131`` + + In order to make `zone.c` more readable, split it up in separate + source files. This moves most of the set and get functions to + `zoneproperties.c`. :gl:`!11501` + +Bug Fixes +~~~~~~~~~ + +- Check validator name when adding EDE text. ``a6b44a6007`` + + When a validator is being shut down, the associated name `val->name` + is set to NULL. This could cause a crash if a worker thread + subsequently added an EDE code with `val->name` in the extra text. + + `validator_addede()` now checks whether the name is NULL before trying + to add it to the extra text. :gl:`#5613` :gl:`!11945` + +- Use the zone file's basename as origin in DNSSEC tools. ``08fa344014`` + + In `dnssec-signzone` and `dnssec-verify`, when the zone origin is not + specified using the `-o` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + `dnssec-signzone -S example.com` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, `dnssec-signzone -S zones/example.com` will set the origin + value to `example.com`. :gl:`#5678` :gl:`!11360` + +- Fix a possible race condition during zone transfers. ``175b121bc3`` + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` :gl:`!11781` + +- Do not resend query after BADCOOKIE answer on TCP. ``53593e8e13`` + + When an upstream server answers BADCOOKIE, no matter which transport + is used, the resolver resends the query using TCP. However, if the + upstream server responded with BADCOOKIE again over TCP, the resolver + would keep resending until the maximum query count was reached. + + This is now fixed by no longer resending once the query has already + been sent over TCP. :gl:`#5804` + +- Make BIND9 compatible with OpenSSL 4. ``80794c5e87`` + + OPENSSL_cleanup() in OpenSSL 4 doesn't free the memory, and that is + not compatible with BIND 9's memory leak detection code. Don't use + custom allocation/deallocation functions for OpenSSL's internal memory + management. + + See https://github.com/openssl/openssl/pull/29721 :gl:`#5808` + :gl:`!11865` + +- Fix named crash when processing SIG records in dynamic updates. + ``5f0b2f255f`` + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` :gl:`!11864` + +- Fix wrong NSEC proof for empty non-terminals after IXFR. + ``15f058b5a2`` + + When a secondary received an IXFR that transitioned a zone from + unsigned to NSEC-signed, queries for empty non-terminal names returned + the zone apex NSEC record instead of the NSEC that actually covers the + queried name. The issue only occurred with incremental transfers; a + full AXFR or a server restart resolved it. :gl:`#5824` :gl:`!11786` + +- Fix rndc modzone behavior for a zone in named.conf. ``c56d4c13f9`` + + If a zone was present in the configuration file and not originally + added by `rndc addzone`, `rndc modzone` for that zone would succeed + once but subsequent `modzone` attempts would fail. This has been + fixed. :gl:`#5826` :gl:`!11744` + +- Fix zone verification of NSEC3 signed zones. ``0633effb5b`` + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted mirror zones which are NSEC3-signed, + `dnssec-signzone` and `dnssec-verifyzone`. This has been fixed. + :gl:`#5834` :gl:`!11804` + +- Fix 'rndc modzone' issue with non-existing zones. ``09a4b80301`` + + The :iscman:`named` process could terminate unexpectedly or become + subject to undefined behavior when issued an :option:`rndc modzone` + operation for a non-existing zone. This has been fixed. :gl:`#5848` + :gl:`!11844` + +- Fix zone filename token-parsing bug. ``ef29555ba4`` + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog member zone containing special characters like + '%' or '$' which could be interpreted as zone filename tokens and + trigger a case-sensitivity bug in the token-parsing code. This has + been fixed. :gl:`#5849` :gl:`!11839` + +- Prevent a crash when using both dns64 and filter-aaaa. ``3c60322fa3`` + + An assertion failure could be triggered if both `dns64` and the + `filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + :gl:`!11949` + +- Remove unnecessary dns_name_free call. ``bbdca691c0`` + + When processing a catalog zone member's primaries definition and there + is a TXT record containing an invalid name TSIG key name, + dns_name_free was incorrectly called triggering an assertion. This has + been fixed. :gl:`#5858` :gl:`!11832` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + ``120eaf546f`` + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` :gl:`!11917` + +- Add missing parenthesis to fxhash. ``a8d412ab21`` + + The fxhash implementation had a missing parenthesis that caused it to + diverge from Rust's reference implementation. This commit fixes this. + :gl:`#5882` :gl:`!11857` + +- Fix strict weak ordering violation in resign_sooner() ``13a6867757`` + + resign_sooner_values() only checked whether rhs was SOA-typed when + resign times were equal, but did not check lhs. When both entries were + SOA-typed with equal resign times, the comparison returned true in + both directions, violating irreflexivity and corrupting heap + invariants. + + Add lhs_typepair parameter and require lhs to be non-SOA for the + tie-breaking logic to apply. :gl:`#5884` :gl:`!11874` + +- Fix inverted gethostname() check in rndc status. ``4a8c6a0933`` + + The replacement of named_os_gethostname() with raw gethostname() + inverted the success check: the "localhost" fallback runs on success, + and on failure the uninitialized hostname buffer is read by + snprintf(), leaking stack memory via the rndc status reply. + :gl:`#5889` :gl:`!11879` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + ``c137dcd1a4`` + + `rndc-confgen -A hmac-sha384` and `-A hmac-sha512` documented a `-b` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` :gl:`!11903` + +- Validate key names in rndc-confgen, tsig-keygen, ddns-confgen. + ``b8e09a5b5f`` + + The three tools embedded the key-name argument verbatim into the + generated `named.conf` block, so a name containing characters like + `"`, `{`, or `;` produced output that did not match the intended `key` + clause. Key names are now restricted to letters, digits, dots, + hyphens, and underscores. :gl:`#5904` :gl:`!11904` + +- Do not follow symlinks when chowning the NZD database. ``ce77138b5c`` + + When `named` runs as root, the per-view NZD database file is chowned + to the user `named` drops to. The chown call followed symlinks, so a + symlink at the database path could redirect the ownership change to an + unrelated file. The chown now refuses non-regular files and never + follows symlinks. :gl:`#5905` :gl:`!11907` + +- Prevent crafted queries from degrading RRL performance. ``cf18479882`` + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` :gl:`!11950` + +- Fix swapped arguments in redirect2() single-label branch. + ``f5853e765f`` + + On a recursive resolver with nxdomain-redirect configured, an NXDOMAIN + result for a query whose qname is the root could corrupt the view's + nxdomain-redirect target, after which the redirect feature stopped + working for every subsequent query in that view until named was + restarted. :gl:`#5908` :gl:`!11908` + +- Avoid named assertion failure during parent-NS lookups when none + exist. ``90c7385000`` + + Configuring the root zone as a signed primary with parental agents (or + with notify-on-cds-changes) caused named to exit on an internal + assertion as soon as the DS-publication machinery tried to look up the + parent NS RRset — the root has no parent. The lookup is now + short-circuited cleanly. + + Similar, a zone with no NS records in the parent caused named to exit + in the same way. :gl:`#5910` :gl:`!11909` + +- Remove the rndc testgen command. ``28025ceff8`` + + testgen existed only to let the rndc system test generate large + response payloads. It accepted an unbounded count and was reachable + from read-only control channels, so any read-only rndc client could + drive named into memory exhaustion. The command and its supporting + test helper are gone; remaining rndc commands already produce + non-trivial responses, so transport coverage is preserved. :gl:`#5911` + :gl:`!11912` + +- Free per-command rndc state when response serialisation fails. + ``bf7ee390ba`` + + When isccc_cc_towire failed while building an rndc reply, + control_respond returned without releasing the per-command request, + response, HMAC secret copy, and text buffer. They were eventually + freed when the connection closed, but until then the HMAC key copy + stayed in named's memory. The failure path now goes through the same + cleanup label as every other error. :gl:`#5913` :gl:`!11915` + +- Handle KSR files with DNSKEY records before any header. ``55213079c6`` + + A DNSKEY record appearing before the first ';; KeySigningRequest' + header in a KSR file made dnssec-ksr abort on an internal assertion + instead of producing a structured error, killing pipelines that fed it + crafted or corrupted input. The tool now exits with a fatal error + naming the file and line. :gl:`#5914` :gl:`!11916` + +- Prevent rare named crash when notifies are cancelled. ``d079ca1b92`` + + Under heavy load, named could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + :gl:`!11918` + +- Stop delv from aborting on a malformed query name. ``ddf6239534`` + + delv aborts with SIGABRT instead of exiting cleanly when given a query + name that fails wire-format conversion (e.g. a label longer than 63 + octets). After this change delv prints the parse error and exits with + a normal failure code. :gl:`#5916` :gl:`!11921` + +- Fix dig -x crash on excessively long arguments. ``62ced63e22`` + + dig -x crashed with a segmentation fault rather than printing an error + when given an argument with thousands of dot-separated components. dig + -x now rejects such inputs cleanly with "Invalid IP address". + :gl:`#5917` :gl:`!11928` + +- Reject RSA DNSKEYs with degenerate modulus. ``d455794d0d`` + + A crafted DNSKEY rdata whose declared exponent length consumed the + whole buffer produced an RSA key with no modulus, which + dnssec-importkey accepted as valid and wrote to a .private file with + no key material. The wire-format parser now rejects RSA public keys + with a modulus smaller than 512 bits, the lowest legitimate size + across the RSA DNSSEC algorithms. :gl:`#5920` :gl:`!11929` + +- Reject negative and out-of-range TTLs in dnssec-* tools. + ``31818f6417`` + + The dnssec-* tools accepted negative and out-of-range values for TTL + flags such as dnssec-keygen -L, dnssec-signzone -t and dnssec-settime + -L, silently turning them into TTLs of around 136 years in the + resulting key or zone files. The flag values are now validated and + rejected with a clear "TTL must be non-negative" or "TTL out of range" + error. :gl:`#5923` :gl:`!11933` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + ``386177ec67`` + + When named was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause `named` to + crash. This has been fixed. :gl:`#5938` :gl:`!11948` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. ``774e08dee3`` + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid ``allow-query`` or + ``allow-transfer`` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + :gl:`!11954` + +- Fix a stack use-after-free in qpzone. ``82f67fc633`` + + In previous_closest_nsec(), a new qpreader was opened to search the + NSEC tree. It was possible for that to be used to update a QP iterator + object owned by the caller, and then be destroyed when the function + returned. + + This qpreader object isn't necessary anymore; since namespaces were + added to the QP trie in commit 15653c54a0, we can now just reuse the + existing reader for the main tree. :gl:`#5942` :gl:`!11955` + +- Fix a memory leak issue in the catalog zones. ``deb3694a63`` + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys using both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) at the + same time. This has been fixed. :gl:`#5943` :gl:`!11951` + +- Avoid extra round trips for DS lookups when the parent delegation is + already cached. ``07c8cddb4c`` + + DS queries could take two unnecessary extra round trips when the + resolver sent them to the child zone instead of the parent. The child + responds with NODATA, forcing a recovery path to rediscover the parent + delegation even though it was already cached. The resolver now + consults its delegation cache before starting DS fetches, sending + queries directly to the correct parent nameservers and eliminating the + extra latency. :gl:`!11835` + +- Enforce dns_adb_createaddrinfofind() invariant. ``bb330e533b`` + + ADB `dns_adb_createaddrinfofind()` expects `maxaddrs` paramaters is + always strictly positive. Add an assertion to enforce it. :gl:`!11819` + +- Fix a bug with template filename reuse. ``cf11b88e0e`` + + When a zone filename is defined in `named.conf` which will be written + to by the server - i.e., for secondary or dynamically updated zones - + there is a test at configuration time to ensure that the filename is + non-unique. + + This test is run before the zone is actually created, so a zone + configured using a template may not have had its filename expanded + yet. This can cause a configuration to fail because, for example, + multiple zones appear to using the filename `$name.db`. This has + been fixed by adding a new function `dns_zone_expandzonefile()` and + calling it during the uniqueness check. :gl:`!11769` + +- Fix suppressed missing-glue check in named-checkzone. ``e75f146485`` + + named-checkzone and named-checkconf -z silently skipped the + missing-glue check for any NS name that had already triggered an + extra-AAAA-glue warning, so zones missing required A glue could pass + validation and be deployed with broken delegations. :gl:`!11899` + +- Glues from different parent are rejected. ``48d098467f`` + + The changes making BIND 9 parent-centric !11621 introduced an issue + where it could be possible, when processing a referral, to use the + glue to a nameserver which has a different parent than the zonecut. + For instance: + + AUTHORITY test.example. NS ns.test.example. + test.example. NS ns.foo.example. test.example. + NS ns.bar. ADDITIONAL ns.bar. + A 1.2.3.4 ns.foo.example. A 5.6.7.8 + ns.test.example. A 9.8.7.6 + + In such situation, only the glues for `ns.foo.example.` and + `ns.test.example.` should be used, and the glue from `ns.bar.` must be + ignored as this is not either a sub-domain or a sibling domain, the + parent is different (`bar.` instead of `example.`). This is now fixed. + + Sibling glue and cyclic sibling glues are defined in RFC 9471 section + 2.2 and section 2.3. :gl:`!11873` + +- Harden dig's EDNS option parsing against malformed replies. + ``7b87ab0236`` + + dig's parser for EDNS options in a DNS reply now stops cleanly when an + option declares a length that runs past the end of the option data, + rather than trusting the upstream OPT-record validator to reject the + reply first. This is a defensive change; behavior is unchanged in + practice. :gl:`!11937` + +- Implement seamless outgoing TCP connection reuse. ``a61427e8ee`` + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by RFC 7766. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into TIME_WAIT state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. :gl:`!11845` + +- Include according by checking in meson. ``f27aba4d7d`` + + The header has existed in macOS since around ~26. This + causes the `htobeNN`/`htoleNN` macros to be redefined in + in terms of when other system + headers include . + + Fix this issue by using checking for the existence of + in meson and including it according to the probe result. :gl:`!11751` + +- Pass empty string instead of NULL to ns_client_dumpmessage() + ``a0084190b4`` + + Pass "" instead of NULL to ns_client_dumpmessage() to get the log + message printed. + +- Possible crash when a resolver validate a static-stub zone. + ``b8dcabbd72`` + + A NULL pointer dereference could be made in some circumstances when + resolving and validating a name under a `static-stub` zone. This is + now fixed. :gl:`!11788` + +- Prevent excessive priming queries to the root servers. ``5f8624df76`` + + BIND was sending a priming query to the root servers on nearly every + recursive lookup instead of only when the cached root information + expired. Priming now rearms only after the TTL of the fetched records + elapses, and the refreshed root NS set is used for query routing until + the next cycle. :gl:`!11847` + +- Reject record sets too large to serve in DNS. ``a925af7ce6`` + + When BIND was asked to store a record set whose total size exceeds + what fits in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. :gl:`!11963` + +- Remove deadcode in `query_addbestns()` ``43c58dafcc`` + + The local variable `zfname` was released in the cleanup part of the + function if not NULL, but it turns out it is now always NULL at that + point. + + The flow can get to that part only in two cases: either `zfname` is + not NULL, and then it's ownership is moved to a different variable + (thus, it is now NULL), or `zfname` is already NULL. + + Removing the bit of deadcode releasing it. :gl:`!11790` + +- Remove unneeded options in dns_zonefetch. ``a43a6cfba4`` + + In the `dns_zonefetch` mechanism, some option flags for + `dns_resolver_createfetch()` were used for all fetches, but were + actually only needed by the `DNSKEY` refresh fetches. + + (Specifially, these options were `DNS_FETCHOPT_UNSHARED` and + `DNS_FETCHOPT_NOCACHED`, which were used along with + `DNS_FETCHOPT_NOVALIDATE` to ensure we get a new copy of the DNSKEY as + it is currently published by the authority, without prior validation. + Those conditions are needed for RFC 5011 trust anchor maintenace, but + not when looking up parent-`NS` or `DSYNC` RRsets.) :gl:`!11866` + +- Stop rndc-confgen from following symlinks when writing the keyfile. + ``468b09feb2`` + + When rndc-confgen -a (re)created the rndc control key, it followed a + symbolic link if one happened to exist at the keyfile path: the + existence check looked through the link, then the file was truncated, + its ownership changed, and the key contents written into whatever file + the link pointed at. rndc-confgen now refuses to follow symbolic links + at the keyfile path and fails with an error instead, so the wrong file + can no longer be overwritten by accident. :gl:`!11902` + +- Validate -l and -L numeric arguments in named-checkzone. + ``1064d11af2`` + + named-checkzone and named-compilezone parsed the -l (max TTL) and -L + (source serial) arguments with strtol(), so a negative value such as + -1 silently became UINT32_MAX and out-of-range values were truncated + to 32 bits without warning; -l in particular appeared to cap TTLs but + no longer enforced anything. Both flags now go through + isc_parse_uint32() and reject any value that is not a valid 32-bit + unsigned integer. :gl:`!11900` + + diff --git a/doc/notes/notes-9.21.22.rst b/doc/notes/notes-9.21.22.rst new file mode 100644 index 0000000000..048207b073 --- /dev/null +++ b/doc/notes/notes-9.21.22.rst @@ -0,0 +1,402 @@ +.. 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. + +Notes for BIND 9.21.22 +---------------------- + +Security Fixes +~~~~~~~~~~~~~~ + +- Limit resolver server list size. :cve:`2026-3592` + + When resolving a domain with many nameservers that shared overlapping + IP addresses (e.g., 10 NS records all pointing at the same set of + addresses), BIND could previously waste time querying duplicate + addresses and build up excessively large server lists. Addresses in + the resolver's server list are now deduplicated so that each unique IP is only + queried once per resolution attempt, regardless of how many NS records + point to it. The number of addresses stored per nameserver name + is also now capped at six (combined A and AAAA), preventing memory and CPU overhead from + domains with unusually large NS/glue sets. + + ISC would like to thank Shuhan Zhang from Tsinghua University for + reporting this issue. :gl:`#5641` + +- Fix GSS-API resource leak. :cve:`2026-3039` + + A memory leak was fixed where each GSS-API TKEY negotiation leaked a + security context inside the GSS library. An unauthenticated attacker + could exhaust server memory by sending repeated TKEY queries to a + server with :any:`tkey-gssapi-keytab` configured. The leaked memory was + allocated by the GSS library, bypassing BIND's memory accounting. + + Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is now + rejected, as BIND never supported it correctly and Kerberos/SPNEGO + completes in a single round. + + ISC would like to thank Vitaly Simonovich for bringing this + vulnerability to our attention. :gl:`#5752` + +- Disable recursion, UPDATE, and NOTIFY for non-IN views. + :cve:`2026-5946` + + Recursion, dynamic updates (UPDATE), and zone change notifications + (NOTIFY) are now disabled for views with a class other than IN (such + as CHAOS or HESIOD); authoritative service for non-IN zones (e.g. + version.bind in class CHAOS) continues to work as before. Servers + configured with :namedconf:ref:`recursion yes; ` + in a non-IN view log a warning at + startup, and :iscman:`named-checkconf` flags the same condition. UPDATE and + NOTIFY messages that specify the meta-classes ANY or NONE in the + question section are now rejected with FORMERR. + + This addresses a set of closely related security issues collectively + identified as CVE-2026-5946. ISC would like to thank Mcsky23 for + bringing these issues to our attention. :gl:`#5784` + +- Avoid unbounded recursion loop. :cve:`2026-5950` + + A bug during bad server handling could cause the resolver to enter an + infinite loop, continuously sending queries to an upstream server with + no exit condition, until the resolver query timeout was hit. This has + been fixed. + + ISC would like to thank Billy Baraja (BielraX) for bringing this issue + to our attention. :gl:`#5804` + +- Fix crash in resolver when SIG(0)-signed responses are received under + load. :cve:`2026-5947` + + A resolver could crash when handling a SIG(0)-signed response if the + matching client query was cancelled while signature verification was + still in progress — for example, when the recursive-clients quota was + exhausted. This has been fixed. + + ISC would like to thank Naoki Wakamatsu for bringing this + vulnerability to our attention. :gl:`#5819` + +- Fix use-after-free error in DNS-over-HTTPS when processing HTTP/2 + SETTINGS frames. :cve:`2026-3593` + + Previously, a use-after-free vulnerability in the DNS-over-HTTPS implementation + could cause :iscman:`named` to crash when a client sent a flood of HTTP/2 + SETTINGS frames while a DoH response was being written. This affected + servers with DoH (DNS-over-HTTPS) enabled and has been fixed. + + ISC would like to thank Naresh Kandula Parmar (Nottiboy) for reporting + this. :gl:`#5755` + +- Fix outgoing zone transfers' quota issue. + + Unauthorized clients could consume the entire outgoing zone-transfer quota and + block authorized zone transfer clients. This has been fixed. + :gl:`#3589` + + +Feature Changes +~~~~~~~~~~~~~~~ + +- Fix CPU spikes and slow queries when cache approaches memory limit. + + Cache cleanup is now spread probabilistically to avoid CPU usage spikes and a + drop in query throughput. :gl:`#5891` + +- Implement :rfc:`3645` Section 4.1.1 key expiry check in TKEY. + + BIND now checks for existing TSIG keys before accepting a new GSS-API + negotiation, and deletes the key if it has expired. Previously, an + expired GSS key would permanently block re-negotiation for that name + until the server was restarted. :gl:`!11713` + +- Reduce memory footprint by actively returning unused memory to the OS. + + Previously, :iscman:`named` relied on the default allocator settings + for releasing unused memory back to the operating system, which could + result in unnecessarily high resident memory usage. :iscman:`named` + now actively manages memory page purging. On systems using jemalloc, + background cleanup threads are enabled and the dirty page decay time + is reduced from 10 seconds to 5 seconds. Additionally, a volume-based + decay pass is triggered after every 16 MiB of freed memory. On + glibc-based systems, a similar volume-based mechanism using + ``malloc_trim()`` is used instead. :gl:`!11761` + +Bug Fixes +~~~~~~~~~ + +- Use the zone file's basename as origin in DNSSEC tools. + + In :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`, when the zone origin is not + specified using the ``-o`` parameter, the default behavior is to try to + sign using the zone's file name as the origin. So, for example, + ``dnssec-signzone -S example.com`` will work, so long as the file name + matches the zone name. + + This now also works if the zone is in a different directory. For + example, ``dnssec-signzone -S zones/example.com`` will set the origin + value to ``example.com``. :gl:`#5678` + +- Fix a possible race condition during zone transfers. + + The :iscman:`named` process could terminate unexpectedly when + processing an IXFR message during a zone transfer. This has been + fixed. :gl:`#5767` + +- Do not resend query after ``BADCOOKIE`` answer on TCP. + + When an upstream server answered ``BADCOOKIE``, no matter which transport + was used, the resolver resent the query using TCP. However, if the + upstream server responded with ``BADCOOKIE`` again over TCP, the resolver + would keep resending until the maximum query count was reached. + + This is now fixed by no longer resending once the query has already + been sent over TCP. :gl:`#5804` + +- Fix :iscman:`named` crash when processing SIG records in dynamic updates. + + Previously, :iscman:`named` could abort if a client sent a dynamic + update containing a SIG record (the legacy signature type) to a zone + configured with an update-policy. The function `dns_db_findrdataset` + had an incorrect requirements prerequisite that prevented SIG records + from being looked up, which was triggered as part of processing an UPDATE + request and could be triggered remotely by any client permitted to + send updates. This has been fixed by ensuring that SIG records are + handled consistently with RRSIG records during update processing. + :gl:`#5818` + +- Fix wrong NSEC proof for empty non-terminals after IXFR. + + When a secondary received an IXFR that transitioned a zone from + unsigned to NSEC-signed, queries for empty non-terminal names returned + the zone apex NSEC record instead of the NSEC that actually covered the + queried name. The issue only occurred with incremental transfers; a + full AXFR or a server restart resolved it. :gl:`#5824` + +- Fix :option:`rndc modzone` behavior for a zone in named.conf. + + If a zone was present in the configuration file and not originally + added by :option:`rndc addzone`, :option:`rndc modzone` for that zone would succeed + once but subsequent :option:`rndc modzone` attempts would fail. This has been + fixed. :gl:`#5826` + +- Fix zone verification of NSEC3 signed zones. + + Previously, when computing the compressed bitmap during verification + of an NSEC3-signed zone, an undersized buffer was used that resulted + in an out-of-bounds write if there were too many active windows in the + bitmap. This impacted the mirror zones which are NSEC3-signed, + :iscman:`dnssec-signzone` and :iscman:`dnssec-verify`. This has been fixed. + :gl:`#5834` + +- Fix :option:`rndc modzone` issue with non-existing zones. + + The :iscman:`named` process could terminate unexpectedly or become + subject to undefined behavior when issued an :option:`rndc modzone` + operation for a non-existing zone. This has been fixed. :gl:`#5848` + +- Fix zone filename token-parsing bug. + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog member zone containing special characters like + "%" or "$", which could be interpreted as zone filename tokens and + trigger a case-sensitivity bug in the token-parsing code. This has + been fixed. :gl:`#5849` + +- Prevent a crash when using both :any:`dns64` and :any:`filter-aaaa`. + + An assertion failure could be triggered if both :any:`dns64` and the + :any:`filter-aaaa` plugin were in use simultaneously. This happened if the + plugin triggered a second recursion process, which then attempted to + store DNS64 state information in a pointer that had already been set + by the original recursion process. This has been fixed. :gl:`#5854` + +- Fixed an assertion failure when processing catalog zones. + + If a TXT record containing an invalid name TSIG key name was found + when processing a catalog zone member's primaries definition, + ``dns_name_free`` was incorrectly called, triggering an assertion. This has + been fixed. :gl:`#5858` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + + A DNSSEC-signed zone could publish a DNSKEY with an unusually large + RSA public exponent and force any validator resolving names in that + zone to spend disproportionate CPU verifying signatures. The + validator now rejects such DNSKEYs, matching the limit already applied + to keys read from files or HSMs. :gl:`#5881` + +- Fix :iscman:`rndc-confgen` aborting on HMAC-SHA-384/512 keys above 512 bits. + + :iscman:`rndc-confgen` (with either ``-A hmac-sha384`` or + ``-A hmac-sha512``) previously documented a ``-b`` + range of 1..1024, but any value above 512 aborted on hardened builds + instead of producing a key. The full advertised range now works. + :gl:`#5903` + +- Validate key names in :iscman:`rndc-confgen`, :iscman:`tsig-keygen`, and :iscman:`ddns-confgen`. + + The three tools embedded the key-name argument verbatim into the + generated `named.conf` block, so a name containing characters like + `"`, `{`, or `;` produced output that did not match the intended `key` + clause. Key names are now restricted to letters, digits, dots, + hyphens, and underscores. :gl:`#5904` + +- Prevent crafted queries from degrading RRL performance. + + With response rate limiting enabled, an attacker sending queries from + many spoofed source addresses could steer entries into the same slot + of the internal rate-limit table and slow down query processing on the + affected server. The table now uses a per-process keyed hash so the + placement of entries cannot be predicted or influenced from the + network. :gl:`#5906` + +- Prevent rare :iscman:`named` crash when notifies are cancelled. + + Under heavy load, :iscman:`named` could occasionally crash when a queued + outbound notify or zone refresh was cancelled at the moment it was + being sent — for example, while a zone was being reloaded or removed. + The race that caused the crash is now prevented. :gl:`#5915` + +- Stop :iscman:`delv` from aborting on a malformed query name. + + :iscman:`delv` previously aborted with SIGABRT instead of exiting cleanly when given a query + name that failed wire-format conversion (e.g. a label longer than 63 + octets). After this change :iscman:`delv` prints the parse error and exits with + a normal failure code. :gl:`#5916` + +- Fix :option:`dig -x` crash on excessively long arguments. + + Previously, :option:`dig -x` crashed with a segmentation fault rather than printing + an error when given an argument with thousands of dot-separated components. + :option:`dig -x` now rejects such inputs cleanly with "Invalid IP address". + :gl:`#5917` + +- Reject negative and out-of-range TTLs in ``dnssec-*`` tools. + + The ``dnssec-*`` tools previously accepted negative and out-of-range values for TTL + flags such as :option:`dnssec-keygen -L`, :option:`dnssec-signzone -t` + and :option:`dnssec-settime -L`, + silently turning them into TTLs of around 136 years in the + resulting key or zone files. The flag values are now validated and + rejected with a clear "TTL must be non-negative" or "TTL out of range" + error. :gl:`#5923` + +- Fix a crash when reconfiguring while an NTA is being rechecked. + + Previously, if :iscman:`named` was reconfigured or shut down while a negative trust anchor + was being rechecked against authoritative servers, the in-flight + recheck could outlive the view that owned it and cause :iscman:`named` to + crash. This has been fixed. :gl:`#5938` + +- Fix a bug in :any:`allow-query`/:any:`allow-transfer` catalog zone custom + properties. + + The :iscman:`named` process could terminate unexpectedly when + processing a catalog zone with an invalid :any:`allow-query` or + :any:`allow-transfer` custom property (i.e. having a non-APL type) + coexisting with the valid property. This has been fixed. :gl:`#5941` + +- Fix a memory leak issue in catalog zones. + + The :iscman:`named` process could leak small amounts of memory when + processing a catalog zone entry which had defined custom primary + servers with TSIG keys, if both the regular ``primaries`` custom + property syntax and the legacy alternative syntax (``masters``) were used at the + same time. This has been fixed. :gl:`#5943` + +- Avoid extra round trips for DS lookups when the parent delegation is + already cached. + + Previously, DS queries could take two unnecessary extra round trips when the + resolver sent them to the child zone instead of the parent. The child + would respond with NODATA, forcing a recovery path to rediscover the parent + delegation even though it was already cached. The resolver now + consults its delegation cache before starting DS fetches, sending + queries directly to the correct parent nameservers and eliminating the + extra latency. :gl:`!11835` + +- Fix suppressed missing-glue check in :iscman:`named-checkzone`. + + :iscman:`named-checkzone` and :option:`named-checkconf -z` silently + skipped the missing-glue check for any NS name that had already + triggered an extra-AAAA-glue warning, so zones missing required A glue + could pass validation and be deployed with broken delegations. + :gl:`!11899` + +- Reject glues from different parents. + + The changes that made BIND 9 parent-centric (!11621) introduced an issue + where it could be possible, when processing a referral, to use the + glue to direct queries to a nameserver which had a different parent than the zonecut. + For instance::: + + AUTHORITY + test.example. NS ns.test.example. + test.example. NS ns.foo.example. + test.example. NS ns.bar. + + ADDITIONAL + ns.bar. A 1.2.3.4 + ns.foo.example. A 5.6.7.8 + ns.test.example. A 9.8.7.6 + + In such a situation, only the glues for ``ns.foo.example.`` and + ``ns.test.example.`` should have been used; the glue from ``ns.bar.`` must be + ignored as this is neither a sub-domain nor a sibling domain, and the + parent is different (``bar.`` instead of ``example.``). This is now fixed. + + Sibling glue and cyclic sibling glues are defined in :rfc:`9471` section + 2.2 and section 2.3. :gl:`!11873` + +- Implement seamless outgoing TCP connection reuse. + + The resolver can and will reuse outgoing TCP connections to the same + host, as recommended by :rfc:`7766`. This prevents a whole class of + attacks that abuse the fact that establishing a TCP connection is + expensive and it is fairly easy to deplete the outgoing TCP ports by + putting them into ``TIME_WAIT`` state. + + The number of pipelined queries per connection is capped at 256 to + limit the impact of a connection drop. :gl:`!11845` + +- Prevent possible crash when a resolver validates a static-stub zone. + + A NULL pointer dereference could be made in some circumstances when + resolving and validating a name under a `static-stub` zone. This is + now fixed. :gl:`!11788` + +- Prevent excessive priming queries to the root servers. + + BIND was sending a priming query to the root servers on nearly every + recursive lookup instead of only when the cached root information + expired. Priming now rearms only after the TTL of the fetched records + elapses, and the refreshed root NS set is used for query routing until + the next cycle. :gl:`!11847` + +- Reject record sets too large to serve in DNS. + + When BIND was asked to store a record set whose total size exceeded + what fit in a DNS message, it would allocate memory and build the + structure, then fail later at response time. Such oversized record + sets are now rejected at the time of storage with an error, avoiding + wasted work on data that can never be served. :gl:`!11963` + +- Stop :iscman:`rndc-confgen` from following symlinks when writing the + keyfile. + + When :option:`rndc-confgen -a` (re)created the rndc control key, it + followed a symbolic link if one happened to exist at the keyfile path; + the existence check looked through the link, then the file was truncated, + its ownership changed, and the key contents written into whatever file + the link pointed at. :iscman:`rndc-confgen` now refuses to follow symbolic + links at the keyfile path and fails with an error instead, so the wrong + file can no longer be overwritten by accident. :gl:`!11902` + + diff --git a/lib/dns/adb.c b/lib/dns/adb.c index f189664f80..758f0d8187 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -566,6 +566,9 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdtype = rdataset->type; + REQUIRE(rdataset->rdclass == dns_rdataclass_in); + REQUIRE(dns_rdatatype_isaddr(rdtype)); + switch (rdataset->trust) { case dns_trust_glue: case dns_trust_additional: @@ -584,8 +587,6 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdataset->ttl = ttlclamp(rdataset->ttl); } - REQUIRE(dns_rdatatype_isaddr(rdtype)); - DNS_RDATASET_FOREACH(rdataset) { /* FIXME: Move to a separate function */ dns_adbnamehooklist_t *hookhead = NULL; @@ -1370,8 +1371,10 @@ log_quota(dns_adbentry_t *entry, const char *fmt, ...) { } static void -copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { +copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name, + size_t maxfindlen, size_t *findlen) { dns_adbentry_t *entry = NULL; + size_t count = 0; if ((find->options & DNS_ADBFIND_INET) != 0) { ISC_LIST_FOREACH(name->v4, namehook, name_link) { @@ -1391,6 +1394,12 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { * Found a valid entry. Add it to the find's list. */ ISC_LIST_APPEND(find->list, addrinfo, publink); + + count++; + if (maxfindlen - count == 0) { + SET_IF_NOT_NULL(findlen, count); + return; + } } } @@ -1412,8 +1421,16 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbname_t *name) { * Found a valid entry. Add it to the find's list. */ ISC_LIST_APPEND(find->list, addrinfo, publink); + + count++; + if (maxfindlen - count == 0) { + SET_IF_NOT_NULL(findlen, count); + return; + } } } + + SET_IF_NOT_NULL(findlen, count); } static bool @@ -1735,7 +1752,7 @@ out: void dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, in_port_t port, unsigned int options, - isc_stdtime_t now, size_t maxaddrs, + isc_stdtime_t now, size_t maxfindlen, dns_adbfind_t **findp, size_t *findlen) { dns_adbfind_t *find = NULL; isc_sockaddr_t sockaddr = {}; @@ -1743,7 +1760,7 @@ dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, REQUIRE(DNS_ADB_VALID(adb)); REQUIRE(addrs != NULL); REQUIRE(findp != NULL && *findp == NULL); - REQUIRE(maxaddrs > 0); + REQUIRE(maxfindlen > 0); rcu_read_lock(); @@ -1794,7 +1811,7 @@ dns_adb_createaddrinfosfind(dns_adb_t *adb, isc_netaddrlist_t *addrs, ISC_LIST_APPEND(find->list, addrinfo, publink); (*findlen)++; - if (maxaddrs - *findlen == 0) { + if (maxfindlen - *findlen == 0) { break; } } @@ -1825,7 +1842,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, const dns_name_t *name, unsigned int options, isc_stdtime_t now, in_port_t port, unsigned int depth, isc_counter_t *qc, isc_counter_t *gqc, fetchctx_t *parent, - dns_adbfind_t **findp) { + size_t maxfindlen, dns_adbfind_t **findp, size_t *findlen) { isc_result_t result = ISC_R_UNEXPECTED; dns_adbfind_t *find = NULL; dns_adbname_t *adbname = NULL; @@ -1844,6 +1861,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, } REQUIRE(name != NULL); REQUIRE(findp != NULL && *findp == NULL); + REQUIRE(maxfindlen > 0); REQUIRE((options & DNS_ADBFIND_ADDRESSMASK) != 0); @@ -2056,7 +2074,7 @@ fetch: * Run through the name and copy out the bits we are * interested in. */ - copy_namehook_lists(adb, find, adbname); + copy_namehook_lists(adb, find, adbname, maxfindlen, findlen); post_copy: if (NAME_FETCH_A(adbname)) { diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index b500e94cda..9e025b1a3b 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -296,7 +296,7 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, isc_mem_t *mctx, char **err_message) { isc_region_t r; isc_buffer_t namebuf; - gss_name_t gname; + gss_name_t gname = NULL; OM_uint32 gret, minor, ret_flags, flags; gss_buffer_desc gintoken, *gintokenp, gouttoken = GSS_C_EMPTY_BUFFER; isc_result_t result; @@ -336,7 +336,14 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, GSS_SPNEGO_MECHANISM, flags, 0, NULL, gintokenp, NULL, &gouttoken, &ret_flags, NULL); - if (gret != GSS_S_COMPLETE && gret != GSS_S_CONTINUE_NEEDED) { + switch (gret) { + case GSS_S_COMPLETE: + result = ISC_R_SUCCESS; + break; + case GSS_S_CONTINUE_NEEDED: + result = DNS_R_CONTINUE; + break; + default: gss_err_message(mctx, gret, minor, err_message); if (err_message != NULL && *err_message != NULL) { gss_log(3, "Failure initiating security context: %s", @@ -349,9 +356,22 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, } /* - * XXXSRA Not handled yet: RFC 3645 3.1.1: check ret_flags - * MUTUAL and INTEG flags, fail if either not set. + * RFC 3645 Section 3.1.1: verify that replay detection, mutual + * authentication and integrity are supported. The RFC mandates + * checking replay_det_state and mutual_state; integ_avail is + * also verified because GSS-TSIG cannot function without it. */ + if (gret == GSS_S_COMPLETE && + (ret_flags & + (GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) != + (GSS_C_REPLAY_FLAG | GSS_C_MUTUAL_FLAG | GSS_C_INTEG_FLAG)) + { + gss_log(3, + "GSS-API context lacks required REPLAY, MUTUAL, " + "or INTEG flags (ret_flags=0x%x)", + (unsigned int)ret_flags); + CLEANUP(ISC_R_FAILURE); + } /* * RFC 2744 states the a valid output token has a non-zero length. @@ -361,23 +381,19 @@ dst_gssapi_initctx(const dns_name_t *name, isc_buffer_t *intoken, CHECK(isc_buffer_copyregion(outtoken, &r)); } - if (gret == GSS_S_COMPLETE) { - result = ISC_R_SUCCESS; - } else { - result = DNS_R_CONTINUE; - } - cleanup: if (gouttoken.length != 0U) { (void)gss_release_buffer(&minor, &gouttoken); } - (void)gss_release_name(&minor, &gname); + if (gname != NULL) { + (void)gss_release_name(&minor, &gname); + } return result; } isc_result_t dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, - isc_buffer_t **outtoken, dns_gss_ctx_id_t *ctxout, + isc_buffer_t **outtokenp, dns_gss_ctx_id_t *ctxout, dns_name_t *principal, isc_mem_t *mctx) { isc_region_t r; isc_buffer_t namebuf; @@ -389,16 +405,11 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, isc_result_t result; char buf[1024]; - REQUIRE(outtoken != NULL && *outtoken == NULL); + REQUIRE(outtokenp != NULL && *outtokenp == NULL); + REQUIRE(*ctxout == NULL); REGION_TO_GBUFFER(*intoken, gintoken); - if (*ctxout == NULL) { - context = GSS_C_NO_CONTEXT; - } else { - context = *ctxout; - } - if (gssapi_keytab != NULL) { #if HAVE_GSSAPI_GSSAPI_KRB5_H || HAVE_GSSAPI_KRB5_H gret = gsskrb5_register_acceptor_identity(gssapi_keytab); @@ -433,17 +444,39 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, #endif } + OM_uint32 ret_flags = 0; + gret = gss_accept_sec_context(&minor, &context, GSS_C_NO_CREDENTIAL, &gintoken, GSS_C_NO_CHANNEL_BINDINGS, - &gname, NULL, &gouttoken, NULL, NULL, - NULL); + &gname, NULL, &gouttoken, &ret_flags, + NULL, NULL); result = ISC_R_FAILURE; switch (gret) { case GSS_S_COMPLETE: - case GSS_S_CONTINUE_NEEDED: + /* + * RFC 2743 Section 1.2.2: verify that the negotiated + * context provides integrity protection. + */ + if ((ret_flags & GSS_C_INTEG_FLAG) == 0) { + gss_log(3, + "GSS-API context lacks required INTEG " + "flag (ret_flags=0x%x)", + (unsigned int)ret_flags); + (void)gss_delete_sec_context(&minor, &context, NULL); + result = DNS_R_INVALIDTKEY; + goto cleanup; + } break; + /* + * RFC 3645 4.1.3: we don't handle GSS_S_CONTINUE_NEEDED + * Multi-round GSS-API negotiation is not supported. + */ + case GSS_S_CONTINUE_NEEDED: + gss_log(3, "multi-round GSS-API negotiation not supported"); + (void)gss_delete_sec_context(&minor, &context, NULL); + FALLTHROUGH; case GSS_S_DEFECTIVE_TOKEN: case GSS_S_DEFECTIVE_CREDENTIAL: case GSS_S_BAD_SIG: @@ -456,7 +489,7 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, case GSS_S_BAD_MECH: case GSS_S_FAILURE: result = DNS_R_INVALIDTKEY; - /* fall through */ + FALLTHROUGH; default: gss_log(3, "failed gss_accept_sec_context: %s", gss_error_tostring(gret, minor, buf, sizeof(buf))); @@ -467,49 +500,54 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, } if (gouttoken.length > 0U) { - isc_buffer_allocate(mctx, outtoken, + isc_buffer_allocate(mctx, outtokenp, (unsigned int)gouttoken.length); GBUFFER_TO_REGION(gouttoken, r); - CHECK(isc_buffer_copyregion(*outtoken, &r)); + CHECK(isc_buffer_copyregion(*outtokenp, &r)); (void)gss_release_buffer(&minor, &gouttoken); } - if (gret == GSS_S_COMPLETE) { - gret = gss_display_name(&minor, gname, &gnamebuf, NULL); - if (gret != GSS_S_COMPLETE) { - gss_log(3, "failed gss_display_name: %s", - gss_error_tostring(gret, minor, buf, - sizeof(buf))); - CLEANUP(ISC_R_FAILURE); - } + INSIST(gret == GSS_S_COMPLETE); - /* - * Compensate for a bug in Solaris8's implementation - * of gss_display_name(). Should be harmless in any - * case, since principal names really should not - * contain null characters. - */ - if (gnamebuf.length > 0U && - ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') - { - gnamebuf.length--; - } - - gss_log(3, "gss-api source name (accept) is %.*s", - (int)gnamebuf.length, (char *)gnamebuf.value); - - GBUFFER_TO_REGION(gnamebuf, r); - isc_buffer_init(&namebuf, r.base, r.length); - isc_buffer_add(&namebuf, r.length); - - CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0)); - } else { - result = DNS_R_CONTINUE; + gret = gss_display_name(&minor, gname, &gnamebuf, NULL); + if (gret != GSS_S_COMPLETE) { + gss_log(3, "failed gss_display_name: %s", + gss_error_tostring(gret, minor, buf, sizeof(buf))); + CLEANUP(ISC_R_FAILURE); } + /* + * Compensate for a bug in Solaris8's implementation + * of gss_display_name(). Should be harmless in any + * case, since principal names really should not + * contain null characters. + */ + if (gnamebuf.length > 0U && + ((char *)gnamebuf.value)[gnamebuf.length - 1] == '\0') + { + gnamebuf.length--; + } + + gss_log(3, "gss-api source name (accept) is %.*s", (int)gnamebuf.length, + (char *)gnamebuf.value); + + GBUFFER_TO_REGION(gnamebuf, r); + isc_buffer_init(&namebuf, r.base, r.length); + isc_buffer_add(&namebuf, r.length); + + CHECK(dns_name_fromtext(principal, &namebuf, dns_rootname, 0)); + *ctxout = context; cleanup: + if (result != ISC_R_SUCCESS && *outtokenp != NULL) { + isc_buffer_free(outtokenp); + } + + if (result != ISC_R_SUCCESS && context != GSS_C_NO_CONTEXT) { + (void)gss_delete_sec_context(&minor, &context, NULL); + } + if (gnamebuf.length != 0U) { gret = gss_release_buffer(&minor, &gnamebuf); if (gret != GSS_S_COMPLETE) { diff --git a/lib/dns/include/dns/adb.h b/lib/dns/include/dns/adb.h index 7c1aef4cc5..82abbf98f5 100644 --- a/lib/dns/include/dns/adb.h +++ b/lib/dns/include/dns/adb.h @@ -285,7 +285,7 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, const dns_name_t *name, unsigned int options, isc_stdtime_t now, in_port_t port, unsigned int depth, isc_counter_t *qc, isc_counter_t *gqc, fetchctx_t *parent, - dns_adbfind_t **find); + size_t maxfindlen, dns_adbfind_t **find, size_t *findlen); /*%< * Main interface for clients. The adb will look up the name given in * "name" and will build up a list of found addresses, and perhaps start @@ -333,6 +333,8 @@ dns_adb_createfind(dns_adb_t *adb, isc_loop_t *loop, isc_job_cb cb, void *cbarg, * *\li find != NULL && *find == NULL. * + *\li findlen is optional, if not NULL, it will be set to the length of find. + * * Returns: * *\li #ISC_R_SUCCESS Addresses might have been returned, and events will be diff --git a/lib/dns/include/dst/gssapi.h b/lib/dns/include/dst/gssapi.h index a519487da0..6551e014ce 100644 --- a/lib/dns/include/dst/gssapi.h +++ b/lib/dns/include/dst/gssapi.h @@ -71,18 +71,17 @@ dst_gssapi_acceptctx(const char *gssapi_keytab, isc_region_t *intoken, * generated by gss_accept_sec_context() to be sent to the * initiator * 'context' is a valid pointer to receive the generated context handle. - * On the initial call, it should be a pointer to NULL, which - * will be allocated as a dns_gss_ctx_id_t. Subsequent calls - * should pass in the handle generated on the first call. * * Requires: - * 'outtoken' to != NULL && *outtoken == NULL. + * 'outtoken' != NULL && *outtoken == NULL. + * 'context' != NULL && *context == NULL. * * Returns: - * ISC_R_SUCCESS msg was successfully updated to include the - * query to be sent - * DNS_R_CONTINUE transaction still in progress - * other an error occurred while building the message + * ISC_R_SUCCESS msg was successfully updated to include + * the query to be sent + * DNS_R_INVALIDTKEY an error occurred while accepting the + * context + * ISC_R_FAILURE other error occurred */ isc_result_t diff --git a/lib/dns/message.c b/lib/dns/message.c index 98f57d1c79..cfc489f98d 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -972,6 +972,17 @@ getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, rdtype = isc_buffer_getuint16(source); rdclass = isc_buffer_getuint16(source); + /* + * Notify and update messages need to specify the data class. + */ + if ((msg->opcode == dns_opcode_update || + msg->opcode == dns_opcode_notify) && + (rdclass == dns_rdataclass_none || + rdclass == dns_rdataclass_any)) + { + DO_ERROR(DNS_R_FORMERR); + } + /* * If this class is different than the one we already read, * this is an error. diff --git a/lib/dns/notify.c b/lib/dns/notify.c index 46766ea711..3b2ec98f5d 100644 --- a/lib/dns/notify.c +++ b/lib/dns/notify.c @@ -761,9 +761,10 @@ dns_notify_find_address(dns_notify_t *notify) { goto destroy; } - result = dns_adb_createfind(adb, loop, process_notify_adb_event, notify, - ¬ify->ns, options, 0, notify->port, 0, - NULL, NULL, NULL, ¬ify->find); + result = dns_adb_createfind( + adb, loop, process_notify_adb_event, notify, ¬ify->ns, + options, 0, notify->port, 0, NULL, NULL, NULL, + view->max_delegation_servers, ¬ify->find, NULL); dns_adb_detach(&adb); /* Something failed? */ diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index a8d15b57ae..db8c663f11 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -376,7 +376,16 @@ struct fetchctx { dns_message_t *qmessage; ISC_LIST(resquery_t) queries; dns_adbfindlist_t finds; - dns_adbfind_t *find; + /* + * This is a state to keep track of the latest upstream server which is + * being queried. See `nextaddress()`. + * + * `addrinfo` is basically a copy of `foundaddrinfo` but came from the + * response of the query, so fields like the SRTT/timing might have been + * altered. So it might be possible (?) to wrap those two in an union + * for clarity (and memory saving). + */ + dns_adbaddrinfo_t *foundaddrinfo; /* * altfinds are names and/or addresses of dual stack servers that * should be used when iterative resolution to a server is not @@ -1417,7 +1426,7 @@ fctx_cleanup(fetchctx_t *fctx) { dns_adb_destroyfind(&find); fetchctx_unref(fctx); } - fctx->find = NULL; + fctx->foundaddrinfo = NULL; ISC_LIST_FOREACH(fctx->altfinds, find, publink) { ISC_LIST_UNLINK(fctx->altfinds, find, publink); @@ -3264,89 +3273,6 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo, classbuf, addrbuf); } -/* - * Sort addrinfo list by RTT. - */ -static void -sort_adbfind(dns_adbfind_t *find, unsigned int bias) { - dns_adbaddrinfo_t *best, *curr; - dns_adbaddrinfolist_t sorted; - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(find->list)) { - unsigned int best_srtt; - best = ISC_LIST_HEAD(find->list); - best_srtt = best->srtt; - if (isc_sockaddr_pf(&best->sockaddr) != AF_INET6) { - best_srtt += bias; - } - curr = ISC_LIST_NEXT(best, publink); - while (curr != NULL) { - unsigned int curr_srtt = curr->srtt; - if (isc_sockaddr_pf(&curr->sockaddr) != AF_INET6) { - curr_srtt += bias; - } - if (curr_srtt < best_srtt) { - best = curr; - best_srtt = curr_srtt; - } - curr = ISC_LIST_NEXT(curr, publink); - } - ISC_LIST_UNLINK(find->list, best, publink); - ISC_LIST_APPEND(sorted, best, publink); - } - find->list = sorted; -} - -/* - * Sort a list of finds by server RTT. - */ -static void -sort_finds(dns_adbfindlist_t *findlist, unsigned int bias) { - dns_adbfind_t *best = NULL; - dns_adbfindlist_t sorted; - dns_adbaddrinfo_t *addrinfo, *bestaddrinfo; - - /* Sort each find's addrinfo list by SRTT. */ - ISC_LIST_FOREACH(*findlist, curr, publink) { - sort_adbfind(curr, bias); - } - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(*findlist)) { - dns_adbfind_t *curr = NULL; - unsigned int best_srtt; - - best = ISC_LIST_HEAD(*findlist); - bestaddrinfo = ISC_LIST_HEAD(best->list); - INSIST(bestaddrinfo != NULL); - best_srtt = bestaddrinfo->srtt; - if (isc_sockaddr_pf(&bestaddrinfo->sockaddr) != AF_INET6) { - best_srtt += bias; - } - curr = ISC_LIST_NEXT(best, publink); - while (curr != NULL) { - unsigned int curr_srtt; - addrinfo = ISC_LIST_HEAD(curr->list); - INSIST(addrinfo != NULL); - curr_srtt = addrinfo->srtt; - if (isc_sockaddr_pf(&addrinfo->sockaddr) != AF_INET6) { - curr_srtt += bias; - } - if (curr_srtt < best_srtt) { - best = curr; - best_srtt = curr_srtt; - } - curr = ISC_LIST_NEXT(curr, publink); - } - ISC_LIST_UNLINK(*findlist, best, publink); - ISC_LIST_APPEND(sorted, best, publink); - } - *findlist = sorted; -} - /* * Return true iff the ADB find has an already pending fetch for 'type'. This * is used to find out whether we're in a loop, where a fetch is waiting for a @@ -3388,7 +3314,8 @@ already_waiting_for(dns_adbfind_t *find, dns_rdatatype_t type) { static void findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, unsigned int options, unsigned int flags, isc_stdtime_t now, - bool *overquota, bool *need_alternate, bool *have_address) { + bool *overquota, bool *need_alternate, bool *have_address, + size_t maxfindlen, size_t *findlen) { dns_adbfind_t *find = NULL; dns_resolver_t *res = fctx->res; bool unshared = ((fctx->options & DNS_FETCHOPT_UNSHARED) != 0); @@ -3429,7 +3356,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, result = dns_adb_createfind(fctx->adb, fctx->loop, fctx_finddone, fctx, name, options, now, res->view->dstport, fctx->depth + 1, fctx->qc, fctx->gqc, fctx, - &find); + maxfindlen, &find, findlen); isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), "fctx %p(%s): createfind for %s - %s", @@ -3471,6 +3398,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port, } } } + if ((flags & FCTX_ADDRINFO_DUALSTACK) != 0) { ISC_LIST_APPEND(fctx->altfinds, find, publink); } else { @@ -3683,7 +3611,7 @@ fctx_getaddresses_addresses(fetchctx_t *fctx, isc_stdtime_t now, ISC_LIST_FOREACH(fctx->delegset->delegs, deleg, link) { dns_adbfind_t *find = NULL; - size_t maxaddrs = max_delegation_servers - *ns_processed; + size_t maxfindlen = max_delegation_servers - *ns_processed; size_t findlen = 0; if (*ns_processed >= max_delegation_servers) { @@ -3703,7 +3631,7 @@ fctx_getaddresses_addresses(fetchctx_t *fctx, isc_stdtime_t now, fetchctx_ref(fctx); dns_adb_createaddrinfosfind(fctx->adb, &deleg->addresses, fctx->res->view->dstport, options, - now, maxaddrs, &find, &findlen); + now, maxfindlen, &find, &findlen); if (find == NULL) { fetchctx_unref(fctx); @@ -3791,6 +3719,12 @@ shufflens: unsigned int static_stub = 0; unsigned int no_fetch = 0; dns_name_t *ns = nameservers[i]; + size_t maxfindlen = max_delegation_servers - *ns_processed; + size_t findlen = 0; + + if (*ns_processed >= max_delegation_servers) { + break; + } if (fctx->delegset->staticstub && dns_name_equal(ns, fctx->domain)) @@ -3807,15 +3741,15 @@ shufflens: } findname(fctx, ns, 0, stdoptions | static_stub | no_fetch, 0, - now, &overquota, need_alternatep, &have_address); + now, &overquota, need_alternatep, &have_address, + maxfindlen, &findlen); if (!overquota) { *all_spilledp = false; } + *ns_processed += findlen; - if (++(*ns_processed) >= max_delegation_servers) { - break; - } + INSIST(*ns_processed <= max_delegation_servers); } if (fctx->pending_running == 0 && !have_address) { @@ -3841,7 +3775,8 @@ fctx_getaddresses_alternate(fetchctx_t *fctx, isc_stdtime_t now, if (!a->isaddress) { findname(fctx, &a->_u._n.name, a->_u._n.port, stdoptions, FCTX_ADDRINFO_DUALSTACK, now, NULL, - NULL, NULL); + NULL, NULL, + fctx->res->view->max_delegation_servers, NULL); continue; } if (isc_sockaddr_pf(&a->_u.addr) != family) { @@ -4032,8 +3967,6 @@ out: * We've found some addresses. We might still be * looking for more addresses. */ - sort_finds(&fctx->finds, res->view->v6bias); - sort_finds(&fctx->altfinds, 0); return ISC_R_SUCCESS; } @@ -4145,6 +4078,76 @@ possibly_mark(fetchctx_t *fctx, dns_adbaddrinfo_t *addr) { } } +static dns_adbaddrinfo_t * +nextaddress(fetchctx_t *fctx) { + dns_adbaddrinfo_t *prevai = fctx->foundaddrinfo, *lowestsrttai = NULL; + unsigned int v6bias = fctx->res->view->v6bias, lowestsrtt = 0; + + /* + * Let's walk through the list of dns_adbaddrinfo_t to find the best + * next server address to query. This is linear on the number of + * dns_adbaddrinfo_t which are grouped in find list (for each ADB find). + */ + ISC_LIST_FOREACH(fctx->finds, find, publink) { + ISC_LIST_FOREACH(find->list, ai, publink) { + /* + * This address has been marked already, skip it. + */ + if (!UNMARKED(ai)) { + continue; + } + + /* + * This address is the same as the previously used + * address, it's a duplicate, mark it and skip it! + */ + if (prevai != NULL) { + if (prevai->entry == ai->entry) { + ai->flags |= FCTX_ADDRINFO_MARK; + continue; + } + } + + /* + * Mark and skip this address if incompatible (i.e. IPv6 + * address on a v4 only server, or for ACL reason, etc.) + */ + possibly_mark(fctx, ai); + if (!UNMARKED(ai)) { + continue; + } + + /* + * This address hasn't been tried yet and is a + * good candidate. Let's keep track of it if it + * has the lowest SRTT so far (or if there is no + * address with lowest SRTT found yet). + */ + unsigned int aisrtt = ai->srtt; + + if (isc_sockaddr_pf(&ai->sockaddr) != AF_INET6) { + aisrtt += v6bias; + } + + if (lowestsrttai == NULL || aisrtt < lowestsrtt) { + lowestsrttai = ai; + lowestsrtt = aisrtt; + continue; + } + } + } + + /* + * This is the next address to query. If this is NULL, we're done. + */ + if (lowestsrttai != NULL) { + lowestsrttai->flags |= FCTX_ADDRINFO_MARK; + } + fctx->foundaddrinfo = lowestsrttai; + + return lowestsrttai; +} + static dns_adbaddrinfo_t * fctx_nextaddress(fetchctx_t *fctx) { dns_adbfind_t *find = NULL, *start = NULL; @@ -4164,7 +4167,6 @@ fctx_nextaddress(fetchctx_t *fctx) { possibly_mark(fctx, ai); if (UNMARKED(ai)) { ai->flags |= FCTX_ADDRINFO_MARK; - fctx->find = NULL; fctx->forwarding = true; /* @@ -4185,44 +4187,7 @@ fctx_nextaddress(fetchctx_t *fctx) { fctx->forwarding = false; FCTX_ATTR_SET(fctx, FCTX_ATTR_TRIEDFIND); - find = fctx->find; - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } else { - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } - - /* - * Find the first unmarked addrinfo. - */ - if (find != NULL) { - start = find; - do { - ISC_LIST_FOREACH(find->list, ai, publink) { - if (!UNMARKED(ai)) { - continue; - } - possibly_mark(fctx, ai); - if (UNMARKED(ai)) { - ai->flags |= FCTX_ADDRINFO_MARK; - faddrinfo = ai; - break; - } - } - if (faddrinfo != NULL) { - break; - } - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } while (find != start); - } - - fctx->find = find; + faddrinfo = nextaddress(fctx); if (faddrinfo != NULL) { return faddrinfo; } @@ -4298,6 +4263,39 @@ fctx_nextaddress(fetchctx_t *fctx) { return addrinfo; } +static isc_result_t +incr_query_counters(fetchctx_t *fctx) { + isc_result_t result; + + result = isc_counter_increment(fctx->qc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-recursion-queries, querycount=", + isc_counter_used(fctx->qc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, + ISC_LOG_DEBUG(3), + "exceeded max queries resolving '%s' " + "(max-recursion-queries, querycount=%u)", + fctx->info, isc_counter_used(fctx->qc)); + } else if (fctx->gqc != NULL) { + result = isc_counter_increment(fctx->gqc); +#if WANT_QUERYTRACE + FCTXTRACE5("query", "max-query-count, querycount=", + isc_counter_used(fctx->gqc)); +#endif + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), + "exceeded global max queries resolving " + "'%s' (max-query-count, querycount=%u)", + fctx->info, isc_counter_used(fctx->gqc)); + } + } + + return result; +} + static void fctx_try(fetchctx_t *fctx, bool retrying) { isc_result_t result; @@ -4442,36 +4440,11 @@ fctx_try(fetchctx_t *fctx, bool retrying) { return; } - result = isc_counter_increment(fctx->qc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-recursion-queries, querycount=", - isc_counter_used(fctx->qc)); -#endif + result = incr_query_counters(fctx); if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_RESOLVER, DNS_LOGMODULE_RESOLVER, - ISC_LOG_DEBUG(3), - "exceeded max queries resolving '%s' " - "(max-recursion-queries, querycount=%u)", - fctx->info, isc_counter_used(fctx->qc)); goto done; } - if (fctx->gqc != NULL) { - result = isc_counter_increment(fctx->gqc); -#if WANT_QUERYTRACE - FCTXTRACE5("query", "max-query-count, querycount=", - isc_counter_used(fctx->gqc)); -#endif - if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_RESOLVER, - DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3), - "exceeded global max queries resolving " - "'%s' (max-query-count, querycount=%u)", - fctx->info, isc_counter_used(fctx->gqc)); - goto done; - } - } - result = fctx_query(fctx, addrinfo, fctx->options); if (result != ISC_R_SUCCESS) { goto done; @@ -7023,6 +6996,13 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, return true; } + /* + * deny-answer-address doesn't apply to non-IN classes. + */ + if (rdataset->rdclass != dns_rdataclass_in) { + return true; + } + /* * Otherwise, search the filter list for a match for each * address record. If a match is found, the address should be @@ -7741,6 +7721,7 @@ resquery_response(isc_result_t eresult, isc_region_t *region, void *arg) { return; cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -8101,6 +8082,7 @@ resquery_response_continue(void *arg, isc_result_t result) { rctx_done(rctx, result); cleanup: + resquery_detach(&rctx->query); isc_mem_putanddetach(&rctx->mctx, rctx, sizeof(*rctx)); } @@ -8114,7 +8096,7 @@ static void rctx_respinit(resquery_t *query, fetchctx_t *fctx, isc_result_t result, isc_region_t *region, respctx_t *rctx) { *rctx = (respctx_t){ .result = result, - .query = query, + .query = resquery_ref(query), .fctx = fctx, .broken_type = badns_response, .retryopts = query->options }; @@ -9646,9 +9628,9 @@ rctx_nextserver(respctx_t *rctx, dns_message_t *message, * rctx_resend(): * * Resend the query, probably with the options changed. Calls - * fctx_query(), passing rctx->retryopts (which is based on - * query->options, but may have been updated since the last time - * fctx_query() was called). + * fctx_query(), unless query counter limits are hit, passing + * rctx->retryopts (which is based on query->options, but may have + * been updated since the last time fctx_query() was called). */ static void rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { @@ -9656,8 +9638,15 @@ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) { isc_result_t result; FCTXTRACE("resend"); - inc_stats(fctx->res, dns_resstatscounter_retry); + + CHECK(incr_query_counters(fctx)); + result = fctx_query(fctx, addrinfo, rctx->retryopts); + if (result == ISC_R_SUCCESS) { + inc_stats(fctx->res, dns_resstatscounter_retry); + } + +cleanup: if (result != ISC_R_SUCCESS) { fctx_failure_detach(&rctx->fctx, result); } @@ -9976,7 +9965,9 @@ rctx_badserver(respctx_t *rctx, isc_result_t result) { rctx->broken_server = DNS_R_BADVERS; rctx->next_server = true; #endif /* if DNS_EDNS_VERSION > 0 */ - } else if (rcode == dns_rcode_badcookie && rctx->query->rmessage->cc_ok) + } else if (rcode == dns_rcode_badcookie && + rctx->query->rmessage->cc_ok && + (rctx->retryopts & DNS_FETCHOPT_TCP) == 0) { /* * We have recorded the new cookie. diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index 0eab106e4b..b57477c00a 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -181,30 +181,22 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, intoken = (isc_region_t){ tkeyin->key, tkeyin->keylen }; result = dst_gssapi_acceptctx(tctx->gssapi_keytab, &intoken, &outtoken, &gss_ctx, principal, tctx->mctx); - if (result == DNS_R_INVALIDTKEY) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } + if (result != ISC_R_SUCCESS) { tkeyout->error = dns_tsigerror_badkey; tkey_log("process_gsstkey(): dns_tsigerror_badkey"); - return ISC_R_SUCCESS; - } - if (result != DNS_R_CONTINUE && result != ISC_R_SUCCESS) { - CHECK(result); + CLEANUP(ISC_R_SUCCESS); } /* - * XXXDCL Section 4.1.3: Limit GSS_S_CONTINUE_NEEDED to 10 times. + * Multi-round GSS-API negotiation (GSS_S_CONTINUE_NEEDED) is + * rejected in dst_gssapi_acceptctx(), so if we reach here the + * negotiation is complete and the principal must be set. */ if (dns_name_countlabels(principal) == 0U) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } - dst_gssapi_deletectx(tctx->mctx, &gss_ctx); tkeyout->error = dns_tsigerror_badkey; tkey_log("process_gsstkey(): " "completed context with empty principal"); - return ISC_R_SUCCESS; + CLEANUP(ISC_R_SUCCESS); } else if (tsigkey == NULL) { #if HAVE_GSSAPI OM_uint32 gret, minor, lifetime; @@ -283,7 +275,9 @@ cleanup: isc_buffer_free(&outtoken); } - tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + if (result != ISC_R_SUCCESS) { + tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + } return result; } @@ -678,9 +672,8 @@ dns_tkey_gssnegotiate(dns_message_t *qmsg, dns_message_t *rmsg, NULL)); /* - * XXXSRA This seems confused. If we got CONTINUE from initctx, - * the GSS negotiation hasn't completed yet, so we can't sign - * anything yet. + * GSS negotiation is complete (CONTINUE returned earlier). + * Create the TSIG key from the established context. */ CHECK(dns_tsigkey_createfromkey(tkeyname, DST_ALG_GSSAPI, dstkey, true, false, NULL, rtkey.inception, diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 3414ed8d35..4ab816c1f2 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -17278,9 +17278,11 @@ checkds_find_address(dns_checkds_t *checkds) { isc_result_t result; unsigned int options; dns_adb_t *adb = NULL; + dns_view_t *view = NULL; REQUIRE(DNS_CHECKDS_VALID(checkds)); + view = checkds->zone->view; options = DNS_ADBFIND_WANTEVENT; if (isc_net_probeipv4() != ISC_R_DISABLED) { options |= DNS_ADBFIND_INET; @@ -17289,7 +17291,7 @@ checkds_find_address(dns_checkds_t *checkds) { options |= DNS_ADBFIND_INET6; } - dns_view_getadb(checkds->zone->view, &adb); + dns_view_getadb(view, &adb); if (adb == NULL) { goto destroy; } @@ -17297,7 +17299,7 @@ checkds_find_address(dns_checkds_t *checkds) { result = dns_adb_createfind( adb, checkds->zone->loop, process_checkds_adb_event, checkds, &checkds->ns, options, 0, checkds->zone->view->dstport, 0, NULL, - NULL, NULL, &checkds->find); + NULL, NULL, view->max_delegation_servers, &checkds->find, NULL); dns_adb_detach(&adb); /* Something failed? */ diff --git a/lib/isc/mem.c b/lib/isc/mem.c index 9385d4a83a..2ef4e28583 100644 --- a/lib/isc/mem.c +++ b/lib/isc/mem.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -126,7 +127,6 @@ static isc_mutex_t contextslock; typedef union { struct { atomic_int_fast64_t inuse; - atomic_bool is_overmem; }; char padding[ISC_OS_CACHELINE_SIZE]; } isc__mem_stat_t; @@ -620,7 +620,6 @@ mem_create(const char *name, isc_mem_t **ctxp, unsigned int debugging, for (size_t i = 0; i < ARRAY_SIZE(ctx->stat_s); i++) { atomic_init(&ctx->stat_s[i].inuse, 0); - atomic_init(&ctx->stat_s[i].is_overmem, false); } /* Reserve the [-1] index for ISC_TID_UNKNOWN */ @@ -1020,50 +1019,30 @@ bool isc_mem_isovermem(isc_mem_t *ctx) { REQUIRE(VALID_CONTEXT(ctx)); - int32_t tid = isc_tid(); - - bool is_overmem = atomic_load_relaxed(&ctx->stat[tid].is_overmem); - - if (!is_overmem) { - /* We are not overmem, check whether we should be? */ - size_t hiwater = atomic_load_relaxed(&ctx->hi_water); - if (hiwater == 0) { - return false; - } - - size_t inuse = isc_mem_inuse(ctx); - if (inuse <= hiwater) { - return false; - } - - if ((ctx->debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem %s mctx %p inuse %zu hi_water %zu\n", - ctx->name, ctx, inuse, hiwater); - } - - atomic_store_relaxed(&ctx->stat[tid].is_overmem, true); - return true; - } else { - /* We are overmem, check whether we should not be? */ - size_t lowater = atomic_load_relaxed(&ctx->lo_water); - if (lowater == 0) { - return false; - } - - size_t inuse = isc_mem_inuse(ctx); - if (inuse >= lowater) { - return true; - } - - if ((ctx->debugging & ISC_MEM_DEBUGUSAGE) != 0) { - fprintf(stderr, - "overmem %s mctx %p inuse %zu lo_water %zu\n", - ctx->name, ctx, inuse, lowater); - } - atomic_store_relaxed(&ctx->stat[tid].is_overmem, false); + size_t hiwater = atomic_load_relaxed(&ctx->hi_water); + if (hiwater == 0) { return false; } + + size_t inuse = isc_mem_inuse(ctx); + if (inuse >= hiwater) { + return true; + } + + size_t lowater = atomic_load_relaxed(&ctx->lo_water); + if (inuse <= lowater) { + return false; + } + + /* + * Between lo_water and hi_water, return true with a probability + * that ramps linearly from 0 at lo_water to 1 at hi_water. This + * spreads cache cleaning across many inserts instead of triggering + * a thundering herd once the hi_water mark is crossed. + */ + uint32_t prob = (uint32_t)(((uint64_t)(inuse - lowater) * 256) / + (hiwater - lowater)); + return isc_random8() < prob; } const char * diff --git a/lib/isc/netmgr/http.c b/lib/isc/netmgr/http.c index 4d8fe48174..0055311cb2 100644 --- a/lib/isc/netmgr/http.c +++ b/lib/isc/netmgr/http.c @@ -2743,6 +2743,8 @@ server_httpsend(isc_nmhandle_t *handle, isc_nmsocket_t *sock, } else { cb(handle, result, cbarg); } + + isc_buffer_initnull(&sock->h2->wbuf); isc__nm_uvreq_put(&req); } diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 232d4d1890..2995319627 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -2869,13 +2869,17 @@ check_mirror_zone_notify(const cfg_obj_t *zoptions, const char *znamestr) { */ static bool check_recursion(const cfg_obj_t *config, const cfg_obj_t *voptions, - const cfg_obj_t *goptions, cfg_aclconfctx_t *aclctx, - isc_mem_t *mctx) { + dns_rdataclass_t vclass, const cfg_obj_t *goptions, + cfg_aclconfctx_t *aclctx, isc_mem_t *mctx) { dns_acl_t *acl = NULL; const cfg_obj_t *obj; isc_result_t result = ISC_R_SUCCESS; bool retval = true; + if (vclass != dns_rdataclass_in) { + return false; + } + /* * Check the "recursion" option first. */ @@ -3827,7 +3831,7 @@ isccfg_check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions, * contradicts the purpose of the former. */ if (ztype == CFG_ZONE_MIRROR && - !check_recursion(config, voptions, goptions, aclctx, mctx)) + !check_recursion(config, voptions, zclass, goptions, aclctx, mctx)) { cfg_obj_log(zoptions, ISC_LOG_ERROR, "zone '%s': mirror zones cannot be used if " @@ -5646,6 +5650,17 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, cfg_aclconfctx_create(mctx, &aclctx); + if (vclass != dns_rdataclass_in) { + if (check_recursion(config, voptions, dns_rdataclass_in, + options, aclctx, mctx)) + { + cfg_obj_log(opts, ISC_LOG_WARNING, + "recursion will be disabled for " + "non-IN view '%s'", + viewname); + } + } + if (voptions != NULL) { (void)cfg_map_get(voptions, "zone", &zones); } else { diff --git a/lib/ns/client.c b/lib/ns/client.c index 7a4fd19771..7d2f77e646 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -2082,7 +2083,9 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, } } - if (client->message->rdclass == 0) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + switch (client->message->rdclass) { + case dns_rdataclass_reserved0: if ((client->inner.attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && client->message->opcode == dns_opcode_query && @@ -2102,12 +2105,46 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, return; } + ns_client_dumpmessage(client, + "message class could not be determined"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + return; + case dns_rdataclass_in: + break; + case dns_rdataclass_chaos: + break; + case dns_rdataclass_hs: + break; + case dns_rdataclass_none: + if (client->message->opcode != dns_opcode_update) { + ns_client_dumpmessage(client, + "message class NONE can be only " + "used in DNS updates"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + case dns_rdataclass_any: + /* + * Required for TKEY negotiation. + */ + if (client->message->tkey == 0) { + ns_client_dumpmessage(client, + "message class ANY can be only " + "used for TKEY negotiation"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + default: + dns_rdataclass_format(client->message->rdclass, classbuf, + sizeof(classbuf)); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), - "message class could not be determined"); - ns_client_dumpmessage(client, "message class could not be " - "determined"); - ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + "invalid message class: %s", classbuf); + + ns_client_error(client, DNS_R_NOTIMP); return; } @@ -2192,9 +2229,6 @@ ns_client_request_continue(void *arg) { "SIG(0) checks quota reached"); if (can_log_sigchecks_quota()) { - ns_client_log(client, NS_LOGCATEGORY_CLIENT, - NS_LOGMODULE_CLIENT, ISC_LOG_INFO, - "SIG(0) checks quota reached"); ns_client_dumpmessage( client, "SIG(0) checks quota reached"); } @@ -2204,12 +2238,11 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); + ns_client_dumpmessage(client, ""); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", classname); - ns_client_dumpmessage(client, - "no matching view in class"); } dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); @@ -2459,6 +2492,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_update: CTRACE("update"); + if (client->inner.view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } #ifdef HAVE_DNSTAP dns_dt_send(client->inner.view, DNS_DTTYPE_UQ, &client->inner.peeraddr, @@ -2472,6 +2509,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_notify: CTRACE("notify"); + if (client->inner.view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } ns_client_settimeout(client, 60); ns_notify_start(client, client->inner.handle); break; @@ -2835,7 +2876,7 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) { int len = 1024; isc_result_t result; - if (!isc_log_wouldlog(ISC_LOG_DEBUG(1))) { + if (!isc_log_wouldlog(ISC_LOG_DEBUG(1)) || reason == NULL) { return; } diff --git a/lib/ns/update.c b/lib/ns/update.c index 93240ca250..b5ea582879 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -966,7 +966,9 @@ ssu_checkrr(void *data, rr_t *rr) { RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &ptr.ptr; } - if (rr->rdata.type == dns_rdatatype_srv) { + if (rr->rdata.rdclass == dns_rdataclass_in && + rr->rdata.type == dns_rdatatype_srv) + { result = dns_rdata_tostruct(&rr->rdata, &srv, NULL); RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &srv.target; @@ -1311,7 +1313,10 @@ replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { return true; } } - if (db_rr->type == dns_rdatatype_wks) { + + if (db_rr->rdclass == dns_rdataclass_in && + db_rr->type == dns_rdatatype_wks) + { /* * Compare the address and protocol fields only. These * form the first five bytes of the RR data. Do a @@ -1451,9 +1456,8 @@ add_rr_prepare_action(void *data, rr_t *rr) { * 'rdata', and 'ttl', respectively. */ static void -get_current_rr(dns_rdataclass_t zoneclass, dns_name_t *name, dns_rdata_t *rdata, - dns_rdatatype_t *covers, dns_ttl_t *ttl, - dns_rdataclass_t *update_class) { +get_current_rr(dns_name_t *name, dns_rdata_t *rdata, dns_rdatatype_t *covers, + dns_ttl_t *ttl, dns_rdataclass_t *update_class) { dns_rdataset_t *rdataset; isc_result_t result; rdataset = ISC_LIST_HEAD(name->list); @@ -1466,7 +1470,7 @@ get_current_rr(dns_rdataclass_t zoneclass, dns_name_t *name, dns_rdata_t *rdata, dns_rdataset_current(rdataset, rdata); INSIST(dns_rdataset_next(rdataset) == ISC_R_NOMORE); *update_class = rdata->rdclass; - rdata->rdclass = zoneclass; + rdata->rdclass = dns_rdataclass_in; } /*% @@ -1562,7 +1566,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_message_t *request = client->message; isc_mem_t *mctx = client->manager->mctx; dns_aclenv_t *env = client->manager->aclenv; - dns_rdataclass_t zoneclass; dns_rdatatype_t covers; dns_name_t *zonename = NULL; unsigned int *maxbytype = NULL; @@ -1574,11 +1577,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); dns_zone_getssutable(zone, &ssutable); options = dns_zone_getoptions(zone); dns_db_currentversion(db, &ver); + /* Updates are only supported for class IN. */ + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + /* * Update message processing can leak record existence information * so check that we are allowed to query this zone. Additionally, @@ -1623,13 +1628,12 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_rdataclass_t update_class; INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); if (!dns_name_issubdomain(name, zonename)) { FAILC(DNS_R_NOTZONE, "update RR is outside zone"); } - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * Check for meta-RRs. The RFC2136 pseudocode says * check for ANY|AXFR|MAILA|MAILB, but the text adds @@ -1643,6 +1647,7 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CLEANUP(DNS_R_REFUSED); } if ((options & DNS_ZONEOPT_CHECKSVCB) != 0 && + rdata.rdclass == dns_rdataclass_in && rdata.type == dns_rdatatype_svcb) { result = dns_rdata_checksvcb(name, &rdata); @@ -1731,7 +1736,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { } if (update_class == dns_rdataclass_any && - zoneclass == dns_rdataclass_in && (rdata.type == dns_rdatatype_ptr || rdata.type == dns_rdatatype_srv)) { @@ -2629,7 +2633,6 @@ update_action(void *arg) { isc_mem_t *mctx = client->manager->mctx; dns_rdatatype_t covers; dns_message_t *request = client->message; - dns_rdataclass_t zoneclass; dns_name_t *zonename = NULL; dns_fixedname_t tmpnamefixed; dns_name_t *tmpname = NULL; @@ -2646,9 +2649,10 @@ update_action(void *arg) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); options = dns_zone_getoptions(zone); + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + is_inline = (!dns_zone_israw(zone) && dns_zone_issecure(zone)); is_maintain = (dns_zone_getkasp(zone) != NULL) && !dns_zone_israw(zone); is_signing = is_inline || is_maintain; @@ -2669,8 +2673,7 @@ update_action(void *arg) { dns_rdataclass_t update_class; bool flag; - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); if (ttl != 0) { PREREQFAILC(DNS_R_FORMERR, @@ -2733,7 +2736,7 @@ update_action(void *arg) { "prerequisite not satisfied"); } } - } else if (update_class == zoneclass) { + } else if (update_class == dns_rdataclass_in) { /* "temp += rr;" */ temp_append(&temp, name, &rdata); } else { @@ -2784,10 +2787,9 @@ update_action(void *arg) { INSIST(ssutable == NULL || maxidx < maxbytypelen); - get_current_rr(zoneclass, name, &rdata, &covers, &ttl, - &update_class); + get_current_rr(name, &rdata, &covers, &ttl, &update_class); - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * RFC1123 doesn't allow MF and MD in master files. */ diff --git a/lib/ns/xfrout.c b/lib/ns/xfrout.c index 98c7374b4f..1865f6b1f4 100644 --- a/lib/ns/xfrout.c +++ b/lib/ns/xfrout.c @@ -738,6 +738,7 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { bool is_poll = false; bool is_dlz = false; bool is_ixfr = false; + bool is_quota_applied = false; bool useviewacl = false; uint32_t begin_serial = 0, current_serial; @@ -754,16 +755,6 @@ ns_xfr_start(ns_client_t *client, dns_rdatatype_t reqtype) { ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(6), "%s request", mnemonic); - /* - * Apply quota. - */ - result = isc_quota_acquire(&client->manager->sctx->xfroutquota); - if (result != ISC_R_SUCCESS) { - isc_log_write(DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, - ISC_LOG_WARNING, "%s request denied: %s", - mnemonic, isc_result_totext(result)); - goto max_quota; - } /* * Interpret the question section. @@ -922,6 +913,19 @@ got_soa: FAILC(DNS_R_FORMERR, "attempted AXFR over UDP"); } + /* + * Apply quota after ACL is checked, so that unauthorized clients + * can not starve the authorized clients. + */ + result = isc_quota_acquire(&client->manager->sctx->xfroutquota); + if (result != ISC_R_SUCCESS) { + isc_log_write(DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, + ISC_LOG_WARNING, "%s request denied: %s", + mnemonic, isc_result_totext(result)); + goto cleanup; + } + is_quota_applied = true; + /* * Look up the requesting server in the peer table. */ @@ -1060,7 +1064,7 @@ have_stream: CHECK(dns_message_getquerytsig(request, mctx, &tsigbuf)); /* * Create the xfrout context object. This transfers the ownership - * of "stream", "db", "ver", and "quota" to the xfrout context object. + * of "stream", "db" and "ver" to the xfrout context object. */ if (is_dlz) { @@ -1179,10 +1183,13 @@ cleanup: } if (xfr != NULL) { + /* The quota will be released in xfrout_ctx_destroy(). */ + INSIST(is_quota_applied); xfrout_fail(xfr, result, "setting up zone transfer"); } else if (result != ISC_R_SUCCESS) { - isc_quota_release(&client->manager->sctx->xfroutquota); - max_quota: + if (is_quota_applied) { + isc_quota_release(&client->manager->sctx->xfroutquota); + } ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, NS_LOGMODULE_XFER_OUT, ISC_LOG_DEBUG(3), "zone transfer setup failed"); diff --git a/tests/dns/qpdb_test.c b/tests/dns/qpdb_test.c index de3ea1245b..8293c971aa 100644 --- a/tests/dns/qpdb_test.c +++ b/tests/dns/qpdb_test.c @@ -128,7 +128,7 @@ ISC_LOOP_TEST_IMPL(overmempurge_bigrdata) { dns_db_t *db = NULL; isc_mem_t *mctx = NULL; isc_stdtime_t now = isc_stdtime_now(); - size_t i; + size_t i = 0; isc_mem_create("test", &mctx); @@ -140,21 +140,21 @@ ISC_LOOP_TEST_IMPL(overmempurge_bigrdata) { isc_mem_setwater(mctx, hiwater, lowater); /* - * Add cache entries with minimum size of data until 'overmem' - * condition is triggered. - * This should eventually happen, but we also limit the number of - * iteration to avoid an infinite loop in case something gets wrong. + * Add a lot of data entries sufficient to push the context + * above the hi_water mark. */ - for (i = 0; !isc_mem_isovermem(mctx) && i < (maxcache / 10); i++) { - overmempurge_addrdataset(db, now, i, 50053, 0, false); + while (isc_mem_inuse(mctx) < hiwater) { + overmempurge_addrdataset(db, now, i, 50053, 0, true); + i++; } - assert_true(isc_mem_isovermem(mctx)); + assert_true(isc_mem_inuse(mctx) >= hiwater); + assert_true(isc_mem_inuse(mctx) < maxcache); /* * Then try to add the same number of entries, each has very large data. - * 'overmem purge' should keep the total cache size from exceeding - * the 'hiwater' mark too much. So we should be able to assume the - * cache size doesn't reach the "max". + * Probabilistic LRU cleaning should keep the total cache size from + * exceeding the 'hiwater' mark too much. So we should be able to + * assume the cache size doesn't reach the "max". */ while (i-- > 0) { overmempurge_addrdataset(db, now, i, 50054, @@ -180,7 +180,7 @@ ISC_LOOP_TEST_IMPL(overmempurge_longname) { dns_db_t *db = NULL; isc_mem_t *mctx = NULL; isc_stdtime_t now = isc_stdtime_now(); - size_t i; + size_t i = 0; isc_mem_create("test", &mctx); @@ -192,21 +192,21 @@ ISC_LOOP_TEST_IMPL(overmempurge_longname) { isc_mem_setwater(mctx, hiwater, lowater); /* - * Add cache entries with minimum size of data until 'overmem' - * condition is triggered. - * This should eventually happen, but we also limit the number of - * iteration to avoid an infinite loop in case something gets wrong. + * Add a lot of data entries sufficient to push the context + * above the hi_water mark. */ - for (i = 0; !isc_mem_isovermem(mctx) && i < (maxcache / 10); i++) { - overmempurge_addrdataset(db, now, i, 50053, 0, false); + while (isc_mem_inuse(mctx) < hiwater) { + overmempurge_addrdataset(db, now, i, 50053, 0, true); + i++; } - assert_true(isc_mem_isovermem(mctx)); + assert_true(isc_mem_inuse(mctx) >= hiwater); + assert_true(isc_mem_inuse(mctx) < maxcache); /* * Then try to add the same number of entries, each has very long name. - * 'overmem purge' should keep the total cache size from not exceeding - * the 'hiwater' mark too much. So we should be able to assume the cache - * size doesn't reach the "max". + * Probabilistic LRU cleaning should keep the total cache size from + * exceeding the 'hiwater' mark too much. So we should be able to + * assume the cache size doesn't reach the "max". */ while (i-- > 0) { overmempurge_addrdataset(db, now, i, 50054, 0, true); diff --git a/tests/isc/mem_test.c b/tests/isc/mem_test.c index 7724488935..5462b628d4 100644 --- a/tests/isc/mem_test.c +++ b/tests/isc/mem_test.c @@ -291,6 +291,17 @@ ISC_RUN_TEST_IMPL(isc_mem_reallocate) { isc_mem_free(isc_g_mctx, data); } +static bool +at_least_one_overmem(isc_mem_t *mctx) { + for (size_t i = 0; i < UINT16_MAX; i++) { + /* The overmem is probability based in this range */ + if (isc_mem_isovermem(mctx)) { + return true; + } + } + return false; +} + ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_t *mctx = NULL; isc_mem_create("test", &mctx); @@ -298,27 +309,27 @@ ISC_RUN_TEST_IMPL(isc_mem_overmem) { isc_mem_setwater(mctx, 1024, 512); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ void *data1 = isc_mem_allocate(mctx, 256); assert_false(isc_mem_isovermem(mctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ void *data2 = isc_mem_allocate(mctx, 512); - assert_false(isc_mem_isovermem(mctx)); + assert_true(at_least_one_overmem(mctx)); - /* hi_water < inuse */ + /* hi_water <= inuse is always true */ void *data3 = isc_mem_allocate(mctx, 512); assert_true(isc_mem_isovermem(mctx)); - /* lo_water < inuse < hi_water */ + /* lo_water < inuse < hi_water might be true or false */ isc_mem_free(mctx, data2); - assert_true(isc_mem_isovermem(mctx)); + assert_true(at_least_one_overmem(mctx)); - /* inuse < lo_water */ + /* inuse <= lo_water is always false */ isc_mem_free(mctx, data3); assert_false(isc_mem_isovermem(mctx)); - /* inuse == 0 */ + /* inuse == 0 is always false */ isc_mem_free(mctx, data1); assert_false(isc_mem_isovermem(mctx));