]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Replace digdelv/ans8 with AsyncDnsServer
authorNicki Křížek <nicki@isc.org>
Fri, 28 Nov 2025 15:41:44 +0000 (16:41 +0100)
committerNicki Křížek (GitLab job 6713398) <nicki@isc.org>
Mon, 12 Jan 2026 10:53:23 +0000 (10:53 +0000)
Previously, the ans8 server had different response modes that applied to
all queries. Replace it with AsyncDnsServer that has serves the different
response modes under different domains without the need to change the
server behaviour at runtime.

Add the new queries that require an ns3 fallback to the ns3/example.db
zone.

(cherry picked from commit 0b7a089c7f575e6005fbcf84c8faf4dd439d521a)

bin/tests/system/digdelv/ans8/ans.py
bin/tests/system/digdelv/ns2/example.db.in
bin/tests/system/digdelv/tests.sh

index 3e18edc1cc65a62e0cc227db67ced84ccdd5f18a..d959b597e23eb451b13989efe315a433ee9b15ed 100644 (file)
 # See the COPYRIGHT file distributed with this work for additional
 # information regarding copyright ownership.
 
-from __future__ import print_function
-import os
-import sys
-import signal
-import socket
-import select
-import struct
+from typing import AsyncGenerator
 
-import dns, dns.message
-from dns.rcode import *
+import dns
+import dns.rcode
 
-modes = [
-    b"silent",  # Do not respond
-    b"close",  # UDP: same as silent; TCP: also close the connection
-    b"servfail",  # Always respond with SERVFAIL
-    b"unstable",  # Constantly switch between "silent" and "servfail"
-]
-mode = modes[0]
-n = 0
-
-
-def ctrl_channel(msg):
-    global modes, mode, n
-
-    msg = msg.splitlines().pop(0)
-    print("Received control message: %s" % msg)
-
-    if msg in modes:
-        mode = msg
-        n = 0
-        print("New mode: %s" % str(mode))
-
-
-def create_servfail(msg):
-    m = dns.message.from_wire(msg)
-    qname = m.question[0].name.to_text()
-    rrtype = m.question[0].rdtype
-    typename = dns.rdatatype.to_text(rrtype)
-
-    with open("query.log", "a") as f:
-        f.write("%s %s\n" % (typename, qname))
-        print("%s %s" % (typename, qname), end=" ")
-
-    r = dns.message.make_response(m)
-    r.set_rcode(SERVFAIL)
-    return r
-
-
-def sigterm(signum, frame):
-    print("Shutting down now...")
-    os.remove("ans.pid")
-    running = False
-    sys.exit(0)
-
-
-ip4 = "10.53.0.8"
-
-try:
-    port = int(os.environ["PORT"])
-except:
-    port = 5300
-
-try:
-    ctrlport = int(os.environ["EXTRAPORT1"])
-except:
-    ctrlport = 5300
-
-query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-query4_udp.bind((ip4, port))
-
-query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-query4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-query4_tcp.bind((ip4, port))
-query4_tcp.listen(100)
-
-ctrl4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-ctrl4_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-ctrl4_tcp.bind((ip4, ctrlport))
-ctrl4_tcp.listen(100)
-
-signal.signal(signal.SIGTERM, sigterm)
-
-f = open("ans.pid", "w")
-pid = os.getpid()
-print(pid, file=f)
-f.close()
-
-running = True
-
-print("Listening on %s port %d" % (ip4, port))
-print("Listening on %s port %d" % (ip4, ctrlport))
-print("Ctrl-c to quit")
-
-input = [query4_udp, query4_tcp, ctrl4_tcp]
-
-hung_conns = []
-
-while running:
-    try:
-        inputready, outputready, exceptready = select.select(input, [], [])
-    except select.error as e:
-        break
-    except socket.error as e:
-        break
-    except KeyboardInterrupt:
-        break
-
-    for s in inputready:
-        if s == query4_udp:
-            n = n + 1
-            print("UDP query received on %s" % ip4, end=" ")
-            msg = s.recvfrom(65535)
-            if (
-                mode == b"silent"
-                or mode == b"close"
-                or (mode == b"unstable" and n % 2 == 1)
-            ):
-                # Do not respond.
-                print("NO RESPONSE (%s)" % str(mode))
-                continue
-            elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0):
-                rsp = create_servfail(msg[0])
-                if rsp:
-                    print(dns.rcode.to_text(rsp.rcode()))
-                    s.sendto(rsp.to_wire(), msg[1])
-                else:
-                    print("NO RESPONSE (can not create a response)")
-            else:
-                raise (Exception("unsupported mode: %s" % mode))
-        elif s == query4_tcp:
-            n = n + 1
-            print("TCP query received on %s" % ip4, end=" ")
-            conn = None
-            try:
-                if mode == b"silent" or (mode == b"unstable" and n % 2 == 1):
-                    conn, addr = s.accept()
-                    # Do not respond and hang the connection.
-                    print("NO RESPONSE (%s)" % str(mode))
-                    hung_conns.append(conn)
-                    continue
-                elif mode == b"close":
-                    conn, addr = s.accept()
-                    # Do not respond and close the connection.
-                    print("NO RESPONSE (%s)" % str(mode))
-                    conn.close()
-                    continue
-                elif mode == b"servfail" or (mode == b"unstable" and n % 2 == 0):
-                    conn, addr = s.accept()
-                    # get TCP message length
-                    msg = conn.recv(2)
-                    if len(msg) != 2:
-                        print("NO RESPONSE (can not read the message length)")
-                        conn.close()
-                        continue
-                    length = struct.unpack(">H", msg[:2])[0]
-                    msg = conn.recv(length)
-                    if len(msg) != length:
-                        print("NO RESPONSE (can not read the message)")
-                        conn.close()
-                        continue
-                    rsp = create_servfail(msg)
-                    if rsp:
-                        print(dns.rcode.to_text(rsp.rcode()))
-                        wire = rsp.to_wire()
-                        conn.send(struct.pack(">H", len(wire)))
-                        conn.send(wire)
-                    else:
-                        print("NO RESPONSE (can not create a response)")
-                else:
-                    raise (Exception("unsupported mode: %s" % mode))
-            except socket.error as e:
-                print("NO RESPONSE (error: %s)" % str(e))
-            if conn:
-                conn.close()
-        elif s == ctrl4_tcp:
-            print("Control channel connected")
-            conn = None
-            try:
-                # Handle control channel input
-                conn, addr = s.accept()
-                msg = conn.recv(1024)
-                if msg:
-                    ctrl_channel(msg)
-                conn.close()
-            except s.timeout:
-                pass
-            if conn:
-                conn.close()
-
-    if not running:
-        break
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    CloseConnection,
+    DnsResponseSend,
+    DomainHandler,
+    IgnoreAllQueries,
+    QueryContext,
+    ResponseAction,
+    ResponseDrop,
+)
+
+
+class SilentHandler(DomainHandler, IgnoreAllQueries):
+    """Handler that doesn't respond."""
+
+    domains = ["silent.example"]
+
+
+class CloseHandler(DomainHandler):
+    """Handler that doesn't respond and closes TCP connection."""
+
+    domains = ["close.example"]
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        yield CloseConnection()
+
+
+class SilentThenServfailHandler(DomainHandler):
+    """Handler that drops one query and response to the next one with SERVFAIL."""
+
+    domains = ["silent-then-servfail.example"]
+    counter = 0
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        if self.counter % 2 == 0:
+            yield ResponseDrop()
+        else:
+            qctx.response.set_rcode(dns.rcode.SERVFAIL)
+            yield DnsResponseSend(qctx.response, authoritative=False)
+        self.counter += 1
+
+
+def main() -> None:
+    server = AsyncDnsServer()
+    server.install_response_handlers(
+        [
+            CloseHandler(),
+            SilentHandler(),
+            SilentThenServfailHandler(),
+        ]
+    )
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
index 8a88e038cf3a61b2c239759f736bd66583cc7333..9f938873f557a4aba3e24e996af45425a73d6d15 100644 (file)
@@ -33,6 +33,9 @@ c                     AAAA    fd92:7065:b8e:ffff::3
 d                      A       10.0.0.0
 d                      AAAA    fd92:7065:b8e:ffff::
 
