From 7b456deec32752bd86c387139c5c4f439b7b83d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 18 Mar 2025 06:19:01 +0100 Subject: [PATCH 1/4] Use isctest.asyncserver in the "qmin" test 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). (cherry picked from commit 7faa34c6ee40653eeec23ef2df8093564cfc1891) --- .../system/qmin/ans2/1.0.0.2.ip6.arpa.db | 17 + bin/tests/system/qmin/ans2/ans.py | 527 +++--------------- bin/tests/system/qmin/ans2/bad.db | 1 + bin/tests/system/qmin/ans2/fwd.db | 1 + bin/tests/system/qmin/ans2/good.db | 26 + bin/tests/system/qmin/ans2/slow.db | 1 + bin/tests/system/qmin/ans2/stale.db | 15 + bin/tests/system/qmin/ans2/ugly.db | 1 + .../qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db | 15 + bin/tests/system/qmin/ans3/a.b.stale.db | 15 + bin/tests/system/qmin/ans3/ans.py | 307 ++-------- bin/tests/system/qmin/ans3/zoop.boing.bad.db | 14 + bin/tests/system/qmin/ans3/zoop.boing.good.db | 14 + bin/tests/system/qmin/ans3/zoop.boing.slow.db | 14 + bin/tests/system/qmin/ans3/zoop.boing.ugly.db | 14 + .../ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db | 15 + bin/tests/system/qmin/ans4/a.b.stale.db | 15 + bin/tests/system/qmin/ans4/ans.py | 409 +++----------- bin/tests/system/qmin/ans4/b.stale.db | 16 + .../qmin/ans4/icky.ptang.zoop.boing.bad.db | 15 + .../qmin/ans4/icky.ptang.zoop.boing.good.db | 15 + .../qmin/ans4/icky.ptang.zoop.boing.slow.db | 15 + .../qmin/ans4/icky.ptang.zoop.boing.ugly.db | 15 + bin/tests/system/qmin/qmin_ans.py | 107 ++++ 24 files changed, 565 insertions(+), 1039 deletions(-) create mode 100644 bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db mode change 100755 => 100644 bin/tests/system/qmin/ans2/ans.py create mode 120000 bin/tests/system/qmin/ans2/bad.db create mode 120000 bin/tests/system/qmin/ans2/fwd.db create mode 100644 bin/tests/system/qmin/ans2/good.db create mode 120000 bin/tests/system/qmin/ans2/slow.db create mode 100644 bin/tests/system/qmin/ans2/stale.db create mode 120000 bin/tests/system/qmin/ans2/ugly.db create mode 100644 bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db create mode 100644 bin/tests/system/qmin/ans3/a.b.stale.db mode change 100755 => 100644 bin/tests/system/qmin/ans3/ans.py create mode 100644 bin/tests/system/qmin/ans3/zoop.boing.bad.db create mode 100644 bin/tests/system/qmin/ans3/zoop.boing.good.db create mode 100644 bin/tests/system/qmin/ans3/zoop.boing.slow.db create mode 100644 bin/tests/system/qmin/ans3/zoop.boing.ugly.db create mode 100644 bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db create mode 100644 bin/tests/system/qmin/ans4/a.b.stale.db mode change 100755 => 100644 bin/tests/system/qmin/ans4/ans.py create mode 100644 bin/tests/system/qmin/ans4/b.stale.db create mode 100644 bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db create mode 100644 bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db create mode 100644 bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db create mode 100644 bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db create mode 100644 bin/tests/system/qmin/qmin_ans.py 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 index 0000000000..7042434e16 --- /dev/null +++ b/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db @@ -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. diff --git a/bin/tests/system/qmin/ans2/ans.py b/bin/tests/system/qmin/ans2/ans.py old mode 100755 new mode 100644 index d372c2003b..7fa6a6c2c5 --- a/bin/tests/system/qmin/ans2/ans.py +++ b/bin/tests/system/qmin/ans2/ans.py @@ -1,456 +1,111 @@ -# 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. +""" + +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, +) -# Log query to file -def logquery(type, qname): - with open("qlog", "a") as f: - f.write("%s %s\n", type, qname) +class QueryLogger(QueryLogHandler): + domains = ["1.0.0.2.ip6.arpa.", "fwd.", "good."] -def endswith(domain, labels): - return domain.endswith("." + labels) or domain == labels +class BadHandler(EntRcodeChanger): + domains = ["bad."] + rcode = dns.rcode.NXDOMAIN -############################################################################ -# 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 +class UglyHandler(EntRcodeChanger): + domains = ["ugly."] + rcode = dns.rcode.FORMERR -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +class SlowHandler(DelayedResponseHandler): + domains = ["slow."] + delay = 0.2 -############################################################################ -# 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" +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.`, + 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 + ) -try: - port = int(os.environ["PORT"]) -except: - port = 5300 + response = dns.message.make_response(qctx.query) + response.set_rcode(dns.rcode.NOERROR) + response.authority.append(ns_rrset) + response.additional.append(a_rrset) -query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -query4_socket.bind((ip4, port)) + return DnsResponseSend(response, authoritative=False) -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) +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. + """ -f = open("ans.pid", "w") -pid = os.getpid() -print(pid, file=f) -f.close() + domains = ["stale."] -running = True + 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") -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(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 index 0000000000..ebb0f7008f --- /dev/null +++ b/bin/tests/system/qmin/ans2/bad.db @@ -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 index 0000000000..ebb0f7008f --- /dev/null +++ b/bin/tests/system/qmin/ans2/fwd.db @@ -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 index 0000000000..54a20e653c --- /dev/null +++ b/bin/tests/system/qmin/ans2/good.db @@ -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 index 0000000000..ebb0f7008f --- /dev/null +++ b/bin/tests/system/qmin/ans2/slow.db @@ -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 index 0000000000..b1b1ac69aa --- /dev/null +++ b/bin/tests/system/qmin/ans2/stale.db @@ -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 index 0000000000..ebb0f7008f --- /dev/null +++ b/bin/tests/system/qmin/ans2/ugly.db @@ -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 index 0000000000..e50c75c348 --- /dev/null +++ b/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db @@ -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 index 0000000000..e59589024c --- /dev/null +++ b/bin/tests/system/qmin/ans3/a.b.stale.db @@ -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 diff --git a/bin/tests/system/qmin/ans3/ans.py b/bin/tests/system/qmin/ans3/ans.py old mode 100755 new mode 100644 index b5ae73c3fa..057bbb34d5 --- a/bin/tests/system/qmin/ans3/ans.py +++ b/bin/tests/system/qmin/ans3/ans.py @@ -1,285 +1,46 @@ -# 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. +""" + +import dns.rcode + +from isctest.asyncserver import AsyncDnsServer + +from qmin_ans import DelayedResponseHandler, EntRcodeChanger, QueryLogHandler -# Log query to file -def logquery(type, qname): - with open("qlog", "a") as f: - f.write("%s %s\n", type, qname) +class QueryLogger(QueryLogHandler): + domains = ["8.2.6.0.1.0.0.2.ip6.arpa.", "a.b.stale.", "zoop.boing.good."] -def endswith(domain, labels): - return domain.endswith("." + labels) or domain == labels +class ZoopBoingBadHandler(EntRcodeChanger): + domains = ["zoop.boing.bad."] + rcode = dns.rcode.NXDOMAIN -############################################################################ -# 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 = "" - - # 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) - - ip6req = False - - 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) - - if slow: - time.sleep(0.4) - return r +class ZoopBoingUglyHandler(EntRcodeChanger): + domains = ["zoop.boing.ugly."] + rcode = dns.rcode.FORMERR -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +class ZoopBoingSlowHandler(DelayedResponseHandler): + domains = ["zoop.boing.slow."] + delay = 0.4 -############################################################################ -# 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 index 0000000000..c3a5b529e7 --- /dev/null +++ b/bin/tests/system/qmin/ans3/zoop.boing.bad.db @@ -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 index 0000000000..cc4a3ebb6f --- /dev/null +++ b/bin/tests/system/qmin/ans3/zoop.boing.good.db @@ -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 index 0000000000..67635bbecd --- /dev/null +++ b/bin/tests/system/qmin/ans3/zoop.boing.slow.db @@ -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 index 0000000000..27c5ee0854 --- /dev/null +++ b/bin/tests/system/qmin/ans3/zoop.boing.ugly.db @@ -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 index 0000000000..f4058b306a --- /dev/null +++ b/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db @@ -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 index 0000000000..68e63ce032 --- /dev/null +++ b/bin/tests/system/qmin/ans4/a.b.stale.db @@ -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" diff --git a/bin/tests/system/qmin/ans4/ans.py b/bin/tests/system/qmin/ans4/ans.py old mode 100755 new mode 100644 index 517217aec1..ca43845a1d --- a/bin/tests/system/qmin/ans4/ans.py +++ b/bin/tests/system/qmin/ans4/ans.py @@ -1,344 +1,93 @@ -# 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. +""" + +from typing import AsyncGenerator + +import dns.rcode + +from isctest.asyncserver import ( + AsyncDnsServer, + DnsResponseSend, + DomainHandler, + QueryContext, + ResponseAction, +) + +from qmin_ans import DelayedResponseHandler, EntRcodeChanger, QueryLogHandler, log_query -# Log query to file -def logquery(type, qname): - with open("qlog", "a") as f: - f.write("%s %s\n", type, qname) +class QueryLogger(QueryLogHandler): + domains = [ + "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", + "icky.ptang.zoop.boing.good.", + ] -def endswith(domain, labels): - return domain.endswith("." + labels) or domain == labels +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. + """ + + domains = ["b.stale", "a.b.stale"] + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + log_query(qctx) + + if qctx.qtype == dns.rdatatype.NS: + assert qctx.zone + assert qctx.response.answer[0] + + 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) -############################################################################ -# 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 - - # 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) - - ip6req = False - - 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. - 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: - # 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 - - # 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 slow: - time.sleep(0.4) - return r +class IckyPtangZoopBoingBadHandler(EntRcodeChanger): + domains = ["icky.ptang.zoop.boing.bad."] + rcode = dns.rcode.NXDOMAIN -def sigterm(signum, frame): - print("Shutting down now...") - os.remove("ans.pid") - running = False - sys.exit(0) +class IckyPtangZoopBoingUglyHandler(EntRcodeChanger): + domains = ["icky.ptang.zoop.boing.ugly."] + rcode = dns.rcode.FORMERR -############################################################################ -# 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" +class IckyPtangZoopBoingSlowHandler(DelayedResponseHandler): + domains = ["icky.ptang.zoop.boing.slow."] + delay = 0.4 -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(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 index 0000000000..4dda578729 --- /dev/null +++ b/bin/tests/system/qmin/ans4/b.stale.db @@ -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 index 0000000000..a5d4d08d6d --- /dev/null +++ b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db @@ -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 index 0000000000..152451a93f --- /dev/null +++ b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db @@ -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 index 0000000000..091cca406c --- /dev/null +++ b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db @@ -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 index 0000000000..fbc71d17c2 --- /dev/null +++ b/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db @@ -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 index 0000000000..c610eb5726 --- /dev/null +++ b/bin/tests/system/qmin/qmin_ans.py @@ -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) From 0f53c1c6e5d69d5d063102e2b5cbc0eb5561d548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 18 Mar 2025 06:19:01 +0100 Subject: [PATCH 2/4] Fix PYTHONPATH set for ans.py servers by start.pl Commit 6c010a5644324947c8c13b5600cd8d988ae7684f caused the PYTHONPATH environment variable to be set for ans.py servers started using start.pl. However, no system test has actually used the new isctest.asyncserver module since that change was applied, so it has not been noticed until now that including the source directory in PYTHONPATH is only sufficient for in-tree builds. Include the build directory instead of the source directory in the PYTHONPATH environment variable set for ans.py servers started by start.pl so that they work correctly for both in-tree and out-of-tree builds. (cherry picked from commit a799dd04adc08a062ec9961a026573abcc7c9181) --- bin/tests/system/start.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/tests/system/start.pl b/bin/tests/system/start.pl index cabbfc1602..d4a0b40b5b 100755 --- a/bin/tests/system/start.pl +++ b/bin/tests/system/start.pl @@ -324,7 +324,7 @@ sub construct_ans_command { } if (-e "$testdir/$server/ans.py") { - $ENV{'PYTHONPATH'} = $testdir . ":" . $ENV{'srcdir'}; + $ENV{'PYTHONPATH'} = $testdir . ":" . $builddir; $command = "$PYTHON -u ans.py 10.53.0.$n $queryport"; } elsif (-e "$testdir/$server/ans.pl") { $command = "$PERL ans.pl"; From 5a26c218ac62902d2c14c623809bf0bb68329a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 18 Mar 2025 06:19:01 +0100 Subject: [PATCH 3/4] Ignore .hypothesis files created by system tests Some versions of the Hypothesis Python library - notably the one included in stock OS repositories for Ubuntu 20.04 Focal Fossa - cause a .hypothesis file to be created in a Python script's working directory when the hypothesis module is present in its import chain. Ignore such files by adding them to the list of expected test artifacts to prevent pytest teardown checks from failing due to these files appearing in the file system after running system tests. (cherry picked from commit f413ddbe5f2edfdeedc41603dcd2afe105ed2844) --- bin/tests/system/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/tests/system/conftest.py b/bin/tests/system/conftest.py index 0abc30df86..2c80777640 100644 --- a/bin/tests/system/conftest.py +++ b/bin/tests/system/conftest.py @@ -308,6 +308,7 @@ def logger(request, system_test_name): @pytest.fixture(scope="module") def expected_artifacts(request): common_artifacts = [ + "*/.hypothesis", # drop after Ubuntu 20.04 Focal Fossa gets removed from CI ".libs/*", # possible build artifacts, see GL #5055 "ns*/named.conf", "ns*/named.memstats", From c5ae1a7f54faf08f73ed7adc4506f4b48f3f99c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Tue, 18 Mar 2025 06:19:01 +0100 Subject: [PATCH 4/4] Broaden vulture exclude glob for ans.py servers The vulture tool seems to be unable to follow how the parent classes defined in bin/tests/system/qmin/qmin_ans.py use mandatory properties specified by child classes in bin/tests/system/qmin/ans*/ans.py. Make the tool ignore not just ans.py servers, but also *_ans.py utility modules above the ansX/ subdirectories to prevent false positives about unused code from causing CI pipeline failures. (cherry picked from commit dfd37918d6913b783ead915d608b5951386f5974) --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f71cf9ac41..7f79870fea 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -585,7 +585,7 @@ vulture: <<: *precheck_job needs: [] script: - - vulture --exclude "*/ans*/ans.py,conftest.py,isctest" --ignore-names "pytestmark" bin/tests/system/ + - vulture --exclude "*ans.py,conftest.py,isctest" --ignore-names "pytestmark" bin/tests/system/ ci-variables: stage: precheck