]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Use isctest.asyncserver in the "qmin" test
authorMichał Kępień <michal@isc.org>
Tue, 18 Mar 2025 05:19:01 +0000 (06:19 +0100)
committerMichał Kępień <michal@isc.org>
Tue, 18 Mar 2025 05:19:01 +0000 (06:19 +0100)
Replace custom DNS servers used in the "qmin" system test with new code
based on the isctest.asyncserver module.  The revised code employs zone
files and a limited amount of custom logic, which massively improves
test readability and maintainability, extends logging, and fixes
non-compliant replies sent by some of the custom servers in response to
certain queries (e.g. AA=0 in authoritative empty non-terminal
responses, non-glue address records in ADDITIONAL section).

24 files changed:
bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db [new file with mode: 0644]
bin/tests/system/qmin/ans2/ans.py [changed mode: 0755->0644]
bin/tests/system/qmin/ans2/bad.db [new symlink]
bin/tests/system/qmin/ans2/fwd.db [new symlink]
bin/tests/system/qmin/ans2/good.db [new file with mode: 0644]
bin/tests/system/qmin/ans2/slow.db [new symlink]
bin/tests/system/qmin/ans2/stale.db [new file with mode: 0644]
bin/tests/system/qmin/ans2/ugly.db [new symlink]
bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db [new file with mode: 0644]
bin/tests/system/qmin/ans3/a.b.stale.db [new file with mode: 0644]
bin/tests/system/qmin/ans3/ans.py [changed mode: 0755->0644]
bin/tests/system/qmin/ans3/zoop.boing.bad.db [new file with mode: 0644]
bin/tests/system/qmin/ans3/zoop.boing.good.db [new file with mode: 0644]
bin/tests/system/qmin/ans3/zoop.boing.slow.db [new file with mode: 0644]
bin/tests/system/qmin/ans3/zoop.boing.ugly.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/a.b.stale.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/ans.py [changed mode: 0755->0644]
bin/tests/system/qmin/ans4/b.stale.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db [new file with mode: 0644]
bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db [new file with mode: 0644]
bin/tests/system/qmin/qmin_ans.py [new file with mode: 0644]