+silent                 A       10.0.0.1
+close                  A       10.0.0.1
+
 xn--caf-dma            A       10.1.2.3
 
 foo                    TXT     "testing"
index 8d6155ef5c11db9451e00173d92dec58bdd8dfb2..69a5a5b167600a182af05b14fd8b1b4e65a9cc3f 100644 (file)
@@ -1208,20 +1208,16 @@ if [ -x "$DIG" ]; then
   # See [GL #3020] for more information
   n=$((n + 1))
   echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)"
-  # Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes)
-  echo "unstable" | sendcmd 10.53.0.8
   ret=0
-  dig_with_opts +timeout=1 +nofail @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts +timeout=1 +nofail @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
   grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
   n=$((n + 1))
   echo_i "check that dig handles TCP timeout followed by a SERVFAIL correctly ($n)"
-  # Ask ans8 to be in "unstable" mode (switching between "silent" and "servfail" modes)
-  echo "unstable" | sendcmd 10.53.0.8
   ret=0
-  dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts +timeout=1 +nofail +tcp @10.53.0.8 silent-then-servfail.example >dig.out.test$n 2>&1 || ret=1
   grep -F "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
@@ -1254,10 +1250,8 @@ if [ -x "$DIG" ]; then
 
   n=$((n + 1))
   echo_i "check that dig tries the next server after a TCP socket read error ($n)"
-  # Ask ans8 to be in "close" mode, which closes the connection after accepting it
-  echo "close" | sendcmd 10.53.0.8
   ret=0
-  dig_with_opts +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts +tcp @10.53.0.8 @10.53.0.3 close.example >dig.out.test$n 2>&1 || ret=1
   grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
@@ -1279,20 +1273,16 @@ if [ -x "$DIG" ]; then
 
   n=$((n + 1))
   echo_i "check that dig tries the next server after UDP socket read timeouts ($n)"
-  # Ask ans8 to be in "silent" mode
-  echo "silent" | sendcmd 10.53.0.8
   ret=0
-  dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts +timeout=1 @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
   grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
 
   n=$((n + 1))
   echo_i "check that dig tries the next server after TCP socket read timeouts ($n)"
-  # Ask ans8 to be in "silent" mode
-  echo "silent" | sendcmd 10.53.0.8
   ret=0
-  dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts +timeout=1 +tcp @10.53.0.8 @10.53.0.3 silent.example >dig.out.test$n 2>&1 || ret=1
   grep -F "status: NOERROR" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi
   status=$((status + ret))
@@ -1301,7 +1291,7 @@ if [ -x "$DIG" ]; then
   n=$((n + 1))
   echo_i "check that dig correctly refuses to use a server with a IPv4 mapped IPv6 address after failing with a regular IP address ($n)"
   ret=0
-  dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 a.example >dig.out.test$n 2>&1 || ret=1
+  dig_with_opts @10.53.0.8 @::ffff:10.53.0.8 silent.example >dig.out.test$n 2>&1 || ret=1
   grep -F ";; Skipping mapped address" dig.out.test$n >/dev/null || ret=1
   grep -F ";; No acceptable nameservers" dig.out.test$n >/dev/null || ret=1
   if [ $ret -ne 0 ]; then echo_i "failed"; fi