]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add common parts of reclimit test custom servers
authorŠtěpán Balážik <stepan@isc.org>
Fri, 16 Jan 2026 09:14:04 +0000 (10:14 +0100)
committerŠtěpán Balážik <stepan@isc.org>
Tue, 4 Aug 2026 11:21:28 +0000 (13:21 +0200)
These will be shared by all the ans*/ans.py files.

bin/tests/system/reclimit/reclimit_ans.py [new file with mode: 0644]
bin/tests/system/reclimit/tests_sh_reclimit.py

diff --git a/bin/tests/system/reclimit/reclimit_ans.py b/bin/tests/system/reclimit/reclimit_ans.py
new file mode 100644 (file)
index 0000000..f9dc40d
--- /dev/null
@@ -0,0 +1,236 @@
+"""
+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
+from typing import final
+
+import abc
+import asyncio
+
+import dns.flags
+import dns.name
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    ControlCommand,
+    ControllableAsyncDnsServer,
+    DnsResponseSend,
+    QnameHandler,
+    QueryContext,
+    ResponseAction,
+    ResponseHandler,
+)
+
+
+class ReclimitStateHandler(QnameHandler):
+    """
+    Handler for the "count." and "reset." queries that also holds the state
+    shared by all the handlers in one server.
+    """
+
+    qnames = ["count.", "reset."]
+
+    def __init__(self, indirect_send_response_default: bool = True) -> None:
+        self._indirect_send_response_default = indirect_send_response_default
+        self.count = 0
+        self.limit = 0
+        self.indirect_send_response = indirect_send_response_default
+        super().__init__()
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        if f"{qctx.qname}" == "count.":
+            self.count += 1
+            qctx.response.answer.append(
+                dns.rrset.from_text(
+                    "count.", 0, dns.rdataclass.IN, dns.rdatatype.TXT, f"{self.count}"
+                )
+            )
+            yield DnsResponseSend(qctx.response, authoritative=True)
+        elif f"{qctx.qname}" == "reset.":
+            self.reset()
+            yield DnsResponseSend(qctx.response, authoritative=False)
+
+    def reset(self) -> None:
+        self.count = 0
+        self.indirect_send_response = self._indirect_send_response_default
+
+
+class ReclimitHandler(ResponseHandler):
+    """
+    Base class for handlers in this test.
+
+    Increments the shared query counter on each query and delegates the actual
+    response generation to the `_get_counted_responses()` method.
+    """
+
+    def __init__(self, state_handler: ReclimitStateHandler) -> None:
+        self._state = state_handler
+        super().__init__()
+
+    @final
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        self._state.count += 1
+        async for response in self._get_counted_responses(qctx):
+            yield response
+
+    @abc.abstractmethod
+    async def _get_counted_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        yield DnsResponseSend(qctx.response)
+
+
+class LimitControlCommand(ControlCommand):
+    control_subdomain = "limit"
+
+    def __init__(self, state_handler: ReclimitStateHandler) -> None:
+        self._state_handler = state_handler
+        super().__init__()
+
+    def handle(
+        self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext
+    ) -> str | None:
+        if len(args) != 1:
+            return "Expected exactly one label"
+
+        try:
+            limit = int(args[0])
+        except ValueError:
+            return "Expected an integer"
+
+        self._state_handler.limit = limit
+        return f"Limit set to {limit}"
+
+
+def a(owner: str | dns.name.Name, ns_number: int) -> dns.rrset.RRset:
+    return dns.rrset.from_text(
+        f"{owner}", 3600, dns.rdataclass.IN, dns.rdatatype.A, f"10.53.0.{ns_number}"
+    )
+
+
+def ns(owner: str | dns.name.Name, target: str | dns.name.Name) -> dns.rrset.RRset:
+    return dns.rrset.from_text(
+        f"{owner}", 86400, dns.rdataclass.IN, dns.rdatatype.NS, f"{target}"
+    )
+
+
+class DirectExampleHandler(ReclimitHandler, QnameHandler):
+    qnames = ["direct.example.org", "direct.example.net"]
+
+    def __init__(
+        self, state_handler: ReclimitStateHandler, local_ns_number: int
+    ) -> None:
+        self._local_ns_number = local_ns_number
+        super().__init__(state_handler)
+
+    async def _get_counted_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        if qctx.qtype == dns.rdatatype.A:
+            qctx.response.answer.append(a(qctx.qname, self._local_ns_number))
+        yield DnsResponseSend(qctx.response)
+
+
+class IndirectExampleOrgHandler(ReclimitHandler, QnameHandler):
+    qnames = [f"indirect{i}.example.org" for i in range(1, 9)]
+
+    def __init__(
+        self, state_handler: ReclimitStateHandler, local_ns_number: int
+    ) -> None:
+        self._local_ns_number = local_ns_number
+        super().__init__(state_handler)
+
+    async def _get_counted_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        if not self._state.indirect_send_response:
+            qctx.response.authority.append(ns(f"{qctx.qname}", "ns1.1.example.org."))
+            qctx.response.flags &= ~dns.flags.AA
+        elif qctx.qtype == dns.rdatatype.A:
+            qctx.response.answer.append(a(qctx.qname, self._local_ns_number))
+        yield DnsResponseSend(qctx.response)
+
+
+def is_ns1_example(qname: dns.name.Name, tld: str) -> bool:
+    labels = qname.labels
+    return (
+        len(labels) == 5
+        and labels[3] == tld.encode()
+        and labels[2] == b"example"
+        and labels[1].isdigit()
+        and labels[0] == b"ns1"
+    )
+
+
+class Ns1ExampleOrgHandler(ReclimitHandler):
+    def __init__(self, state_handler: ReclimitStateHandler) -> None:
+        self._second_query_events: dict[dns.name.Name, asyncio.Event] = {}
+        super().__init__(state_handler)
+
+    def match(self, qctx: QueryContext) -> bool:
+        return is_ns1_example(qctx.qname, "org") and qctx.qtype in (
+            dns.rdatatype.A,
+            dns.rdatatype.AAAA,
+        )
+
+    async def _get_counted_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        ns_number = int(qctx.qname.labels[1])
+        next_ns_number = ns_number + 1
+        if not self._state.limit or (
+            not self._state.indirect_send_response
+            and next_ns_number <= self._state.limit
+        ):
+            qctx.response.authority.append(
+                ns(f"{ns_number}.example.org.", f"ns1.{next_ns_number}.example.org.")
+            )
+            qctx.response.flags &= ~dns.flags.AA
+        else:
+            self._state.indirect_send_response = True
+            if qctx.qtype == dns.rdatatype.A:
+                qctx.response.answer.append(a(qctx.qname, 4))
+
+        second_query_event = self._second_query_events.get(qctx.qname)
+        if second_query_event is not None:
+            # Second query arrived, release the first response.
+            second_query_event.set()
+            await asyncio.sleep(0)  # Yield to allow the first response to be sent.
+            yield DnsResponseSend(qctx.response)
+        else:
+            # Delay the response until the second query for the same QNAME
+            # arrives; give up waiting after 500 ms.
+            second_query_event = asyncio.Event()
+            self._second_query_events[qctx.qname] = second_query_event
+            try:
+                await asyncio.wait_for(second_query_event.wait(), timeout=0.5)
+            except asyncio.TimeoutError:
+                pass
+            finally:
+                del self._second_query_events[qctx.qname]
+            yield DnsResponseSend(qctx.response)
+
+
+class FallbackNxdomainHandler(ReclimitHandler):
+    async def _get_counted_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        qctx.response.set_rcode(dns.rcode.NXDOMAIN)
+        yield DnsResponseSend(qctx.response)
index 3d88c511fe5a0c23bbaf16ae56bb2c59dc06cf20..054fe6ffe06452566dad181c36f31e90f71c7167 100644 (file)
@@ -11,8 +11,6 @@
 
 import pytest
 
-import isctest.mark
-
 EXTRA_ARTIFACTS = pytest.mark.extra_artifacts(
     [
         "dig.out.*",
@@ -25,11 +23,7 @@ EXTRA_ARTIFACTS = pytest.mark.extra_artifacts(
     ]
 )
 
-pytestmark = [
-    isctest.mark.requires_net_dns,
-    isctest.mark.requires_net_dns_nameserver,
-    EXTRA_ARTIFACTS,
-]
+pytestmark = EXTRA_ARTIFACTS
 
 
 # The reclimit is known to be quite unstable. GL #1587