mirror of
https://github.com/isc-projects/bind9.git
synced 2026-06-11 07:49:59 -04:00
[9.18] [CVE-2026-3592] sec: usr: Limit resolver server list size
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. Closes isc-projects/bind9#5641 Backport of !909 Merge branch 'backport-5641-selfpointedglue-9.18' into 'security-bind-9.18' See merge request isc-private/bind9!954
This commit is contained in:
commit
5abfbc2663
29 changed files with 1445 additions and 245 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]}'"
|
||||
|
|
|
|||
|
|
@ -111,11 +111,9 @@ class NoErrorHandler(ResponseHandler):
|
|||
def resend_server() -> AsyncDnsServer:
|
||||
server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
|
||||
server.install_response_handlers(
|
||||
[
|
||||
PrimeHandler(),
|
||||
CookieHandler(),
|
||||
NoErrorHandler(),
|
||||
]
|
||||
PrimeHandler(),
|
||||
CookieHandler(),
|
||||
NoErrorHandler(),
|
||||
)
|
||||
return server
|
||||
|
||||
|
|
|
|||
28
bin/tests/system/selfpointedglue/ns1/named.conf.j2
Normal file
28
bin/tests/system/selfpointedglue/ns1/named.conf.j2
Normal 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";
|
||||
};
|
||||
24
bin/tests/system/selfpointedglue/ns1/root.db
Normal file
24
bin/tests/system/selfpointedglue/ns1/root.db
Normal 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
|
||||
28
bin/tests/system/selfpointedglue/ns2/named.conf.j2
Normal file
28
bin/tests/system/selfpointedglue/ns2/named.conf.j2
Normal 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";
|
||||
};
|
||||
27
bin/tests/system/selfpointedglue/ns2/tld.db
Normal file
27
bin/tests/system/selfpointedglue/ns2/tld.db
Normal 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
|
||||
155
bin/tests/system/selfpointedglue/ns3/example.tld.db
Normal file
155
bin/tests/system/selfpointedglue/ns3/example.tld.db
Normal 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
|
||||
33
bin/tests/system/selfpointedglue/ns3/example2.tld.db
Normal file
33
bin/tests/system/selfpointedglue/ns3/example2.tld.db
Normal 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
|
||||
44
bin/tests/system/selfpointedglue/ns3/named.conf.j2
Normal file
44
bin/tests/system/selfpointedglue/ns3/named.conf.j2
Normal 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";
|
||||
};
|
||||
3
bin/tests/system/selfpointedglue/ns4/named.args.j2
Normal file
3
bin/tests/system/selfpointedglue/ns4/named.args.j2
Normal 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@
|
||||
59
bin/tests/system/selfpointedglue/ns4/named.conf.j2
Normal file
59
bin/tests/system/selfpointedglue/ns4/named.conf.j2
Normal 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; };
|
||||
};
|
||||
14
bin/tests/system/selfpointedglue/ns4/root.hint
Normal file
14
bin/tests/system/selfpointedglue/ns4/root.hint
Normal 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
|
||||
20
bin/tests/system/selfpointedglue/prereq.sh
Normal file
20
bin/tests/system/selfpointedglue/prereq.sh
Normal 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
|
||||
75
bin/tests/system/selfpointedglue/tests_selfpointedglue.py
Normal file
75
bin/tests/system/selfpointedglue/tests_selfpointedglue.py
Normal 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,
|
||||
)
|
||||
18
bin/tests/system/srtt/README
Normal file
18
bin/tests/system/srtt/README
Normal 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
|
||||
36
bin/tests/system/srtt/ans2/ans.py
Normal file
36
bin/tests/system/srtt/ans2/ans.py
Normal 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()
|
||||
36
bin/tests/system/srtt/ans3/ans.py
Normal file
36
bin/tests/system/srtt/ans3/ans.py
Normal 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()
|
||||
36
bin/tests/system/srtt/ans4/ans.py
Normal file
36
bin/tests/system/srtt/ans4/ans.py
Normal 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()
|
||||
36
bin/tests/system/srtt/ans5/ans.py
Normal file
36
bin/tests/system/srtt/ans5/ans.py
Normal 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()
|
||||
29
bin/tests/system/srtt/ns1/named.conf.j2
Normal file
29
bin/tests/system/srtt/ns1/named.conf.j2
Normal 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";
|
||||
};
|
||||
36
bin/tests/system/srtt/ns1/root.db
Normal file
36
bin/tests/system/srtt/ns1/root.db
Normal 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
|
||||
1
bin/tests/system/srtt/ns6/named.args
Normal file
1
bin/tests/system/srtt/ns6/named.args
Normal file
|
|
@ -0,0 +1 @@
|
|||
-D srtt-ns6 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4
|
||||
41
bin/tests/system/srtt/ns6/named.conf.j2
Normal file
41
bin/tests/system/srtt/ns6/named.conf.j2
Normal 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";
|
||||
};
|
||||
20
bin/tests/system/srtt/prereq.sh
Normal file
20
bin/tests/system/srtt/prereq.sh
Normal 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
|
||||
59
bin/tests/system/srtt/srtt_ans.py
Normal file
59
bin/tests/system/srtt/srtt_ans.py
Normal 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)
|
||||
86
bin/tests/system/srtt/tests_srtt.py
Normal file
86
bin/tests/system/srtt/tests_srtt.py
Normal 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")
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -3355,91 +3364,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
|
||||
|
|
@ -3546,6 +3474,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 {
|
||||
|
|
@ -3961,8 +3890,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;
|
||||
}
|
||||
|
||||
|
|
@ -4037,6 +3964,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;
|
||||
|
|
@ -4059,7 +4060,6 @@ fctx_nextaddress(fetchctx_t *fctx) {
|
|||
possibly_mark(fctx, addrinfo);
|
||||
if (UNMARKED(addrinfo)) {
|
||||
addrinfo->flags |= FCTX_ADDRINFO_MARK;
|
||||
fctx->find = NULL;
|
||||
fctx->forwarding = true;
|
||||
|
||||
/*
|
||||
|
|
@ -4080,49 +4080,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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
Loading…
Reference in a new issue