Merge tag 'v9.18.49' into bind-9.18
Some checks are pending
CodeQL / Analyze (push) Waiting to run
SonarCloud / Build and analyze (push) Waiting to run

This commit is contained in:
Andoni Duarte 2026-05-20 10:18:26 +00:00
commit ec35c78729
78 changed files with 3179 additions and 466 deletions

View file

@ -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);
}

View file

@ -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));

View file

@ -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))

View file

@ -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

View file

@ -0,0 +1,12 @@
options {
directory ".";
};
view chaos ch {
match-clients { any; };
recursion yes;
zone "." {
type hint;
file "chaos.hints";
};
};

View file

@ -0,0 +1,4 @@
. CH NS ns.root.
ns.root. CH A ns.root. 1
ns.root. CH AAAA \# 1 00

View file

@ -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";
};
};

View file

@ -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"

View file

@ -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

View file

@ -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; };
};
};

View file

@ -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; };
};

View file

@ -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

View file

@ -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)

View file

@ -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=<zone>, 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")

View file

@ -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]}'"

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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 ] || {

View file

@ -40,6 +40,7 @@
# -p <port>: specify port
# -t <protocol>: specify UDP or TCP
# -r <num>: send packet <num> 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 <repeats>] [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);

View file

@ -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()

View file

@ -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";
};

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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";
};

View file

@ -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

View file

@ -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";
};

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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";
};

View file

@ -0,0 +1,3 @@
{% set adblimit = adblimit | default("") %}
-D selfpointedglue-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 @adblimit@

View file

@ -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; };
};

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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";
};

View file

@ -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

View file

@ -0,0 +1 @@
-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4

View file

@ -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";
};

View file

@ -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

View file

@ -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<N>.example."
with QTYPE=A, where <N> 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)

View file

@ -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")

Binary file not shown.

View file

@ -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

View file

@ -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";
};

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -22,6 +22,8 @@ options {
recursion no;
dnssec-validation no;
notify yes;
transfers-out 3;
};
key rndc_key {

View file

@ -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";
};

View file

@ -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

View file

@ -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

View file

@ -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",
)

View file

@ -18,6 +18,7 @@ Changelog
development. Regular users should refer to :ref:`Release Notes <relnotes>`
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

View file

@ -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

View file

@ -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`

192
doc/notes/notes-9.18.49.rst Normal file
View file

@ -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; <recursion>`
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`

View file

@ -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 {

View file

@ -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;

View file

@ -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) {

View file

@ -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

View file

@ -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.

View file

@ -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);
}

View file

@ -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(

View file

@ -29,6 +29,7 @@
#include <isc/once.h>
#include <isc/os.h>
#include <isc/print.h>
#include <isc/random.h>
#include <isc/refcount.h>
#include <isc/string.h>
#include <isc/types.h>
@ -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

View file

@ -44,6 +44,7 @@
#include <dns/dispatch.h>
#include <dns/dnstap.h>
#include <dns/edns.h>
#include <dns/enumclass.h>
#include <dns/events.h>
#include <dns/message.h>
#include <dns/peer.h>
@ -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;
}

View file

@ -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.name, rr.type> += 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.
*/

View file

@ -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, &quota);
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, &quota);
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,

View file

@ -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.

View file

@ -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)