diff --git a/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db b/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db
new file mode 100644 (file)
index 0000000..7042434
--- /dev/null
@@ -0,0 +1,17 @@
+; 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.
+
+@              30      SOA     ns2.good. hostmaster.arpa. 2018050100 1 1 1 1
+@              30      NS      ns2.good.
+
+8.2.6.0                60      NS      ns3.good.
+
+1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0 1 PTR nee.com.
old mode 100755 (executable)
new mode 100644 (file)
index d372c20..7fa6a6c
-# 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.
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 
-from __future__ import print_function
-import os
-import sys
-import signal
-import socket
-import select
-from datetime import datetime, timedelta
-import time
-import functools
-
-import dns, dns.message, dns.query, dns.flags
-from dns.rdatatype import *
-from dns.rdataclass import *
-from dns.rcode import *
-from dns.name import *
-
-
-# Log query to file
-def logquery(type, qname):
-    with open("qlog", "a") as f:
-        f.write("%s %s\n", type, qname)
-
-
-def endswith(domain, labels):
-    return domain.endswith("." + labels) or domain == labels
-
-
-############################################################################
-# Respond to a DNS query.
-# For good. it serves:
-# ns2.good. IN A 10.53.0.2
-# zoop.boing.good. NS ns3.good.
-# ns3.good. IN A 10.53.0.3
-# too.many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.good. A 192.0.2.2
-# it responds properly (with NODATA empty response) to non-empty terminals
-#
-# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
-#
-# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
-#
-# For ugly. it works the same as for good., but returns garbage to non-empty terminals
-#
-# For 1.0.0.2.ip6.arpa it serves
-# 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa. IN PTR nee.com.
-# 8.2.6.0.1.0.0.2.ip6.arpa IN NS ns3.good
-# 1.0.0.2.ip6.arpa. IN NS ns2.good
-# ip6.arpa. IN NS ns2.good
-#
-# For stale. it serves:
-# a.b. NS ns.a.b.stale.
-# ns.a.b.stale. IN A 10.53.0.3
-# b. NS ns.b.stale.
-# ns.b.stale. IN A 10.53.0.4
-############################################################################
-def create_response(msg):
-    m = dns.message.from_wire(msg)
-    qname = m.question[0].name.to_text()
-    lqname = qname.lower()
-    labels = lqname.split(".")
-
-    # get qtype
-    rrtype = m.question[0].rdtype
-    typename = dns.rdatatype.to_text(rrtype)
-    if typename == "A" or typename == "AAAA":
-        typename = "ADDR"
-    bad = False
-    ugly = False
-    slow = False
-
-    # log this query
-    with open("query.log", "a") as f:
-        f.write("%s %s\n" % (typename, lqname))
-        print("%s %s" % (typename, lqname), end=" ")
-
-    r = dns.message.make_response(m)
-    r.set_rcode(NOERROR)
-
-    if endswith(lqname, "1.0.0.2.ip6.arpa."):
-        # Direct query - give direct answer
-        if endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."):
-            # Delegate to ns3
-            r.authority.append(
-                dns.rrset.from_text(
-                    "8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, NS, "ns3.good."
-                )
-            )
-            r.additional.append(
-                dns.rrset.from_text("ns3.good.", 60, IN, A, "10.53.0.3")
-            )
-        elif (
-            lqname
-            == "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa."
-            and rrtype == PTR
-        ):
-            # Direct query - give direct answer
-            r.answer.append(
-                dns.rrset.from_text(
-                    "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.",
-                    1,
-                    IN,
-                    PTR,
-                    "nee.com.",
-                )
-            )
-            r.flags |= dns.flags.AA
-        elif lqname == "1.0.0.2.ip6.arpa." and rrtype == NS:
-            # NS query at the apex
-            r.answer.append(
-                dns.rrset.from_text("1.0.0.2.ip6.arpa.", 30, IN, NS, "ns2.good.")
-            )
-            r.flags |= dns.flags.AA
-        elif endswith(
-            "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.",
-            lqname,
-        ):
-            # NODATA answer
-            r.authority.append(
-                dns.rrset.from_text(
-                    "1.0.0.2.ip6.arpa.",
-                    30,
-                    IN,
-                    SOA,
-                    "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-        else:
-            # NXDOMAIN
-            r.authority.append(
-                dns.rrset.from_text(
-                    "1.0.0.2.ip6.arpa.",
-                    30,
-                    IN,
-                    SOA,
-                    "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-        return r
-    elif endswith(lqname, "ip6.arpa."):
-        if lqname == "ip6.arpa." and rrtype == NS:
-            # NS query at the apex
-            r.answer.append(dns.rrset.from_text("ip6.arpa.", 30, IN, NS, "ns2.good."))
-            r.flags |= dns.flags.AA
-        elif endswith("1.0.0.2.ip6.arpa.", lqname):
-            # NODATA answer
-            r.authority.append(
-                dns.rrset.from_text(
-                    "ip6.arpa.",
-                    30,
-                    IN,
-                    SOA,
-                    "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-        else:
-            # NXDOMAIN
-            r.authority.append(
-                dns.rrset.from_text(
-                    "ip6.arpa.",
-                    30,
-                    IN,
-                    SOA,
-                    "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-        return r
-    elif endswith(lqname, "stale."):
-        if endswith(lqname, "a.b.stale."):
-            # Delegate to ns.a.b.stale.
-            r.authority.append(
-                dns.rrset.from_text("a.b.stale.", 2, IN, NS, "ns.a.b.stale.")
-            )
-            r.additional.append(
-                dns.rrset.from_text("ns.a.b.stale.", 2, IN, A, "10.53.0.3")
-            )
-        elif endswith(lqname, "b.stale."):
-            # Delegate to ns.b.stale.
-            r.authority.append(
-                dns.rrset.from_text("b.stale.", 2, IN, NS, "ns.b.stale.")
-            )
-            r.additional.append(
-                dns.rrset.from_text("ns.b.stale.", 2, IN, A, "10.53.0.4")
-            )
-        elif lqname == "stale." and rrtype == NS:
-            # NS query at the apex.
-            r.answer.append(dns.rrset.from_text("stale.", 2, IN, NS, "ns2.stale."))
-            r.flags |= dns.flags.AA
-        elif lqname == "stale." and rrtype == SOA:
-            # SOA query at the apex.
-            r.answer.append(
-                dns.rrset.from_text(
-                    "stale.", 2, IN, SOA, "ns2.stale. hostmaster.stale. 1 2 3 4 5"
-                )
-            )
-            r.flags |= dns.flags.AA
-        elif lqname == "stale.":
-            # NODATA answer
-            r.authority.append(
-                dns.rrset.from_text(
-                    "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
-                )
-            )
-            r.flags |= dns.flags.AA
-        elif lqname == "ns2.stale.":
-            if rrtype == A:
-                r.additional.append(
-                    dns.rrset.from_text("ns.b.stale.", 2, IN, A, "10.53.0.2")
-                )
-            else:
-                r.authority.append(
-                    dns.rrset.from_text(
-                        "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
-                    )
-                )
-            r.flags |= dns.flags.AA
-        else:
-            # NXDOMAIN
-            r.authority.append(
-                dns.rrset.from_text(
-                    "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-        return r
-    elif endswith(lqname, "bad."):
-        bad = True
-        suffix = "bad."
-        lqname = lqname[:-4]
-    elif endswith(lqname, "ugly."):
-        ugly = True
-        suffix = "ugly."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "good."):
-        suffix = "good."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "slow."):
-        slow = True
-        suffix = "slow."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "fwd."):
-        suffix = "fwd."
-        lqname = lqname[:-4]
-    else:
-        r.set_rcode(REFUSED)
-        return r
-
-    # Good/bad/ugly differs only in how we treat non-empty terminals
-    if endswith(lqname, "zoop.boing."):
-        r.authority.append(
-            dns.rrset.from_text("zoop.boing." + suffix, 1, IN, NS, "ns3." + suffix)
-        )
-    elif (
-        lqname == "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z."
-        and rrtype == A
-    ):
-        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
-        r.flags |= dns.flags.AA
-    elif lqname == "" and rrtype == NS:
-        r.answer.append(dns.rrset.from_text(suffix, 30, IN, NS, "ns2." + suffix))
-        r.flags |= dns.flags.AA
-    elif lqname == "ns2.":
-        r.flags |= dns.flags.AA
-        if rrtype == A:
-            r.answer.append(
-                dns.rrset.from_text("ns2." + suffix, 30, IN, A, "10.53.0.2")
-            )
-        elif rrtype == AAAA:
-            r.answer.append(
-                dns.rrset.from_text(
-                    "ns2." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::2"
-                )
-            )
-        else:
-            r.authority.append(
-                dns.rrset.from_text(
-                    suffix,
-                    30,
-                    IN,
-                    SOA,
-                    "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-    elif lqname == "ns3.":
-        r.flags |= dns.flags.AA
-        if rrtype == A:
-            r.answer.append(
-                dns.rrset.from_text("ns3." + suffix, 30, IN, A, "10.53.0.3")
-            )
-        elif lqname == "ns3." and rrtype == AAAA:
-            r.answer.append(
-                dns.rrset.from_text(
-                    "ns3." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::3"
-                )
-            )
-        else:
-            r.authority.append(
-                dns.rrset.from_text(
-                    suffix,
-                    30,
-                    IN,
-                    SOA,
-                    "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-    elif lqname == "ns4.":
-        r.flags |= dns.flags.AA
-        if rrtype == A:
-            r.answer.append(
-                dns.rrset.from_text("ns4." + suffix, 30, IN, A, "10.53.0.4")
-            )
-        elif rrtype == AAAA:
-            r.answer.append(
-                dns.rrset.from_text(
-                    "ns4." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::4"
-                )
-            )
-        else:
-            r.authority.append(
-                dns.rrset.from_text(
-                    suffix,
-                    30,
-                    IN,
-                    SOA,
-                    "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-                )
-            )
-    elif lqname == "a.bit.longer.ns.name." and rrtype == A:
-        r.answer.append(
-            dns.rrset.from_text("a.bit.longer.ns.name." + suffix, 1, IN, A, "10.53.0.4")
-        )
-        r.flags |= dns.flags.AA
-    elif lqname == "a.bit.longer.ns.name." and rrtype == AAAA:
-        r.answer.append(
-            dns.rrset.from_text(
-                "a.bit.longer.ns.name." + suffix, 1, IN, AAAA, "fd92:7065:b8e:ffff::4"
-            )
-        )
-        r.flags |= dns.flags.AA
-    else:
-        r.authority.append(
-            dns.rrset.from_text(
-                suffix,
-                1,
-                IN,
-                SOA,
-                "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-            )
-        )
-        if bad or not (
-            endswith("icky.icky.icky.ptang.zoop.boing.", lqname)
-            or endswith(
-                "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.",
-                lqname,
-            )
-            or endswith("a.bit.longer.ns.name.", lqname)
-        ):
-            r.set_rcode(NXDOMAIN)
-        if ugly:
-            r.set_rcode(FORMERR)
-    if slow:
-        time.sleep(0.2)
-    return r
-
-
-def sigterm(signum, frame):
-    print("Shutting down now...")
-    os.remove("ans.pid")
-    running = False
-    sys.exit(0)
-
-
-############################################################################
-# Main
-#
-# Set up responder and control channel, open the pid file, and start
-# the main loop, listening for queries on the query channel or commands
-# on the control channel and acting on them.
-############################################################################
-ip4 = "10.53.0.2"
-ip6 = "fd92:7065:b8e:ffff::2"
-
-try:
-    port = int(os.environ["PORT"])
-except:
-    port = 5300
-
-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-query4_socket.bind((ip4, port))
-
-havev6 = True
-try:
-    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
-    try:
-        query6_socket.bind((ip6, port))
-    except:
-        query6_socket.close()
-        havev6 = False
-except:
-    havev6 = False
-
-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))
-if havev6:
-    print("Listening on %s port %d" % (ip6, port))
-print("Ctrl-c to quit")
-
-if havev6:
-    input = [query4_socket, query6_socket]
-else:
-    input = [query4_socket]
-
-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_socket or s == query6_socket:
-            print(
-                "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
-            )
-            # Handle incoming queries
-            msg = s.recvfrom(65535)
-            rsp = create_response(msg[0])
-            if rsp:
-                print(dns.rcode.to_text(rsp.rcode()))
-                s.sendto(rsp.to_wire(), msg[1])
-            else:
-                print("NO RESPONSE")
-    if not running:
-        break
+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 typing import AsyncGenerator
+
+import dns.message
+import dns.name
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsResponseSend,
+    DomainHandler,
+    QueryContext,
+    ResponseAction,
+)
+
+from qmin_ans import (
+    DelayedResponseHandler,
+    EntRcodeChanger,
+    QueryLogHandler,
+    log_query,
+)
+
+
+class QueryLogger(QueryLogHandler):
+    domains = ["1.0.0.2.ip6.arpa.", "fwd.", "good."]
+
+
+class BadHandler(EntRcodeChanger):
+    domains = ["bad."]
+    rcode = dns.rcode.NXDOMAIN
+
+
+class UglyHandler(EntRcodeChanger):
+    domains = ["ugly."]
+    rcode = dns.rcode.FORMERR
+
+
+class SlowHandler(DelayedResponseHandler):
+    domains = ["slow."]
+    delay = 0.2
+
+
+def send_delegation(
+    qctx: QueryContext, zone_cut: dns.name.Name, target_addr: str
+) -> ResponseAction:
+    """
+    Delegate `zone_cut` to a single in-bailiwick name server, `ns.<zone_cut>`,
+    with a single IPv4 glue record (provided in `target_addr`) included in the
+    ADDITIONAL section.
+    """
+    ns_name = "ns." + zone_cut.to_text()
+    ns_rrset = dns.rrset.from_text(
+        zone_cut, 2, dns.rdataclass.IN, dns.rdatatype.NS, ns_name
+    )
+    a_rrset = dns.rrset.from_text(
+        ns_name, 2, dns.rdataclass.IN, dns.rdatatype.A, target_addr
+    )
+
+    response = dns.message.make_response(qctx.query)
+    response.set_rcode(dns.rcode.NOERROR)
+    response.authority.append(ns_rrset)
+    response.additional.append(a_rrset)
+
+    return DnsResponseSend(response, authoritative=False)
+
+
+class StaleHandler(DomainHandler):
+    """
+    `a.b.stale` is a subdomain of `b.stale` and these two subdomains need to be
+    delegated to different name servers.  Therefore, their delegations cannot
+    be placed in the zone file because the zone cut at `b.stale` would occlude
+    the one at `a.b.stale`.  Generate these delegations dynamically depending
+    on the QNAME.
+    """
+
+    domains = ["stale."]
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        log_query(qctx)
+        a_b_stale = dns.name.from_text("a.b.stale.")
+        b_stale = dns.name.from_text("b.stale.")
+        if qctx.qname.is_subdomain(a_b_stale):
+            yield send_delegation(qctx, a_b_stale, "10.53.0.3")
+        elif qctx.qname.is_subdomain(b_stale):
+            yield send_delegation(qctx, b_stale, "10.53.0.4")
+
+
+if __name__ == "__main__":
+    server = AsyncDnsServer()
+    server.install_response_handler(QueryLogger())
+    server.install_response_handler(BadHandler())
+    server.install_response_handler(UglyHandler())
+    server.install_response_handler(SlowHandler())
+    server.install_response_handler(StaleHandler())
+    server.run()
diff --git a/bin/tests/system/qmin/ans2/bad.db b/bin/tests/system/qmin/ans2/bad.db
new file mode 120000 (symlink)
index 0000000..ebb0f70
--- /dev/null
@@ -0,0 +1 @@
+good.db
\ No newline at end of file
diff --git a/bin/tests/system/qmin/ans2/fwd.db b/bin/tests/system/qmin/ans2/fwd.db
new file mode 120000 (symlink)
index 0000000..ebb0f70
--- /dev/null
@@ -0,0 +1 @@
+good.db
\ No newline at end of file
diff --git a/bin/tests/system/qmin/ans2/good.db b/bin/tests/system/qmin/ans2/good.db
new file mode 100644 (file)
index 0000000..54a20e6
--- /dev/null
@@ -0,0 +1,26 @@
+; 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.
+
+@                      1       SOA     ns2 hostmaster.arpa. 2018050100 1 1 1 1
+
+@                      30      NS      ns2
+ns2                    30      A       10.53.0.2
+                       30      AAAA    fd92:7065:b8e:ffff::2
+
+zoop.boing             30      NS      ns3
+ns3                    30      A       10.53.0.3
+                       30      AAAA    fd92:7065:b8e:ffff::3
+
+ns4                    30      A       10.53.0.4
+                       30      AAAA    fd92:7065:b8e:ffff::4
+
+a.bit.longer.ns.name   1       A       10.53.0.4
+                       1       AAAA    fd92:7065:b8e:ffff::4
diff --git a/bin/tests/system/qmin/ans2/slow.db b/bin/tests/system/qmin/ans2/slow.db
new file mode 120000 (symlink)
index 0000000..ebb0f70
--- /dev/null
@@ -0,0 +1 @@
+good.db
\ No newline at end of file
diff --git a/bin/tests/system/qmin/ans2/stale.db b/bin/tests/system/qmin/ans2/stale.db
new file mode 100644 (file)
index 0000000..b1b1ac6
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@      2       SOA     ns2 hostmaster.stale. 1 2 3 4 5
+@      2       NS      ns2
+ns2    2       A       10.53.0.2
+       2       AAAA    fd92:7065:b8e:ffff::2
diff --git a/bin/tests/system/qmin/ans2/ugly.db b/bin/tests/system/qmin/ans2/ugly.db
new file mode 120000 (symlink)
index 0000000..ebb0f70
--- /dev/null
@@ -0,0 +1 @@
+good.db
\ No newline at end of file
diff --git a/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db b/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db
new file mode 100644 (file)
index 0000000..e50c75c
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@              30      SOA     ns3.good. hostmaster.arpa. 2018050100 1 1 1 1
+@              30      NS      ns3.good.
+
+1.1.1.1                60      NS      ns4.good.
diff --git a/bin/tests/system/qmin/ans3/a.b.stale.db b/bin/tests/system/qmin/ans3/a.b.stale.db
new file mode 100644 (file)
index 0000000..e595890
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@      1       SOA     ns hostmaster.a.b.stale. 1 2 3 4 5
+@      1       NS      ns
+@      1       TXT     "peekaboo"
+ns     1       A       10.53.0.3
old mode 100755 (executable)
new mode 100644 (file)
index b5ae73c..057bbb3
-# 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.
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 
-from __future__ import print_function
-import os
-import sys
-import signal
-import socket
-import select
-from datetime import datetime, timedelta
-import time
-import functools
+SPDX-License-Identifier: MPL-2.0
 
-import dns, dns.message, dns.query, dns.flags
-from dns.rdatatype import *
-from dns.rdataclass import *
-from dns.rcode import *
-from dns.name import *
+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.
+"""
 
-# Log query to file
-def logquery(type, qname):
-    with open("qlog", "a") as f:
-        f.write("%s %s\n", type, qname)
+import dns.rcode
 
+from isctest.asyncserver import AsyncDnsServer
 
-def endswith(domain, labels):
-    return domain.endswith("." + labels) or domain == labels
+from qmin_ans import DelayedResponseHandler, EntRcodeChanger, QueryLogHandler
 
 
-############################################################################
-# Respond to a DNS query.
-# For good. it serves:
-# zoop.boing.good. NS ns3.good.
-# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name.good.
-# it responds properly (with NODATA empty response) to non-empty terminals
-#
-# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
-#
-# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
-#
-# For ugly. it works the same as for good., but returns garbage to non-empty terminals
-#
-# For stale. it serves:
-# a.b.stale. IN TXT peekaboo (resolver did not do qname minimization)
-############################################################################
-def create_response(msg):
-    m = dns.message.from_wire(msg)
-    qname = m.question[0].name.to_text()
-    lqname = qname.lower()
-    labels = lqname.split(".")
-    suffix = ""
+class QueryLogger(QueryLogHandler):
+    domains = ["8.2.6.0.1.0.0.2.ip6.arpa.", "a.b.stale.", "zoop.boing.good."]
 
-    # get qtype
-    rrtype = m.question[0].rdtype
-    typename = dns.rdatatype.to_text(rrtype)
-    if typename == "A" or typename == "AAAA":
-        typename = "ADDR"
-    bad = False
-    ugly = False
-    slow = False
 
-    # log this query
-    with open("query.log", "a") as f:
-        f.write("%s %s\n" % (typename, lqname))
-        print("%s %s" % (typename, lqname), end=" ")
+class ZoopBoingBadHandler(EntRcodeChanger):
+    domains = ["zoop.boing.bad."]
+    rcode = dns.rcode.NXDOMAIN
 
-    r = dns.message.make_response(m)
-    r.set_rcode(NOERROR)
 
-    ip6req = False
+class ZoopBoingUglyHandler(EntRcodeChanger):
+    domains = ["zoop.boing.ugly."]
+    rcode = dns.rcode.FORMERR
 
-    if endswith(lqname, "bad."):
-        bad = True
-        suffix = "bad."
-        lqname = lqname[:-4]
-    elif endswith(lqname, "ugly."):
-        ugly = True
-        suffix = "ugly."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "good."):
-        suffix = "good."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "slow."):
-        slow = True
-        suffix = "slow."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."):
-        ip6req = True
-    elif endswith(lqname, "a.b.stale."):
-        if lqname == "a.b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == TXT:
-                # Direct query.
-                r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "peekaboo"))
-            elif rrtype == NS:
-                # NS a.b.
-                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale."))
-                r.additional.append(
-                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")
-                )
-            elif rrtype == SOA:
-                # SOA a.b.
-                r.answer.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-            else:
-                # NODATA.
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-        elif lqname == "ns.a.b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == A:
-                r.answer.append(
-                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")
-                )
-            else:
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-        else:
-            r.flags |= dns.flags.AA
-            r.authority.append(
-                dns.rrset.from_text(
-                    lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-            # NXDOMAIN.
-        return r
-    else:
-        r.set_rcode(REFUSED)
-        return r
 
-    # Good/bad differs only in how we treat non-empty terminals
-    if lqname == "zoop.boing." and rrtype == NS:
-        r.answer.append(
-            dns.rrset.from_text(lqname + suffix, 1, IN, NS, "ns3." + suffix)
-        )
-        r.flags |= dns.flags.AA
-    elif endswith(lqname, "icky.ptang.zoop.boing."):
-        r.authority.append(
-            dns.rrset.from_text(
-                "icky.ptang.zoop.boing." + suffix,
-                1,
-                IN,
-                NS,
-                "a.bit.longer.ns.name." + suffix,
-            )
-        )
-    elif endswith("icky.ptang.zoop.boing.", lqname):
-        r.authority.append(
-            dns.rrset.from_text(
-                "zoop.boing." + suffix,
-                1,
-                IN,
-                SOA,
-                "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-            )
-        )
-        if bad:
-            r.set_rcode(NXDOMAIN)
-        if ugly:
-            r.set_rcode(FORMERR)
-    elif endswith(lqname, "zoop.boing."):
-        r.authority.append(
-            dns.rrset.from_text(
-                "zoop.boing." + suffix,
-                1,
-                IN,
-                SOA,
-                "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-            )
-        )
-        r.set_rcode(NXDOMAIN)
-    elif ip6req:
-        r.authority.append(
-            dns.rrset.from_text(
-                "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, NS, "ns4.good."
-            )
-        )
-        r.additional.append(dns.rrset.from_text("ns4.good.", 60, IN, A, "10.53.0.4"))
-    else:
-        r.set_rcode(REFUSED)
+class ZoopBoingSlowHandler(DelayedResponseHandler):
+    domains = ["zoop.boing.slow."]
+    delay = 0.4
 
-    if slow:
-        time.sleep(0.4)
-    return r
 
-
-def sigterm(signum, frame):
-    print("Shutting down now...")
-    os.remove("ans.pid")
-    running = False
-    sys.exit(0)
-
-
-############################################################################
-# Main
-#
-# Set up responder and control channel, open the pid file, and start
-# the main loop, listening for queries on the query channel or commands
-# on the control channel and acting on them.
-############################################################################
-ip4 = "10.53.0.3"
-ip6 = "fd92:7065:b8e:ffff::3"
-
-try:
-    port = int(os.environ["PORT"])
-except:
-    port = 5300
-
-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-query4_socket.bind((ip4, port))
-
-havev6 = True
-try:
-    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
-    try:
-        query6_socket.bind((ip6, port))
-    except:
-        query6_socket.close()
-        havev6 = False
-except:
-    havev6 = False
-
-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))
-if havev6:
-    print("Listening on %s port %d" % (ip6, port))
-print("Ctrl-c to quit")
-
-if havev6:
-    input = [query4_socket, query6_socket]
-else:
-    input = [query4_socket]
-
-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_socket or s == query6_socket:
-            print(
-                "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
-            )
-            # Handle incoming queries
-            msg = s.recvfrom(65535)
-            rsp = create_response(msg[0])
-            if rsp:
-                print(dns.rcode.to_text(rsp.rcode()))
-                s.sendto(rsp.to_wire(), msg[1])
-            else:
-                print("NO RESPONSE")
-    if not running:
-        break
+if __name__ == "__main__":
+    server = AsyncDnsServer()
+    server.install_response_handler(QueryLogger())
+    server.install_response_handler(ZoopBoingBadHandler())
+    server.install_response_handler(ZoopBoingUglyHandler())
+    server.install_response_handler(ZoopBoingSlowHandler())
+    server.run()
diff --git a/bin/tests/system/qmin/ans3/zoop.boing.bad.db b/bin/tests/system/qmin/ans3/zoop.boing.bad.db
new file mode 100644 (file)
index 0000000..c3a5b52
--- /dev/null
@@ -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.
+
+@                      1       SOA     ns3.bad. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      ns3.bad.
+icky.ptang             1       NS      a.bit.longer.ns.name.bad.
diff --git a/bin/tests/system/qmin/ans3/zoop.boing.good.db b/bin/tests/system/qmin/ans3/zoop.boing.good.db
new file mode 100644 (file)
index 0000000..cc4a3eb
--- /dev/null
@@ -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.
+
+@                      1       SOA     ns3.good. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      ns3.good.
+icky.ptang             1       NS      a.bit.longer.ns.name.good.
diff --git a/bin/tests/system/qmin/ans3/zoop.boing.slow.db b/bin/tests/system/qmin/ans3/zoop.boing.slow.db
new file mode 100644 (file)
index 0000000..67635bb
--- /dev/null
@@ -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.
+
+@                      1       SOA     ns3.slow. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      ns3.slow.
+icky.ptang             1       NS      a.bit.longer.ns.name.slow.
diff --git a/bin/tests/system/qmin/ans3/zoop.boing.ugly.db b/bin/tests/system/qmin/ans3/zoop.boing.ugly.db
new file mode 100644 (file)
index 0000000..27c5ee0
--- /dev/null
@@ -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.
+
+@                      1       SOA     ns3.ugly. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      ns3.ugly.
+icky.ptang             1       NS      a.bit.longer.ns.name.ugly.
diff --git a/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db b/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db
new file mode 100644 (file)
index 0000000..f4058b3
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@              30      SOA     ns4.good. hostmaster.arpa. 2018050100 1 1 1 1
+@              30      NS      ns4.good.
+
+test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4 1 TXT "long_ip6_name"
diff --git a/bin/tests/system/qmin/ans4/a.b.stale.db b/bin/tests/system/qmin/ans4/a.b.stale.db
new file mode 100644 (file)
index 0000000..68e63ce
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@      1       SOA     ns hostmaster.a.b.stale. 1 2 3 4 5
+@      1       NS      ns
+ns     1       A       10.53.0.4
+@      1       TXT     "hooray"
old mode 100755 (executable)
new mode 100644 (file)
index f5fe3cc..ca43845
-# 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.
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 
-from __future__ import print_function
-import os
-import sys
-import signal
-import socket
-import select
-from datetime import datetime, timedelta
-import time
-import functools
+SPDX-License-Identifier: MPL-2.0
 
-import dns, dns.message, dns.query, dns.flags
-from dns.rdatatype import *
-from dns.rdataclass import *
-from dns.rcode import *
-from dns.name import *
+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.
+"""
 
-# Log query to file
-def logquery(type, qname):
-    with open("qlog", "a") as f:
-        f.write("%s %s\n", type, qname)
+from typing import AsyncGenerator
 
+import dns.rcode
 
-def endswith(domain, labels):
-    return domain.endswith("." + labels) or domain == labels
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsResponseSend,
+    DomainHandler,
+    QueryContext,
+    ResponseAction,
+)
 
+from qmin_ans import DelayedResponseHandler, EntRcodeChanger, QueryLogHandler, log_query
 
-############################################################################
-# Respond to a DNS query.
-# For good. it serves:
-# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name.
-# icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.1
-# more.icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.2
-# it responds properly (with NODATA empty response) to non-empty terminals
-#
-# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
-#
-# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
-#
-# For ugly. it works the same as for good., but returns garbage to non-empty terminals
-#
-# For stale. it serves:
-# a.b.stale. IN TXT hooray (resolver did do qname minimization)
-############################################################################
-def create_response(msg):
-    m = dns.message.from_wire(msg)
-    qname = m.question[0].name.to_text()
-    lqname = qname.lower()
-    labels = lqname.split(".")
-    suffix = ""
 
-    # get qtype
-    rrtype = m.question[0].rdtype
-    typename = dns.rdatatype.to_text(rrtype)
-    if typename == "A" or typename == "AAAA":
-        typename = "ADDR"
-    bad = False
-    slow = False
-    ugly = False
+class QueryLogger(QueryLogHandler):
+    domains = [
+        "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
+        "icky.ptang.zoop.boing.good.",
+    ]
 
-    # log this query
-    with open("query.log", "a") as f:
-        f.write("%s %s\n" % (typename, lqname))
-        print("%s %s" % (typename, lqname), end=" ")
 
-    r = dns.message.make_response(m)
-    r.set_rcode(NOERROR)
+class StaleHandler(DomainHandler):
+    """
+    The test code relies on this server returning non-minimal (i.e. including
+    address records in the ADDITIONAL section) responses to NS queries for
+    `b.stale` and `a.b.stale`.  While this logic (returning non-minimal
+    responses to NS queries) could be implemented in AsyncDnsServer itself,
+    doing so breaks a lot of other checks in this system test.  Therefore, only
+    these two zones behave in this particular way, thanks to a custom response
+    handler implemented below.
+    """
 
-    ip6req = False
+    domains = ["b.stale", "a.b.stale"]
 
-    if endswith(lqname, "bad."):
-        bad = True
-        suffix = "bad."
-        lqname = lqname[:-4]
-    elif endswith(lqname, "ugly."):
-        ugly = True
-        suffix = "ugly."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "good."):
-        suffix = "good."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "slow."):
-        slow = True
-        suffix = "slow."
-        lqname = lqname[:-5]
-    elif endswith(lqname, "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."):
-        ip6req = True
-    elif endswith(lqname, "b.stale."):
-        if lqname == "a.b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == TXT:
-                # Direct query.
-                r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "hooray"))
-            elif rrtype == NS:
-                # NS a.b.
-                # This is only returned if a query for b.stale/NS has been made
-                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale."))
-                r.additional.append(
-                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.4")
-                )
-            elif rrtype == SOA:
-                # SOA a.b.
-                r.answer.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-            else:
-                # NODATA.
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-        elif lqname == "ns.a.b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == A:
-                r.answer.append(
-                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.4")
-                )
-            else:
-                # NODATA.
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
-                    )
-                )
-        elif lqname == "b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == NS:
-                # NS b.
-                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.b.stale."))
-                r.additional.append(
-                    dns.rrset.from_text("ns.b.stale.", 1, IN, A, "10.53.0.4")
-                )
-            elif rrtype == SOA:
-                # SOA b.
-                r.answer.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
-                    )
-                )
-            else:
-                # NODATA.
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
-                    )
-                )
-        elif lqname == "ns.b.stale.":
-            r.flags |= dns.flags.AA
-            if rrtype == A:
-                # SOA a.b.
-                r.answer.append(
-                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.4")
-                )
-            else:
-                # NODATA.
-                r.authority.append(
-                    dns.rrset.from_text(
-                        lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
-                    )
-                )
-        else:
-            r.authority.append(
-                dns.rrset.from_text(
-                    lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-            # NXDOMAIN.
-        return r
-    else:
-        r.set_rcode(REFUSED)
-        return r
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        log_query(qctx)
 
-    # Good/bad differs only in how we treat non-empty terminals
-    if lqname == "icky.icky.icky.ptang.zoop.boing." and rrtype == A:
-        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.1"))
-        r.flags |= dns.flags.AA
-    elif lqname == "more.icky.icky.icky.ptang.zoop.boing." and rrtype == A:
-        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
-        r.flags |= dns.flags.AA
-    elif lqname == "icky.ptang.zoop.boing." and rrtype == NS:
-        r.answer.append(
-            dns.rrset.from_text(
-                lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name." + suffix
-            )
-        )
-        r.flags |= dns.flags.AA
-    elif endswith(lqname, "icky.ptang.zoop.boing."):
-        r.authority.append(
-            dns.rrset.from_text(
-                "icky.ptang.zoop.boing." + suffix,
-                1,
-                IN,
-                SOA,
-                "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
-            )
-        )
-        if bad or not endswith("more.icky.icky.icky.ptang.zoop.boing.", lqname):
-            r.set_rcode(NXDOMAIN)
-        if ugly:
-            r.set_rcode(FORMERR)
-    elif ip6req:
-        r.flags |= dns.flags.AA
-        if (
-            lqname
-            == "test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."
-            and rrtype == TXT
-        ):
-            r.answer.append(
-                dns.rrset.from_text(
-                    "test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
-                    1,
-                    IN,
-                    TXT,
-                    "long_ip6_name",
-                )
-            )
-        elif endswith(
-            "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
-            lqname,
-        ):
-            # NODATA answer
-            r.authority.append(
-                dns.rrset.from_text(
-                    "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
-                    60,
-                    IN,
-                    SOA,
-                    "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16",
-                )
-            )
-        else:
-            # NXDOMAIN
-            r.authority.append(
-                dns.rrset.from_text(
-                    "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
-                    60,
-                    IN,
-                    SOA,
-                    "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16",
-                )
-            )
-            r.set_rcode(NXDOMAIN)
-    else:
-        r.set_rcode(REFUSED)
+        if qctx.qtype == dns.rdatatype.NS:
+            assert qctx.zone
+            assert qctx.response.answer[0]
 
-    if slow:
-        time.sleep(0.4)
-    return r
+            for nameserver in qctx.response.answer[0]:
+                if not nameserver.target.is_subdomain(qctx.response.answer[0].name):
+                    continue
+                glue_a = qctx.zone.get_rrset(nameserver.target, dns.rdatatype.A)
+                if glue_a:
+                    qctx.response.additional.append(glue_a)
+                glue_aaaa = qctx.zone.get_rrset(nameserver.target, dns.rdatatype.AAAA)
+                if glue_aaaa:
+                    qctx.response.additional.append(glue_aaaa)
 
+        yield DnsResponseSend(qctx.response)
 
-def sigterm(signum, frame):
-    print("Shutting down now...")
-    os.remove("ans.pid")
-    running = False
-    sys.exit(0)
 
+class IckyPtangZoopBoingBadHandler(EntRcodeChanger):
+    domains = ["icky.ptang.zoop.boing.bad."]
+    rcode = dns.rcode.NXDOMAIN
 
-############################################################################
-# Main
-#
-# Set up responder and control channel, open the pid file, and start
-# the main loop, listening for queries on the query channel or commands
-# on the control channel and acting on them.
-############################################################################
-ip4 = "10.53.0.4"
-ip6 = "fd92:7065:b8e:ffff::4"
 
-try:
-    port = int(os.environ["PORT"])
-except:
-    port = 5300
+class IckyPtangZoopBoingUglyHandler(EntRcodeChanger):
+    domains = ["icky.ptang.zoop.boing.ugly."]
+    rcode = dns.rcode.FORMERR
 
-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-query4_socket.bind((ip4, port))
 
-havev6 = True
-try:
-    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
-    try:
-        query6_socket.bind((ip6, port))
-    except:
-        query6_socket.close()
-        havev6 = False
-except:
-    havev6 = False
+class IckyPtangZoopBoingSlowHandler(DelayedResponseHandler):
+    domains = ["icky.ptang.zoop.boing.slow."]
+    delay = 0.4
 
-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))
-if havev6:
-    print("Listening on %s port %d" % (ip6, port))
-print("Ctrl-c to quit")
-
-if havev6:
-    input = [query4_socket, query6_socket]
-else:
-    input = [query4_socket]
-
-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_socket or s == query6_socket:
-            print(
-                "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
-            )
-            # Handle incoming queries
-            msg = s.recvfrom(65535)
-            rsp = create_response(msg[0])
-            if rsp:
-                print(dns.rcode.to_text(rsp.rcode()))
-                s.sendto(rsp.to_wire(), msg[1])
-            else:
-                print("NO RESPONSE")
-    if not running:
-        break
+if __name__ == "__main__":
+    server = AsyncDnsServer()
+    server.install_response_handler(QueryLogger())
+    server.install_response_handler(StaleHandler())
+    server.install_response_handler(IckyPtangZoopBoingBadHandler())
+    server.install_response_handler(IckyPtangZoopBoingUglyHandler())
+    server.install_response_handler(IckyPtangZoopBoingSlowHandler())
+    server.run()
diff --git a/bin/tests/system/qmin/ans4/b.stale.db b/bin/tests/system/qmin/ans4/b.stale.db
new file mode 100644 (file)
index 0000000..4dda578
--- /dev/null
@@ -0,0 +1,16 @@
+; 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.
+
+@      1       SOA     ns hostmaster.b.stale. 1 2 3 4 5
+@      1       NS      ns
+ns     1       A       10.53.0.4
+a      1       NS      ns.a
+ns.a   1       A       10.53.0.4
diff --git a/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db
new file mode 100644 (file)
index 0000000..a5d4d08
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@                      1       SOA     ns4.bad. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      a.bit.longer.ns.name.bad.
+icky.icky              1       A       192.0.2.1
+more.icky.icky         1       A       192.0.2.2
diff --git a/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db
new file mode 100644 (file)
index 0000000..152451a
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@                      1       SOA     ns4.good. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      a.bit.longer.ns.name.good.
+icky.icky              1       A       192.0.2.1
+more.icky.icky         1       A       192.0.2.2
diff --git a/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db
new file mode 100644 (file)
index 0000000..091cca4
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@                      1       SOA     ns4.slow. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      a.bit.longer.ns.name.slow.
+icky.icky              1       A       192.0.2.1
+more.icky.icky         1       A       192.0.2.2
diff --git a/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db
new file mode 100644 (file)
index 0000000..fbc71d1
--- /dev/null
@@ -0,0 +1,15 @@
+; 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.
+
+@                      1       SOA     ns4.ugly. hostmaster.arpa. 2018050100 1 1 1 1
+@                      1       NS      a.bit.longer.ns.name.ugly.
+icky.icky              1       A       192.0.2.1
+more.icky.icky         1       A       192.0.2.2
diff --git a/bin/tests/system/qmin/qmin_ans.py b/bin/tests/system/qmin/qmin_ans.py
new file mode 100644 (file)
index 0000000..c610eb5
--- /dev/null
@@ -0,0 +1,107 @@
+"""
+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 typing import AsyncGenerator
+
+import abc
+
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+
+from isctest.asyncserver import (
+    DnsResponseSend,
+    DomainHandler,
+    QueryContext,
+    ResponseAction,
+)
+
+from isctest.compat import dns_rcode
+
+
+def log_query(qctx: QueryContext) -> None:
+    """
+    Log a received DNS query to a text file inspected by `tests.sh`.  AAAA and
+    A queries are logged identically because the relative order in which they
+    are received does not matter.
+    """
+    qname = qctx.qname.to_text()
+    qtype = dns.rdatatype.to_text(qctx.qtype)
+    if qtype in ("A", "AAAA"):
+        qtype = "ADDR"
+
+    with open("query.log", "a", encoding="utf-8") as query_log:
+        print(f"{qtype} {qname}", file=query_log)
+
+
+class QueryLogHandler(DomainHandler):
+    """
+    Log all received DNS queries to a text file.  Use the zone file for
+    preparing responses.
+    """
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        log_query(qctx)
+        yield DnsResponseSend(qctx.response)
+
+
+class EntRcodeChanger(DomainHandler):
+    """
+    Log all received DNS queries to a text file.  Use the zone file for
+    preparing responses, but override the RCODE returned for empty
+    non-terminals (ENTs) to the value specified by the child class.  This
+    emulates broken authoritative servers.
+    """
+
+    @property
+    @abc.abstractmethod
+    def rcode(self) -> dns_rcode:
+        raise NotImplementedError
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        assert qctx.zone
+
+        log_query(qctx)
+
+        if (
+            qctx.response.rcode() == dns.rcode.NOERROR
+            and not qctx.response.answer
+            and qctx.response.authority
+            and qctx.response.authority[0].rdtype == dns.rdatatype.SOA
+            and not qctx.zone.get_node(qctx.qname)
+        ):
+            qctx.response.set_rcode(self.rcode)
+            yield DnsResponseSend(qctx.response)
+
+
+class DelayedResponseHandler(DomainHandler):
+    """
+    Log all received DNS queries to a text file.  Use the zone file for
+    preparing responses, but delay sending every answer by the amount of time
+    specified (in seconds) by the child class.  This emulates network delays.
+    """
+
+    @property
+    @abc.abstractmethod
+    def delay(self) -> float:
+        raise NotImplementedError
+
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[ResponseAction, None]:
+        log_query(qctx)
+        yield DnsResponseSend(qctx.response, delay=self.delay)