]> 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 <nicki@isc.org>
Mon, 12 Jan 2026 10:08:26 +0000 (11:08 +0100)
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.

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 7e73d3e97a0057c979ee93750f9407f887c04754..10b1045deeeaea5755e967d6a518f2613bb8797d 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 eb753349ca8b438066b176b1b4cc69f336d74551..c07ed392c095a747cc4de3c6d3368345ce48dd28 100644 (file)
@@ -1275,20 +1275,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))
@@ -1321,10 +1317,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))
@@ -1346,20 +1340,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))
@@ -1368,7 +1358,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