diff --git a/bin/named/main.c b/bin/named/main.c index df47e8d667..ddd43c9b2c 100644 --- a/bin/named/main.c +++ b/bin/named/main.c @@ -114,6 +114,8 @@ extern unsigned int dns_zone_mkey_hour; extern unsigned int dns_zone_mkey_day; extern unsigned int dns_zone_mkey_month; +extern size_t dns_adb_addrslimit; + static bool want_stats = false; static char program_name[NAME_MAX] = "named"; static char absolute_conffile[PATH_MAX]; @@ -805,6 +807,13 @@ parse_T_opt(char *option) { transferstuck = true; } else if (!strncmp(option, "tat=", 4)) { named_g_tat_interval = atoi(option + 4); + } else if (!strncmp(option, "adbaddrslimit=", 14)) { + size_t adb_addrslimit = atoi(option + 14); + if (adb_addrslimit < 1) { + named_main_earlyfatal("adbaddrslimit must be at " + "least 1"); + } + dns_adb_addrslimit = adb_addrslimit; } else { fprintf(stderr, "unknown -T flag '%s'\n", option); } diff --git a/bin/named/server.c b/bin/named/server.c index 72ab7c956a..cbae556589 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1987,10 +1987,12 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) { dns_rdataclass_t zclass = view->rdclass; isc_result_t result; + dns_zone_setclass(zone, zclass); result = dns_zonemgr_managezone(named_g_server->zonemgr, zone); if (result != ISC_R_SUCCESS) { return result; } + dns_zone_setstats(zone, named_g_server->zonestats); return named_zone_configure_writeable_dlz(dlzdb, zone, zclass, origin); @@ -4515,6 +4517,7 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, obj = NULL; result = named_config_get(maps, "max-cache-size", &obj); INSIST(result == ISC_R_SUCCESS); + /* * If "-T maxcachesize=..." is in effect, it overrides any other * "max-cache-size" setting found in configuration, either implicit or @@ -5224,34 +5227,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, } /* - * We have default hints for class IN if we need them. + * 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. Other classes don't support + * recursion and don't need hints. */ if (view->rdclass == dns_rdataclass_in && view->hints == NULL) { dns_view_sethints(view, named_g_server->in_roothints); } - /* - * If we still have no 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->hints == NULL) { - dns_zone_t *rootzone = NULL; - (void)dns_view_findzone(view, dns_rootname, &rootzone); - if (rootzone != NULL) { - dns_zone_detach(&rootzone); - need_hints = false; - } - if (need_hints) { - isc_log_write(named_g_lctx, NAMED_LOGCATEGORY_GENERAL, - NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, - "no root hints for view '%s'", - view->name); - } - } - /* * Configure the view's transports (DoT/DoH) */ @@ -5379,7 +5363,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)); obj = NULL; result = named_config_get(maps, "qname-minimization", &obj); @@ -5479,14 +5464,13 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, CHECK(configure_view_acl(vconfig, config, NULL, "allow-query-cache-on", NULL, actx, named_g_mctx, &view->cacheonacl)); - if (strcmp(view->name, "_bind") != 0 && - view->rdclass != dns_rdataclass_chaos) - { - /* named.conf only */ + if (view->rdclass != dns_rdataclass_in) { + dns_acl_none(named_g_mctx, &view->recursionacl); + dns_acl_none(named_g_mctx, &view->recursiononacl); + } else { CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion", NULL, actx, named_g_mctx, &view->recursionacl)); - /* named.conf only */ CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion-on", NULL, actx, named_g_mctx, &view->recursiononacl)); diff --git a/bin/tests/system/allow_query/tests.sh b/bin/tests/system/allow_query/tests.sh index e59a1abe6b..46d2a78077 100644 --- a/bin/tests/system/allow_query/tests.sh +++ b/bin/tests/system/allow_query/tests.sh @@ -703,7 +703,7 @@ $DIG -p ${PORT} @10.53.1.2 d.normal.example a >dig.out.ns3.4.$n || ret=1 grep 'recursion requested but not available' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'status: REFUSED' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'EDE: 18 (Prohibited)' dig.out.ns3.4.$n >/dev/null || ret=1 -nextpart ns3/named.run | grep 'allow-recursion-on did not match' >/dev/null || ret=1 +nextpart ns3/named.run | grep 'allow-query-cache-on did not match' >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 3bb772e4d5..f29949535a 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -543,6 +543,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 @@ -819,5 +820,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/isctest/asyncserver.py b/bin/tests/system/isctest/asyncserver.py index d35710ba5d..080c08c380 100644 --- a/bin/tests/system/isctest/asyncserver.py +++ b/bin/tests/system/isctest/asyncserver.py @@ -11,20 +11,9 @@ See the COPYRIGHT file distributed with this work for additional information regarding copyright ownership. """ +from collections.abc import AsyncGenerator, Callable, Coroutine, Sequence from dataclasses import dataclass, field -from typing import ( - Any, - AsyncGenerator, - Callable, - Coroutine, - Dict, - List, - Optional, - Set, - Tuple, - Union, - cast, -) +from typing import Any, cast import abc import asyncio @@ -52,11 +41,10 @@ import dns.rdataset import dns.rdatatype import dns.rrset import dns.tsig -import dns.version import dns.zone _UdpHandler = Callable[ - [bytes, Tuple[str, int], asyncio.DatagramTransport], Coroutine[Any, Any, None] + [bytes, tuple[str, int], asyncio.DatagramTransport], Coroutine[Any, Any, None] ] @@ -74,7 +62,7 @@ class _AsyncUdpHandler(asyncio.DatagramProtocol): self, handler: _UdpHandler, ) -> None: - self._transport: Optional[asyncio.DatagramTransport] = None + self._transport: asyncio.DatagramTransport | None = None self._handler: _UdpHandler = handler def connection_made(self, transport: asyncio.BaseTransport) -> None: @@ -83,7 +71,7 @@ class _AsyncUdpHandler(asyncio.DatagramProtocol): """ self._transport = cast(asyncio.DatagramTransport, transport) - def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None: + def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: """ Called by asyncio when a datagram is received. """ @@ -108,9 +96,9 @@ class AsyncServer: def __init__( self, - udp_handler: Optional[_UdpHandler], - tcp_handler: Optional[_TcpHandler], - pidfile: Optional[str] = None, + udp_handler: _UdpHandler | None, + tcp_handler: _TcpHandler | None, + pidfile: str | None = None, ) -> None: logging.basicConfig( format="%(asctime)s %(levelname)8s %(message)s", @@ -132,12 +120,12 @@ class AsyncServer: logging.info("Setting up IPv4 listener at %s:%d", ipv4_address, port) logging.info("Setting up IPv6 listener at [%s]:%d", ipv6_address, port) - self._ip_addresses: Tuple[str, str] = (ipv4_address, ipv6_address) + self._ip_addresses: tuple[str, str] = (ipv4_address, ipv6_address) self._port: int = port - self._udp_handler: Optional[_UdpHandler] = udp_handler - self._tcp_handler: Optional[_TcpHandler] = tcp_handler - self._pidfile: Optional[str] = pidfile - self._work_done: Optional[asyncio.Future] = None + self._udp_handler: _UdpHandler | None = udp_handler + self._tcp_handler: _TcpHandler | None = tcp_handler + self._pidfile: str | None = pidfile + self._work_done: asyncio.Future | None = None def _get_ipv4_address_from_directory_name(self) -> str: containing_directory = pathlib.Path().absolute().stem @@ -185,7 +173,7 @@ class AsyncServer: loop.set_exception_handler(self._handle_exception) def _handle_exception( - self, _: asyncio.AbstractEventLoop, context: Dict[str, Any] + self, _: asyncio.AbstractEventLoop, context: dict[str, Any] ) -> None: assert self._work_done exception = context.get("exception", RuntimeError(context["message"])) @@ -265,17 +253,16 @@ class QueryContext: query: dns.message.Message response: dns.message.Message + socket: Peer peer: Peer protocol: DnsProtocol - zone: Optional[dns.zone.Zone] = field(default=None, init=False) - soa: Optional[dns.rrset.RRset] = field(default=None, init=False) - node: Optional[dns.node.Node] = field(default=None, init=False) - answer: Optional[dns.rdataset.Rdataset] = field(default=None, init=False) - alias: Optional[dns.name.Name] = field(default=None, init=False) - _initialized_response: Optional[dns.message.Message] = field( - default=None, init=False - ) - _initialized_response_with_zone_data: Optional[dns.message.Message] = field( + zone: dns.zone.Zone | None = field(default=None, init=False) + soa: dns.rrset.RRset | None = field(default=None, init=False) + node: dns.node.Node | None = field(default=None, init=False) + answer: dns.rdataset.Rdataset | None = field(default=None, init=False) + alias: dns.name.Name | None = field(default=None, init=False) + _initialized_response: dns.message.Message | None = field(default=None, init=False) + _initialized_response_with_zone_data: dns.message.Message | None = field( default=None, init=False ) @@ -320,7 +307,7 @@ class ResponseAction(abc.ABC): """ @abc.abstractmethod - async def perform(self) -> Optional[Union[dns.message.Message, bytes]]: + async def perform(self) -> dns.message.Message | bytes | None: """ This method is expected to carry out arbitrary actions (e.g. wait for a specific amount of time, modify the answer, etc.) and then return the @@ -343,14 +330,30 @@ class DnsResponseSend(ResponseAction): """ response: dns.message.Message - authoritative: Optional[bool] = None + authoritative: bool | None = None delay: float = 0.0 + acknowledge_hand_rolled_response: bool = False - async def perform(self) -> Optional[Union[dns.message.Message, bytes]]: + async def perform(self) -> dns.message.Message | bytes | None: """ Yield a potentially delayed response that is a dns.message.Message. """ assert isinstance(self.response, dns.message.Message) + if not ( + _is_asyncserver_response(self.response) + or self.acknowledge_hand_rolled_response + ): + error = "The response you are trying to send was not created using " + error += "AsyncDnsServer's response preparation methods. " + error += "This will break features such as automatic AA flag " + error += "and RCODE handling. If you need a fresh copy of a " + error += "response, use `QueryContext.prepare_new_response` " + error += "instead of `dns.message.make_response`. " + error += "To acknowledge this and proceed anyway, set " + error += "`acknowledge_hand_rolled_response=True` in " + error += "DnsResponseSend's constructor." + raise RuntimeError(error) + if self.authoritative is not None: if self.authoritative: self.response.flags |= dns.flags.AA @@ -377,7 +380,7 @@ class BytesResponseSend(ResponseAction): response: bytes delay: float = 0.0 - async def perform(self) -> Optional[Union[dns.message.Message, bytes]]: + async def perform(self) -> dns.message.Message | bytes | None: """ Yield a potentially delayed response that is a sequence of bytes. """ @@ -394,7 +397,7 @@ class ResponseDrop(ResponseAction): Action which does nothing - as if a packet was dropped. """ - async def perform(self) -> Optional[Union[dns.message.Message, bytes]]: + async def perform(self) -> dns.message.Message | bytes | None: return None @@ -403,17 +406,16 @@ class _ConnectionTeardownRequested(Exception): @dataclass -class ResponseDropAndCloseConnection(ResponseAction): +class CloseConnection(ResponseAction): """ - Action which makes the server close the connection after the DNS query is - received by the server (TCP only). + Action which makes the server close the connection (TCP only). The connection may be closed with a delay if requested. """ delay: float = 0.0 - async def perform(self) -> Optional[Union[dns.message.Message, bytes]]: + async def perform(self) -> dns.message.Message | bytes | None: if self.delay > 0: logging.info("Waiting %.1fs before closing TCP connection", self.delay) await asyncio.sleep(self.delay) @@ -495,7 +497,7 @@ class IgnoreAllConnections(ConnectionHandler): client socket, effectively ignoring all incoming connections. """ - _connections: Set[asyncio.StreamWriter] = field(default_factory=set) + _connections: set[asyncio.StreamWriter] = field(default_factory=set) async def handle( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer @@ -529,8 +531,8 @@ class ConnectionReset(ConnectionHandler): make the server send an RST segment; this happens when the server closes a client's socket while there is still unread data in that socket's buffer. If closing the connection _after_ the query is read by the server is enough - for a given use case, the ResponseDropAndCloseConnection response handler - should be used instead. + for a given use case, the CloseConnection response handler should be used + instead. """ delay: float = 0.0 @@ -606,14 +608,14 @@ class QnameHandler(ResponseHandler): @property @abc.abstractmethod - def qnames(self) -> List[str]: + def qnames(self) -> list[str]: """ A list of QNAMEs handled by this class. """ raise NotImplementedError def __init__(self) -> None: - self._qnames: List[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames] + self._qnames: list[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames] def __str__(self) -> str: return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)})" @@ -626,6 +628,105 @@ class QnameHandler(ResponseHandler): return qctx.qname in self._qnames +class QnameQtypeHandler(QnameHandler): + """ + Handle queries for which both of the following conditions are true: + + - the query's QNAME is present in `self.qnames`, + - the query's QTYPE is present in `self.qtypes`. + """ + + @property + @abc.abstractmethod + def qtypes(self) -> list[dns.rdatatype.RdataType]: + """ + A list of QTYPEs handled by this class. + """ + raise NotImplementedError + + def __init__(self) -> None: + super().__init__() + self._qtypes: list[dns.rdatatype.RdataType] = self.qtypes + + def __str__(self) -> str: + return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)}; QTYPEs: {', '.join(map(str, self.qtypes))})" + + def match(self, qctx: QueryContext) -> bool: + """ + Handle queries whose QNAME and QTYPE match any of the QNAMEs and + QTYPEs handled by this class. + """ + return qctx.qtype in self._qtypes and super().match(qctx) + + +class StaticResponseHandler(ResponseHandler): + """ + Base class used for deriving custom static response handlers. + + The derived class can specify the RRsets to be included in the answer, + authority, and additional sections of the response, whether to set the AA + bit in the response, and a delay before sending the response. + + The default implementation of `get_responses()` uses these properties to + prepare and yield a single response. + """ + + @property + def rcode(self) -> dns.rcode.Rcode | None: + """ + Optional RCODE to be set in the response. + """ + return None + + @property + def answer(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the answer section of the response. + """ + return [] + + @property + def authority(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the authority section of the response. + """ + return [] + + @property + def additional(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the additional section of the response. + """ + return [] + + @property + def authoritative(self) -> bool | None: + """ + Whether to set the AA bit in the response. + """ + return None + + @property + def delay(self) -> float: + """ + Delay before sending the response. + """ + return 0.0 + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.prepare_new_response(with_zone_data=False) + qctx.response.answer.extend(self.answer) + qctx.response.authority.extend(self.authority) + qctx.response.additional.extend(self.additional) + if self.rcode is not None: + qctx.response.set_rcode(self.rcode) + yield DnsResponseSend( + qctx.response, authoritative=self.authoritative, delay=self.delay + ) + + class DomainHandler(ResponseHandler): """ Base class used for deriving custom domain handlers. @@ -633,20 +734,28 @@ class DomainHandler(ResponseHandler): The derived class must specify a list of `domains` that it wants to handle. Queries for any of these domains (and their subdomains) will then be passed to the `get_response()` method in the derived class. + + The most specific matching domain is stored in the `matched_domain` attribute. """ @property @abc.abstractmethod - def domains(self) -> List[str]: + def domains(self) -> list[str]: """ A list of domain names handled by this class. """ raise NotImplementedError def __init__(self) -> None: - self._domains: List[dns.name.Name] = [ - dns.name.from_text(d) for d in self.domains - ] + self._domains: list[dns.name.Name] = sorted( + [dns.name.from_text(d) for d in self.domains], reverse=True + ) + self._matched_domain: dns.name.Name | None = None + + @property + def matched_domain(self) -> dns.name.Name: + assert self._matched_domain is not None + return self._matched_domain def __str__(self) -> str: return f"{self.__class__.__name__}(domains: {', '.join(self.domains)})" @@ -656,20 +765,124 @@ class DomainHandler(ResponseHandler): Handle queries whose QNAME matches any of the domains handled by this class. """ + self._matched_domain = None for domain in self._domains: if qctx.qname.is_subdomain(domain): + self._matched_domain = domain return True return False +class ForwarderHandler(ResponseHandler): + """ + A handler forwarding all received queries to another DNS server with an + optional delay and then relaying the responses back to the original client. + + Queries are currently always forwarded via UDP. + """ + + @property + @abc.abstractmethod + def target(self) -> str: + """ + The address of the DNS server to forward queries to. + """ + raise NotImplementedError + + @property + def port(self) -> int: + """ + The port of the DNS server to forward queries to. + + The default value of 0 causes the same port as the one used by this + server for listening to be used. + """ + return 0 + + @property + def delay(self) -> float: + """ + The number of seconds to wait before forwarding each query. + """ + return 0.0 + + def __str__(self) -> str: + return f"{self.__class__.__name__}(target: {self.target}:{self.port})" + + class ForwarderProtocol(asyncio.DatagramProtocol): + def __init__(self, query: bytes, response: asyncio.Future) -> None: + self._query = query + self._response = response + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + logging.debug("[OUT] %s", self._query.hex()) + cast(asyncio.DatagramTransport, transport).sendto(self._query) + + def datagram_received(self, data: bytes, _: tuple[str, int]) -> None: + logging.debug("[IN] %s", data.hex()) + self._response.set_result(data) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + loop = asyncio.get_running_loop() + response = loop.create_future() + forwarding_target = f"{self.target}:{self.port or qctx.socket.port}" + + if self.delay > 0: + logging.info( + "Waiting %.1fs before forwarding %s query from %s to %s over UDP", + self.delay, + qctx.protocol.name, + qctx.peer, + forwarding_target, + ) + await asyncio.sleep(self.delay) + + logging.info( + "Forwarding %s query from %s to %s over UDP", + qctx.protocol.name, + qctx.peer, + forwarding_target, + ) + + transport, _ = await loop.create_datagram_endpoint( + lambda: self.ForwarderProtocol(qctx.query.to_wire(), response), + local_addr=(qctx.socket.host, 0), + remote_addr=(self.target, self.port or qctx.socket.port), + ) + + try: + await response + finally: + transport.close() + + logging.info( + "Relaying UDP response from %s to %s over %s", + forwarding_target, + qctx.peer, + qctx.protocol.name, + ) + + try: + message = _DnsMessageWithTsigDisabled.from_wire(response.result()) + yield DnsResponseSend(message, acknowledge_hand_rolled_response=True) + except dns.exception.DNSException: + logging.warning( + "Failed to parse response from %s as a DNS message, relaying it as raw bytes", + forwarding_target, + ) + yield BytesResponseSend(response.result()) + + @dataclass class _ZoneTreeNode: """ A node representing a zone with one origin. """ - zone: Optional[dns.zone.Zone] - children: List["_ZoneTreeNode"] = field(default_factory=list) + zone: dns.zone.Zone | None + children: list["_ZoneTreeNode"] = field(default_factory=list) class _ZoneTree: @@ -719,7 +932,7 @@ class _ZoneTree: node_from.children.remove(child) node_to.children.append(child) - def find_best_zone(self, name: dns.name.Name) -> Optional[dns.zone.Zone]: + def find_best_zone(self, name: dns.name.Name) -> dns.zone.Zone | None: """ Return the closest matching zone (if any) for the domain name. """ @@ -737,7 +950,7 @@ class _DnsMessageWithTsigDisabled(dns.message.Message): """ class _DisableTsigHandling(contextlib.ContextDecorator): - def __init__(self, message: Optional[dns.message.Message] = None) -> None: + def __init__(self, message: dns.message.Message | None = None) -> None: self.original_tsig_sign = dns.tsig.sign self.original_tsig_validate = dns.tsig.validate if message: @@ -749,7 +962,7 @@ class _DnsMessageWithTsigDisabled(dns.message.Message): from failing on messages initialized with `dns.message.from_wire(keyring=False)`. """ - def sign(*_: Any, **__: Any) -> Tuple[dns.rdata.Rdata, None]: + def sign(*_: Any, **__: Any) -> tuple[dns.rdata.Rdata, None]: assert self.tsig return self.tsig[0], None @@ -792,6 +1005,19 @@ class _NoKeyringType: pass +_ASYNCSERVER_RESPONSE_MARKER = "__is_asyncserver_response__" + + +def _make_asyncserver_response(query: dns.message.Message) -> dns.message.Message: + response = dns.message.make_response(query) + setattr(response, _ASYNCSERVER_RESPONSE_MARKER, True) + return response + + +def _is_asyncserver_response(message: dns.message.Message) -> bool: + return getattr(message, _ASYNCSERVER_RESPONSE_MARKER, False) + + class AsyncDnsServer(AsyncServer): """ DNS server which responds to queries based on zone data and/or custom @@ -812,17 +1038,17 @@ class AsyncDnsServer(AsyncServer): self, /, default_rcode: dns.rcode.Rcode = dns.rcode.REFUSED, - default_aa: bool = True, - keyring: Union[ - Dict[dns.name.Name, dns.tsig.Key], None, _NoKeyringType - ] = _NoKeyringType(), + default_aa: bool = False, + keyring: ( + dict[dns.name.Name, dns.tsig.Key] | None | _NoKeyringType + ) = _NoKeyringType(), acknowledge_manual_dname_handling: bool = False, ) -> None: super().__init__(self._handle_udp, self._handle_tcp, "ans.pid") self._zone_tree: _ZoneTree = _ZoneTree() - self._connection_handler: Optional[ConnectionHandler] = None - self._response_handlers: List[ResponseHandler] = [] + self._connection_handler: ConnectionHandler | None = None + self._response_handlers: list[ResponseHandler] = [] self._default_rcode = default_rcode self._default_aa = default_aa self._keyring = keyring @@ -849,10 +1075,18 @@ class AsyncDnsServer(AsyncServer): else: self._response_handlers.append(handler) - def install_response_handlers(self, handlers: List[ResponseHandler]) -> None: + def install_response_handlers(self, *handlers: ResponseHandler) -> None: for handler in handlers: self.install_response_handler(handler) + def replace_response_handlers(self, *new_handlers: ResponseHandler) -> None: + """ + Uninstall all currently installed handlers and install the provided ones. + """ + logging.info("Uninstalling response handlers: %s", str(self._response_handlers)) + self._response_handlers.clear() + self.install_response_handlers(*new_handlers) + def uninstall_response_handler(self, handler: ResponseHandler) -> None: """ Remove the specified handler from the list of response handlers. @@ -923,11 +1157,13 @@ class AsyncDnsServer(AsyncServer): raise ValueError(error) async def _handle_udp( - self, wire: bytes, addr: Tuple[str, int], transport: asyncio.DatagramTransport + self, wire: bytes, addr: tuple[str, int], transport: asyncio.DatagramTransport ) -> None: logging.debug("Received UDP message: %s", wire.hex()) + socket_info = transport.get_extra_info("sockname") + socket = Peer(socket_info[0], socket_info[1]) peer = Peer(addr[0], addr[1]) - responses = self._handle_query(wire, peer, DnsProtocol.UDP) + responses = self._handle_query(wire, socket, peer, DnsProtocol.UDP) async for response in responses: logging.debug("Sending UDP message: %s", response.hex()) transport.sendto(response, addr) @@ -964,7 +1200,7 @@ class AsyncDnsServer(AsyncServer): async def _read_tcp_query( self, reader: asyncio.StreamReader, peer: Peer - ) -> Optional[bytes]: + ) -> bytes | None: wire_length = await self._read_tcp_query_wire_length(reader, peer) if not wire_length: return None @@ -973,7 +1209,7 @@ class AsyncDnsServer(AsyncServer): async def _read_tcp_query_wire_length( self, reader: asyncio.StreamReader, peer: Peer - ) -> Optional[int]: + ) -> int | None: logging.debug("Receiving TCP message length from %s...", peer) wire_length_bytes = await self._read_tcp_octets(reader, peer, 2) @@ -986,7 +1222,7 @@ class AsyncDnsServer(AsyncServer): async def _read_tcp_query_wire( self, reader: asyncio.StreamReader, peer: Peer, wire_length: int - ) -> Optional[bytes]: + ) -> bytes | None: logging.debug("Receiving TCP message (%d octets) from %s...", wire_length, peer) wire = await self._read_tcp_octets(reader, peer, wire_length) @@ -999,7 +1235,7 @@ class AsyncDnsServer(AsyncServer): async def _read_tcp_octets( self, reader: asyncio.StreamReader, peer: Peer, expected: int - ) -> Optional[bytes]: + ) -> bytes | None: buffer = b"" while len(buffer) < expected: @@ -1024,39 +1260,39 @@ class AsyncDnsServer(AsyncServer): async def _send_tcp_response( self, writer: asyncio.StreamWriter, peer: Peer, wire: bytes ) -> None: - responses = self._handle_query(wire, peer, DnsProtocol.TCP) + socket_info = writer.get_extra_info("sockname") + socket = Peer(socket_info[0], socket_info[1]) + responses = self._handle_query(wire, socket, peer, DnsProtocol.TCP) async for response in responses: logging.debug("Sending TCP response: %s", response.hex()) writer.write(response) await writer.drain() - def _log_query(self, qctx: QueryContext, peer: Peer, protocol: DnsProtocol) -> None: + def _log_query(self, qctx: QueryContext) -> None: logging.info( - "Received %s/%s/%s (ID=%d) query from %s (%s)", + "Received %s/%s/%s (ID=%d) query from %s on %s (%s)", qctx.qname.to_text(omit_final_dot=True), dns.rdataclass.to_text(qctx.qclass), dns.rdatatype.to_text(qctx.qtype), qctx.query.id, - peer, - protocol.name, + qctx.peer, + qctx.socket, + qctx.protocol.name, ) logging.debug( "\n".join([f"[IN] {l}" for l in [""] + str(qctx.query).splitlines()]) ) def _log_response( - self, - qctx: QueryContext, - response: Optional[Union[dns.message.Message, bytes]], - peer: Peer, - protocol: DnsProtocol, + self, qctx: QueryContext, response: dns.message.Message | bytes | None ) -> None: if not response: logging.info( - "Not sending a response to query (ID=%d) from %s (%s)", + "Not sending a response to query (ID=%d) from %s on %s (%s)", qctx.query.id, - peer, - protocol.name, + qctx.peer, + qctx.socket, + qctx.protocol.name, ) return @@ -1071,7 +1307,7 @@ class AsyncDnsServer(AsyncServer): qtype = "-" logging.info( - "Sending %s/%s/%s (ID=%d) response (%d/%d/%d/%d) to a query (ID=%d) from %s (%s)", + "Sending %s/%s/%s (ID=%d) response (%d/%d/%d/%d) to a query (ID=%d) from %s on %s (%s)", qname, qclass, qtype, @@ -1081,8 +1317,9 @@ class AsyncDnsServer(AsyncServer): len(response.authority), len(response.additional), qctx.query.id, - peer, - protocol.name, + qctx.peer, + qctx.socket, + qctx.protocol.name, ) logging.debug( "\n".join([f"[OUT] {l}" for l in [""] + str(response).splitlines()]) @@ -1090,16 +1327,17 @@ class AsyncDnsServer(AsyncServer): return logging.info( - "Sending response (%d bytes) to a query (ID=%d) from %s (%s)", + "Sending response (%d bytes) to a query (ID=%d) from %s on %s (%s)", len(response), qctx.query.id, - peer, - protocol.name, + qctx.peer, + qctx.socket, + qctx.protocol.name, ) logging.debug("[OUT] %s", response.hex()) async def _handle_query( - self, wire: bytes, peer: Peer, protocol: DnsProtocol + self, wire: bytes, socket: Peer, peer: Peer, protocol: DnsProtocol ) -> AsyncGenerator[bytes, None]: """ Yield wire data to send as a response over the established transport. @@ -1109,12 +1347,12 @@ class AsyncDnsServer(AsyncServer): except dns.exception.DNSException as exc: logging.error("Invalid query from %s (%s): %s", peer, wire.hex(), exc) return - response_stub = dns.message.make_response(query) - qctx = QueryContext(query, response_stub, peer, protocol) - self._log_query(qctx, peer, protocol) + response_stub = _make_asyncserver_response(query) + qctx = QueryContext(query, response_stub, socket, peer, protocol) + self._log_query(qctx) responses = self._prepare_responses(qctx) async for response in responses: - self._log_response(qctx, response, peer, protocol) + self._log_response(qctx, response) if response: if isinstance(response, dns.message.Message): response = response.to_wire(max_size=65535) @@ -1146,7 +1384,7 @@ class AsyncDnsServer(AsyncServer): async def _prepare_responses( self, qctx: QueryContext - ) -> AsyncGenerator[Optional[Union[dns.message.Message, bytes]], None]: + ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: """ Yield response(s) either from response handlers or zone data. """ @@ -1339,10 +1577,10 @@ class ControllableAsyncDnsServer(AsyncDnsServer): return dns.name.from_text(self._CONTROL_DOMAIN) @functools.cached_property - def _commands(self) -> Dict[dns.name.Name, "ControlCommand"]: + def _commands(self) -> dict[dns.name.Name, "ControlCommand"]: return {} - def install_control_commands(self, commands: List["ControlCommand"]) -> None: + def install_control_commands(self, *commands: "ControlCommand") -> None: for command in commands: self.install_control_command(command) @@ -1360,7 +1598,7 @@ class ControllableAsyncDnsServer(AsyncDnsServer): async def _prepare_responses( self, qctx: QueryContext - ) -> AsyncGenerator[Optional[Union[dns.message.Message, bytes]], None]: + ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: """ Detect and handle control queries, falling back to normal processing for non-control queries. @@ -1373,9 +1611,7 @@ class ControllableAsyncDnsServer(AsyncDnsServer): async for response in super()._prepare_responses(qctx): yield response - def _handle_control_command( - self, qctx: QueryContext - ) -> Optional[dns.message.Message]: + def _handle_control_command(self, qctx: QueryContext) -> dns.message.Message | None: """ Detect and handle control queries. @@ -1450,8 +1686,8 @@ class ControlCommand(abc.ABC): @abc.abstractmethod def handle( - self, args: List[str], server: ControllableAsyncDnsServer, qctx: QueryContext - ) -> Optional[str]: + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: """ This method is expected to carry out arbitrary actions in response to a control query. Note that it is invoked synchronously (it is not a @@ -1489,11 +1725,11 @@ class ToggleResponsesCommand(ControlCommand): control_subdomain = "send-responses" def __init__(self) -> None: - self._current_handler: Optional[IgnoreAllQueries] = None + self._current_handler: IgnoreAllQueries | None = None def handle( - self, args: List[str], server: ControllableAsyncDnsServer, qctx: QueryContext - ) -> Optional[str]: + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: if len(args) != 1: logging.error("Invalid %s query %s", self, qctx.qname) qctx.response.set_rcode(dns.rcode.SERVFAIL) @@ -1518,3 +1754,30 @@ class ToggleResponsesCommand(ControlCommand): logging.error("Unrecognized response sending mode '%s'", mode) qctx.response.set_rcode(dns.rcode.SERVFAIL) return f"unrecognized response sending mode '{mode}'" + + +class SwitchControlCommand(ControlCommand): + """ + Switch the server's response handlers based on the control query. + + A sequence of response handlers is associated with each key. When a + control query is received, the server's response handlers are replaced + with the sequence associated with the key extracted from the control + query. + """ + + control_subdomain = "switch" + + def __init__(self, handler_mapping: dict[str, Sequence[ResponseHandler]]): + self._handler_mapping = handler_mapping + + def handle( + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: + if len(args) != 1 or args[0] not in self._handler_mapping: + logging.error("Invalid %s query %s", self, qctx.qname) + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return f"invalid query; exactly one of {list(self._handler_mapping.keys())} is expected in QNAME" + + server.replace_response_handlers(*self._handler_mapping[args[0]]) + return f"switched to handler set '{args[0]}'" diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index 0a3b199ea4..7437484e80 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -42,6 +42,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/isctest/query.py b/bin/tests/system/isctest/query.py index a002f626df..4f4e0a7c45 100644 --- a/bin/tests/system/isctest/query.py +++ b/bin/tests/system/isctest/query.py @@ -106,6 +106,7 @@ def create( qtype, qclass=dns.rdataclass.IN, dnssec: bool = True, + rd: bool = True, cd: bool = False, ad: bool = True, ) -> dns.message.Message: @@ -113,7 +114,9 @@ def create( msg = dns.message.make_query( qname, qtype, qclass, use_edns=True, want_dnssec=dnssec ) - msg.flags = dns.flags.RD + msg.flags = 0 + if rd: + msg.flags = dns.flags.RD if ad: msg.flags |= dns.flags.AD if cd: diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh index 4ee045facc..4402329e3b 100644 --- a/bin/tests/system/nsupdate/setup.sh +++ b/bin/tests/system/nsupdate/setup.sh @@ -61,6 +61,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 04cb7369eb..0500e5e951 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -340,8 +340,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 ] || { @@ -352,20 +354,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..217bae0301 --- /dev/null +++ b/bin/tests/system/resend_loop/ans3/ans.py @@ -0,0 +1,126 @@ +# 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.rdatatype +import dns.rrset + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + QueryContext, + ResponseHandler, +) + + +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 + + +class PrimeHandler(ResponseHandler): + """ + Specifically handle priming query for "." NS (type 2) + """ + + def match(self, qctx: QueryContext) -> bool: + return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + + ns_rrset = dns.rrset.from_text( + ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil." + ) + a_rrset = dns.rrset.from_text( + "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3" + ) + + response = qctx.prepare_new_response(with_zone_data=False) + response.set_rcode(dns.rcode.NOERROR) + response.answer.append(ns_rrset) + response.additional.append(a_rrset) + + yield DnsResponseSend(response, authoritative=True) + + +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( + PrimeHandler(), + 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..360bc12e17 --- /dev/null +++ b/bin/tests/system/resend_loop/ns4/named.conf.j2 @@ -0,0 +1,16 @@ +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; }; + 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..f7ed4d3da6 --- /dev/null +++ b/bin/tests/system/resend_loop/tests_resend_loop.py @@ -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. + +import dns.message + +import isctest + + +def test_resend_loop_badcookie(ns4): + expected_log = "exceeded max queries resolving 'test.example/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_line(expected_log) + + 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 2864aae950..db5a18680d 100755 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -979,10 +979,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..5935fd841c --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns2/tld.db @@ -0,0 +1,27 @@ +; 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 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..83ea4d37ec --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/example.tld.db @@ -0,0 +1,155 @@ +; 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 +; Those RR (same below) pointing to 127.0.0.1 won't ever be used as they +; exceeded the ADB limit. +ns01.sub.example.tld. A 127.0.0.1 + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +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 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/named.conf.j2 b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 new file mode 100644 index 0000000000..b5c8bfcf33 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns3/named.conf.j2 @@ -0,0 +1,44 @@ +/* + * 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"; +}; 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..071508fd70 --- /dev/null +++ b/bin/tests/system/selfpointedglue/ns4/named.args.j2 @@ -0,0 +1,3 @@ +{% set adblimit = adblimit | default("") %} + +-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 @adblimit@ 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/prereq.sh b/bin/tests/system/selfpointedglue/prereq.sh new file mode 100644 index 0000000000..747f448982 --- /dev/null +++ b/bin/tests/system/selfpointedglue/prereq.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# 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. + +. ../conf.sh + +$FEATURETEST --enable-dnstap || { + echo_i "This test requires dnstap support." >&2 + exit 255 +} +exit 0 diff --git a/bin/tests/system/selfpointedglue/tests_selfpointedglue.py b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py new file mode 100644 index 0000000000..03a71cf545 --- /dev/null +++ b/bin/tests/system/selfpointedglue/tests_selfpointedglue.py @@ -0,0 +1,75 @@ +# 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 + + +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, nsid, expectedlen): + ns.rndc("dnstap -roll 1") + path = os.path.join(nsid, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [os.getenv("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 test_selfpointedglue(ns4): + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + ips_and_queries = extract_dnstap(ns4, "ns4", 10) + + # Thanks to the de-duplication, only the first 6 NS IPs are + # queried (once sub.example.tld. NS is found) instead of 60 + # (60 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"), + ], + 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..f7c6f8e71b --- /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..5f61e19cd5 --- /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..2e12b0ba7d --- /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..b40306908c --- /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/prereq.sh b/bin/tests/system/srtt/prereq.sh new file mode 100644 index 0000000000..747f448982 --- /dev/null +++ b/bin/tests/system/srtt/prereq.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# 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. + +. ../conf.sh + +$FEATURETEST --enable-dnstap || { + echo_i "This test requires dnstap support." >&2 + exit 255 +} +exit 0 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..55611922a7 --- /dev/null +++ b/bin/tests/system/srtt/tests_srtt.py @@ -0,0 +1,86 @@ +# 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 + + +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, nsid): + ns.rndc("dnstap -roll 1") + path = os.path.join(nsid, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [os.getenv("DNSTAPREAD"), path], + ) + + lines = dnstapread.out.splitlines() + return map(line_to_dst_ips, lines) + + +def assert_used_auth(ns, nsid, authip): + ips = extract_dnstap(ns, nsid) + 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, "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, "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, "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, "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/tkeyleak/prereq.sh b/bin/tests/system/tkeyleak/prereq.sh new file mode 100644 index 0000000000..8a68ae7df1 --- /dev/null +++ b/bin/tests/system/tkeyleak/prereq.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# 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. + +. ../conf.sh + +$FEATURETEST --gssapi || { + echo_i "gssapi not supported - skipping tkeyleak test" + exit 255 +} + +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.in b/bin/tests/system/xferquota/ns1/named.conf.in index e868318ae6..922e6ab3a6 100644 --- a/bin/tests/system/xferquota/ns1/named.conf.in +++ b/bin/tests/system/xferquota/ns1/named.conf.in @@ -22,6 +22,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 230135cd52..bc5e23caf7 100644 --- a/bin/tests/system/xferquota/tests_xferquota.py +++ b/bin/tests/system/xferquota/tests_xferquota.py @@ -10,6 +10,7 @@ # information regarding copyright ownership. import glob +import multiprocessing import os import re from re import compile as Re @@ -18,6 +19,8 @@ import signal import time import dns.message +import dns.query +import dns.zone import pytest import isctest @@ -59,6 +62,9 @@ def test_xferquota(named_port, servers): 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, servers): with servers["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 7100beb402..874f9d2a0c 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.18.49.rst .. include:: ../changelog/changelog-9.18.48.rst .. include:: ../changelog/changelog-9.18.47.rst .. include:: ../changelog/changelog-9.18.46.rst diff --git a/doc/arm/notes.rst b/doc/arm/notes.rst index 8f069eab60..223a49bb2f 100644 --- a/doc/arm/notes.rst +++ b/doc/arm/notes.rst @@ -45,6 +45,7 @@ The list of known issues affecting the latest version in the 9.18 branch can be found at https://gitlab.isc.org/isc-projects/bind9/-/wikis/Known-Issues-in-BIND-9.18 +.. include:: ../notes/notes-9.18.49.rst .. include:: ../notes/notes-9.18.48.rst .. include:: ../notes/notes-9.18.47.rst .. include:: ../notes/notes-9.18.46.rst diff --git a/doc/changelog/changelog-9.18.49.rst b/doc/changelog/changelog-9.18.49.rst new file mode 100644 index 0000000000..247431adb0 --- /dev/null +++ b/doc/changelog/changelog-9.18.49.rst @@ -0,0 +1,226 @@ +.. 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.18.49 +------------ + +Security Fixes +~~~~~~~~~~~~~~ + +- Fix outgoing zone transfers' quota issue. ``694648e14b`` + + 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. ``5abfbc2663`` + + 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. ``03ce21cf30`` + + 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. + + Also implemented missing RFC 3645 requirement: the client now verifies + that mutual authentication and integrity flags are granted by the + GSS-API mechanism (Section 3.1.1). :gl:`#5752` + +- [CVE-2026-5950] Avoid unbounded recursion loop. ``43d173797e`` + + 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-5946] Disable recursion, UPDATE, and NOTIFY for non-IN + views. ``7ce6ce37b1`` + + 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. + +Feature Changes +~~~~~~~~~~~~~~~ + +- Revert isdelegation() to return boolean value again. ``83e5e8c4d0`` + + :gl:`#5838` :gl:`!11803` + +- Fix CPU spikes and slow queries when cache approaches memory limit. + ``874a19c71b`` + + When the cache grew close to the configured max-cache-size, every + subsequent entry triggered all worker threads to run cache cleanup at + once, causing CPU spikes and a drop in query throughput. Cleanup is + now spread probabilistically across inserts as memory approaches the + limit, so the work is distributed evenly instead of piling up at the + threshold. + +Bug Fixes +~~~~~~~~~ + +- Fix named crash when processing SIG records in dynamic updates. + ``df77c239ac`` + + 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:`!11877` + +- Fix zone verification of NSEC3 signed zones. ``3a2e16ae65`` + + 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:`!11834` + +- Prevent a crash when using both dns64 and filter-aaaa. ``891d055efc`` + + 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:`!11968` + +- Remove unnecessary dns_name_free call. ``46aa4fd08d`` + + 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:`!11849` + +- Tidy up the cleanup path in check_signer() ``03af408476`` + + When check_signer() processed a DNSKEY whose public-key data could not + be parsed, the early return on the parse error skipped the cleanup of + the cloned signature rdataset. In every code path that currently + reaches this function the cloned rdataset holds no resources, so no + memory was actually leaked, but the cleanup is restructured so the + parse and the iteration cannot diverge again. :gl:`#5869` :gl:`!11960` + +- Prevent malicious DNSSEC zones from exhausting validator CPU. + ``784725ef85`` + + 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:`!11924` + +- Fix inverted gethostname() check in rndc status. ``c874e39a23`` + + 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:`!11883` + +- Fix rndc-confgen aborting on HMAC-SHA-384/512 keys above 512 bits. + ``739e79592d`` + + `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:`!11911` + +- Prevent crafted queries from degrading RRL performance. ``e81855244d`` + + 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:`!11953` + +- Fix swapped arguments in redirect2() single-label branch. + ``9a969bf1bc`` + + 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:`!11914` + +- Fix a bug in allow-query/allow-transfer catalog zone custom + properties. ``9e5a52e6fa`` + + 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:`!11976` + +- Fix a memory leak issue in the catalog zones. ``0b5874d3e1`` + + 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:`!11974` + +- Fix suppressed missing-glue check in named-checkzone. ``598277fe03`` + + 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:`!11906` + +- Pass empty string instead of NULL to ns_client_dumpmessage() + ``d489d825dc`` + + Pass "" instead of NULL to ns_client_dumpmessage() to get the log + message printed. + +- Reject record sets too large to serve in DNS. ``ab3d96b3e3`` + + 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:`!11965` + + diff --git a/doc/notes/notes-9.18.49.rst b/doc/notes/notes-9.18.49.rst new file mode 100644 index 0000000000..3bd35d7bff --- /dev/null +++ b/doc/notes/notes-9.18.49.rst @@ -0,0 +1,192 @@ +.. 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.18.49 +---------------------- + +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 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` + +Bug Fixes +~~~~~~~~~ + +- 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 :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` + +- 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` + +- 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` + +- 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` + +- 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 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` + + diff --git a/lib/bind9/check.c b/lib/bind9/check.c index cefc3eb3ac..13f3212d08 100644 --- a/lib/bind9/check.c +++ b/lib/bind9/check.c @@ -2789,13 +2789,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, isc_log_t *logctx, - cfg_aclconfctx_t *actx, isc_mem_t *mctx) { + dns_rdataclass_t vclass, const cfg_obj_t *goptions, + isc_log_t *logctx, cfg_aclconfctx_t *actx, isc_mem_t *mctx) { dns_acl_t *acl = NULL; const cfg_obj_t *obj; isc_result_t result; bool retval = true; + if (vclass != dns_rdataclass_in) { + return false; + } + /* * Check the "recursion" option first. */ @@ -3380,7 +3384,8 @@ 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, logctx, actx, mctx)) + !check_recursion(config, voptions, zclass, goptions, logctx, actx, + mctx)) { cfg_obj_log(zoptions, logctx, ISC_LOG_ERROR, "zone '%s': mirror zones cannot be used if " @@ -5215,6 +5220,17 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, cfg_aclconfctx_create(mctx, &actx); + if (vclass != dns_rdataclass_in) { + if (check_recursion(config, voptions, dns_rdataclass_in, + options, logctx, actx, mctx)) + { + cfg_obj_log(opts, logctx, 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/dns/adb.c b/lib/dns/adb.c index 955ecc09f5..21ae217a38 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -86,6 +86,15 @@ #define DNS_ADB_MINADBSIZE (1024U * 1024U) /*%< 1 Megabyte */ +/* + * Default and override for the per-find address limit, the sum of the number of + * A and AAAA RR from an ADB NS name resolution. When non-zero, this value is + * used instead of the default. Can be set via 'named -T adbaddrslimit=N' for + * testing. + */ +#define DEFAULT_ADDRSLIMIT 6 +size_t dns_adb_addrslimit = 0; + typedef ISC_LIST(dns_adbname_t) dns_adbnamelist_t; typedef struct dns_adbnamehook dns_adbnamehook_t; typedef ISC_LIST(dns_adbnamehook_t) dns_adbnamehooklist_t; @@ -940,7 +949,7 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, INSIST(DNS_ADB_VALID(adb)); rdtype = rdataset->type; - INSIST((rdtype == dns_rdatatype_a) || (rdtype == dns_rdatatype_aaaa)); + REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa); addr_bucket = DNS_ADB_INVALIDBUCKET; new_addresses_added = false; @@ -2200,6 +2209,9 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_adbaddrinfo_t *addrinfo; dns_adbentry_t *entry; int bucket; + size_t count = 0; + size_t limit = dns_adb_addrslimit != 0 ? dns_adb_addrslimit + : DEFAULT_ADDRSLIMIT; bucket = DNS_ADB_INVALIDBUCKET; @@ -2232,6 +2244,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, inc_entry_refcnt(adb, entry, false); ISC_LIST_APPEND(find->list, addrinfo, publink); addrinfo = NULL; + + if (++count >= limit) { + DP(ISC_LOG_DEBUG(3), "skipping addresses"); + UNLOCK(&adb->entrylocks[bucket]); + return; + } + nextv4: UNLOCK(&adb->entrylocks[bucket]); bucket = DNS_ADB_INVALIDBUCKET; @@ -2267,6 +2286,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, inc_entry_refcnt(adb, entry, false); ISC_LIST_APPEND(find->list, addrinfo, publink); addrinfo = NULL; + + if (++count >= limit) { + DP(ISC_LOG_DEBUG(3), "skipping addresses"); + UNLOCK(&adb->entrylocks[bucket]); + return; + } + nextv6: UNLOCK(&adb->entrylocks[bucket]); bucket = DNS_ADB_INVALIDBUCKET; diff --git a/lib/dns/gssapictx.c b/lib/dns/gssapictx.c index 60fac791a0..4109a4f975 100644 --- a/lib/dns/gssapictx.c +++ b/lib/dns/gssapictx.c @@ -604,7 +604,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", @@ -629,12 +636,6 @@ 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); @@ -645,7 +646,7 @@ cleanup: isc_result_t dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, - isc_region_t *intoken, isc_buffer_t **outtoken, + isc_region_t *intoken, isc_buffer_t **outtokenp, dns_gss_ctx_id_t *ctxout, dns_name_t *principal, isc_mem_t *mctx) { isc_region_t r; @@ -658,16 +659,11 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, 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); @@ -712,8 +708,15 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, switch (gret) { case GSS_S_COMPLETE: - case GSS_S_CONTINUE_NEEDED: 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: @@ -726,7 +729,7 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, 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))); @@ -737,50 +740,55 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, } 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))); - CHECK(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, - NULL)); - } 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))); + result = ISC_R_FAILURE; + goto cleanup; } + /* + * 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, NULL)); + *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/dst/gssapi.h b/lib/dns/include/dst/gssapi.h index 494b4b0762..5945bf7637 100644 --- a/lib/dns/include/dst/gssapi.h +++ b/lib/dns/include/dst/gssapi.h @@ -113,20 +113,17 @@ dst_gssapi_acceptctx(dns_gss_cred_id_t cred, const char *gssapi_keytab, * 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. - * Call dst_gssapi_releasecred to delete the context and free - * the memory. * * 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 541a854db0..38f5640500 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -1080,6 +1080,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/resolver.c b/lib/dns/resolver.c index d6ae4478f1..edc9c207e1 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -369,7 +369,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 @@ -1534,7 +1543,7 @@ fctx_cleanup(fetchctx_t *fctx) { dns_adb_destroyfind(&find); fctx_unref(fctx); } - fctx->find = NULL; + fctx->foundaddrinfo = NULL; for (find = ISC_LIST_HEAD(fctx->altfinds); find != NULL; find = next_find) @@ -3338,91 +3347,10 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo, } /* - * 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; - unsigned int best_srtt, curr_srtt; - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(find->list)) { - 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) { - 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, *curr; - dns_adbfindlist_t sorted; - dns_adbaddrinfo_t *addrinfo, *bestaddrinfo; - unsigned int best_srtt, curr_srtt; - - /* Sort each find's addrinfo list by SRTT. */ - for (curr = ISC_LIST_HEAD(*findlist); curr != NULL; - curr = ISC_LIST_NEXT(curr, publink)) - { - sort_adbfind(curr, bias); - } - - /* Lame N^2 bubble sort. */ - ISC_LIST_INIT(sorted); - while (!ISC_LIST_EMPTY(*findlist)) { - 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) { - 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 a pending fetch for 'type'. This is - * used to find out whether we're in a loop, where a fetch is waiting for a - * find which is waiting for that same fetch. + * 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 + * find which is waiting for that same fetch. So if the current find actually + * started the fetch, we know it can't be a loop, so we returns false. * * Note: This could be done with either an equivalence check (e.g., * query_pending == DNS_ADBFIND_INET) or with a bit check, as below. If @@ -3529,6 +3457,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 { @@ -3944,8 +3873,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); result = ISC_R_SUCCESS; } @@ -4020,6 +3947,80 @@ 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). + */ + for (dns_adbfind_t *find = ISC_LIST_HEAD(fctx->finds); find != NULL; + find = ISC_LIST_NEXT(find, publink)) + { + for (dns_adbaddrinfo_t *ai = ISC_LIST_HEAD(find->list); + ai != NULL; ai = ISC_LIST_NEXT(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, *start; @@ -4042,7 +4043,6 @@ fctx_nextaddress(fetchctx_t *fctx) { possibly_mark(fctx, addrinfo); if (UNMARKED(addrinfo)) { addrinfo->flags |= FCTX_ADDRINFO_MARK; - fctx->find = NULL; fctx->forwarding = true; /* @@ -4063,49 +4063,9 @@ 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. - */ - addrinfo = NULL; - if (find != NULL) { - start = find; - do { - for (addrinfo = ISC_LIST_HEAD(find->list); - addrinfo != NULL; - addrinfo = ISC_LIST_NEXT(addrinfo, publink)) - { - if (!UNMARKED(addrinfo)) { - continue; - } - possibly_mark(fctx, addrinfo); - if (UNMARKED(addrinfo)) { - addrinfo->flags |= FCTX_ADDRINFO_MARK; - break; - } - } - if (addrinfo != NULL) { - break; - } - find = ISC_LIST_NEXT(find, publink); - if (find == NULL) { - find = ISC_LIST_HEAD(fctx->finds); - } - } while (find != start); - } - - fctx->find = find; - if (addrinfo != NULL) { - return addrinfo; + faddrinfo = nextaddress(fctx); + if (faddrinfo != NULL) { + return faddrinfo; } /* @@ -4186,6 +4146,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_lctx, 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_lctx, 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, bool badcache) { isc_result_t result; @@ -4340,31 +4333,11 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) { return; } - result = isc_counter_increment(fctx->qc); - if (result != ISC_R_SUCCESS) { - isc_log_write(dns_lctx, 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)); - fctx_done_detach(&fctx, DNS_R_SERVFAIL); - return; - } - - if (fctx->gqc != NULL) { - result = isc_counter_increment(fctx->gqc); - if (result != ISC_R_SUCCESS) { - isc_log_write(dns_lctx, 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)); - fctx_done_detach(&fctx, DNS_R_SERVFAIL); - return; - } - } + CHECK(incr_query_counters(fctx)); result = fctx_query(fctx, addrinfo, fctx->options); + +cleanup: if (result != ISC_R_SUCCESS) { fctx_done_detach(&fctx, result); } else if (retrying) { @@ -7343,6 +7316,13 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, } } + /* + * 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 @@ -10075,9 +10055,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) { @@ -10085,8 +10065,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_done_detach(&rctx->fctx, result); } diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c index 8aa4c076d3..072c85e984 100644 --- a/lib/dns/tkey.c +++ b/lib/dns/tkey.c @@ -528,19 +528,9 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, return ISC_R_SUCCESS; } - /* - * XXXDCL need to check for key expiry per 4.1.1 - * XXXDCL need a way to check fully established, perhaps w/key_flags - */ - intoken.base = tkeyin->key; intoken.length = tkeyin->keylen; - result = dns_tsigkey_find(&tsigkey, name, &tkeyin->algorithm, ring); - if (result == ISC_R_SUCCESS) { - gss_ctx = dst_key_getgssctx(tsigkey->key); - } - principal = dns_fixedname_initname(&fixed); /* @@ -549,28 +539,27 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, result = dst_gssapi_acceptctx(tctx->gsscred, 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"); /* XXXSRA - */ - return ISC_R_SUCCESS; - } - if (result != DNS_R_CONTINUE && result != ISC_R_SUCCESS) { - CHECK(result); + tkey_log("process_gsstkey(): dns_tsigerror_badkey"); + result = ISC_R_SUCCESS; + goto cleanup; } + /* - * 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. */ isc_stdtime_get(&now); if (dns_name_countlabels(principal) == 0U) { - if (tsigkey != NULL) { - dns_tsigkey_detach(&tsigkey); - } + tkeyout->error = dns_tsigerror_badkey; + tkey_log("process_gsstkey(): " + "completed context with empty principal"); + result = ISC_R_SUCCESS; + goto cleanup; } else if (tsigkey == NULL) { #if HAVE_GSSAPI OM_uint32 gret, minor, lifetime; @@ -633,6 +622,9 @@ process_gsstkey(dns_message_t *msg, dns_name_t *name, dns_rdata_tkey_t *tkeyin, return ISC_R_SUCCESS; cleanup: + if (dstkey == NULL && gss_ctx != NULL) { + dst_gssapi_deletectx(tctx->mctx, &gss_ctx); + } if (tsigkey != NULL) { dns_tsigkey_detach(&tsigkey); } @@ -645,9 +637,9 @@ cleanup: isc_buffer_free(&outtoken); } - tkey_log("process_gsstkey(): %s", isc_result_totext(result)); /* XXXSRA - */ - + if (result != ISC_R_SUCCESS) { + tkey_log("process_gsstkey(): %s", isc_result_totext(result)); + } return result; } @@ -1546,9 +1538,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( diff --git a/lib/isc/mem.c b/lib/isc/mem.c index 706fb1ca8c..f2b82de7eb 100644 --- a/lib/isc/mem.c +++ b/lib/isc/mem.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +152,6 @@ struct isc_mem { atomic_size_t malloced; atomic_size_t maxmalloced; atomic_bool hi_called; - atomic_bool is_overmem; isc_mem_water_t water; void *water_arg; atomic_size_t hi_water; @@ -534,7 +534,6 @@ mem_create(isc_mem_t **ctxp, unsigned int flags, unsigned int jemalloc_flags) { atomic_init(&ctx->hi_water, 0); atomic_init(&ctx->lo_water, 0); atomic_init(&ctx->hi_called, false); - atomic_init(&ctx->is_overmem, false); for (size_t i = 0; i < STATS_BUCKETS + 1; i++) { atomic_init(&ctx->stats[i].gets, 0); @@ -786,9 +785,6 @@ hi_water(isc_mem_t *ctx) { return false; } - /* We are over water (for the first time) */ - atomic_store_release(&ctx->is_overmem, true); - return true; } @@ -810,9 +806,6 @@ lo_water(isc_mem_t *ctx) { return false; } - /* We are no longer overmem */ - atomic_store_release(&ctx->is_overmem, false); - return true; } @@ -1195,7 +1188,30 @@ bool isc_mem_isovermem(isc_mem_t *ctx) { REQUIRE(VALID_CONTEXT(ctx)); - return atomic_load_relaxed(&ctx->is_overmem); + size_t hiwater = atomic_load_relaxed(&ctx->hi_water); + if (hiwater == 0) { + return false; + } + + size_t inuse = atomic_load_relaxed(&ctx->inuse); + 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; } void diff --git a/lib/ns/client.c b/lib/ns/client.c index 97e99019a4..88327fbc7b 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -2083,7 +2084,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->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && client->message->opcode == dns_opcode_query && client->message->counts[DNS_SECTION_QUESTION] == 0U) @@ -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; } @@ -2140,7 +2177,7 @@ ns__client_request(isc_nmhandle_t *handle, isc_result_t eresult, 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"); + ns_client_dumpmessage(client, ""); ns_client_extendederror(client, DNS_EDE_PROHIBITED, NULL); ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_REFUSED); return; @@ -2337,6 +2374,10 @@ ns__client_request(isc_nmhandle_t *handle, isc_result_t eresult, break; case dns_opcode_update: CTRACE("update"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } #ifdef HAVE_DNSTAP dns_dt_send(client->view, DNS_DTTYPE_UQ, &client->peeraddr, &client->destsockaddr, TCP_CLIENT(client), NULL, @@ -2347,6 +2388,10 @@ ns__client_request(isc_nmhandle_t *handle, isc_result_t eresult, break; case dns_opcode_notify: CTRACE("notify"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } ns_client_settimeout(client, 60); ns_notify_start(client, handle); break; @@ -2773,7 +2818,7 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) { int len = 1024; isc_result_t result; - if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1))) { + if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1)) || reason == NULL) { return; } diff --git a/lib/ns/update.c b/lib/ns/update.c index 415836fd13..d002019fdf 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -999,7 +999,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; @@ -1354,7 +1356,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 @@ -1497,8 +1502,7 @@ cleanup: * 'rdata', and 'ttl', respectively. */ static void -get_current_rr(dns_message_t *msg, dns_section_t section, - dns_rdataclass_t zoneclass, dns_name_t **name, +get_current_rr(dns_message_t *msg, dns_section_t section, 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; @@ -1514,7 +1518,7 @@ get_current_rr(dns_message_t *msg, dns_section_t section, 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; } /*% @@ -1616,7 +1620,6 @@ send_update_event(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; @@ -1626,10 +1629,12 @@ send_update_event(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); 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, @@ -1680,13 +1685,13 @@ send_update_event(ns_client_t *client, dns_zone_t *zone) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &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 @@ -1776,7 +1781,6 @@ send_update_event(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)) { @@ -2860,7 +2864,6 @@ update_action(isc_task_t *task, isc_event_t *event) { isc_mem_t *mctx = client->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; @@ -2880,9 +2883,9 @@ update_action(isc_task_t *task, isc_event_t *event) { 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); /* * Get old and new versions now that queryacl has been checked. */ @@ -2903,8 +2906,8 @@ update_action(isc_task_t *task, isc_event_t *event) { dns_rdataclass_t update_class; bool flag; - get_current_rr(request, DNS_SECTION_PREREQUISITE, zoneclass, - &name, &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_PREREQUISITE, &name, &rdata, + &covers, &ttl, &update_class); if (ttl != 0) { PREREQFAILC(DNS_R_FORMERR, @@ -2967,7 +2970,7 @@ update_action(isc_task_t *task, isc_event_t *event) { "prerequisite not satisfied"); } } - } else if (update_class == zoneclass) { + } else if (update_class == dns_rdataclass_in) { /* "temp += rr;" */ result = temp_append(&temp, name, &rdata); if (result != ISC_R_SUCCESS) { @@ -3029,10 +3032,10 @@ update_action(isc_task_t *task, isc_event_t *event) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &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 d022a68999..bc936e1ed0 100644 --- a/lib/ns/xfrout.c +++ b/lib/ns/xfrout.c @@ -757,16 +757,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_attach(&client->sctx->xfroutquota, "a); - if (result != ISC_R_SUCCESS) { - isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, - "%s request denied: %s", mnemonic, - isc_result_totext(result)); - goto cleanup; - } /* * Interpret the question section. @@ -938,6 +928,18 @@ 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_attach(&client->sctx->xfroutquota, "a); + if (result != ISC_R_SUCCESS) { + isc_log_write(XFROUT_COMMON_LOGARGS, ISC_LOG_WARNING, + "%s request denied: %s", mnemonic, + isc_result_totext(result)); + goto cleanup; + } + /* * Look up the requesting server in the peer table. */ @@ -1194,6 +1196,7 @@ cleanup: } /* XXX kludge */ if (xfr != NULL) { + /* The quota will be released in xfrout_ctx_destroy(). */ xfrout_fail(xfr, result, "setting up zone transfer"); } else if (result != ISC_R_SUCCESS) { ns_client_log(client, DNS_LOGCATEGORY_XFER_OUT, diff --git a/tests/dns/rbtdb_test.c b/tests/dns/rbtdb_test.c index cfc2a5779b..451d5afed0 100644 --- a/tests/dns/rbtdb_test.c +++ b/tests/dns/rbtdb_test.c @@ -307,7 +307,6 @@ ISC_RUN_TEST_IMPL(overmempurge_bigrdata) { for (i = 0; !isc_mem_isovermem(mctx2) && i < (maxcache / 10); i++) { overmempurge_addrdataset(db, now, i, 50053, 0, false); } - assert_true(isc_mem_isovermem(mctx2)); /* * Then try to add the same number of entries, each has very large data. @@ -353,7 +352,6 @@ ISC_RUN_TEST_IMPL(overmempurge_longname) { for (i = 0; !isc_mem_isovermem(mctx2) && i < (maxcache / 10); i++) { overmempurge_addrdataset(db, now, i, 50053, 0, false); } - assert_true(isc_mem_isovermem(mctx2)); /* * Then try to add the same number of entries, each has very large data. diff --git a/tests/isc/mem_test.c b/tests/isc/mem_test.c index c2540df729..ca23960880 100644 --- a/tests/isc/mem_test.c +++ b/tests/isc/mem_test.c @@ -290,6 +290,57 @@ ISC_RUN_TEST_IMPL(isc_mem_reget) { isc_mem_put(mctx, data, REGET_SHRINK_SIZE); } +static bool +at_least_one_overmem(isc_mem_t *omctx) { + for (size_t i = 0; i < UINT16_MAX; i++) { + /* The overmem is probability based in this range */ + if (isc_mem_isovermem(omctx)) { + return true; + } + } + return false; +} + +static void +water(void *arg, int mark) { + UNUSED(arg); + UNUSED(mark); +} + +ISC_RUN_TEST_IMPL(isc_mem_overmem) { + isc_mem_t *omctx = NULL; + isc_mem_create(&omctx); + assert_non_null(omctx); + + isc_mem_setwater(omctx, water, NULL, 1024, 512); + + /* inuse <= lo_water is always false */ + void *data1 = isc_mem_allocate(omctx, 256); + assert_false(isc_mem_isovermem(omctx)); + + /* lo_water < inuse < hi_water might be true or false */ + void *data2 = isc_mem_allocate(omctx, 512); + assert_true(at_least_one_overmem(omctx)); + + /* hi_water <= inuse is always true */ + void *data3 = isc_mem_allocate(omctx, 512); + assert_true(isc_mem_isovermem(omctx)); + + /* lo_water < inuse < hi_water might be true or false */ + isc_mem_free(omctx, data2); + assert_true(at_least_one_overmem(omctx)); + + /* inuse <= lo_water is always false */ + isc_mem_free(omctx, data3); + assert_false(isc_mem_isovermem(omctx)); + + /* inuse == 0 is always false */ + isc_mem_free(omctx, data1); + assert_false(isc_mem_isovermem(omctx)); + + isc_mem_destroy(&omctx); +} + #if ISC_MEM_TRACKLINES /* test mem with no flags */ @@ -501,6 +552,7 @@ ISC_TEST_ENTRY(isc_mem_total) ISC_TEST_ENTRY(isc_mem_inuse) ISC_TEST_ENTRY(isc_mem_zeroget) ISC_TEST_ENTRY(isc_mem_reget) +ISC_TEST_ENTRY(isc_mem_overmem) #if !defined(__SANITIZE_THREAD__) ISC_TEST_ENTRY(isc_mem_benchmark)