From: Michał Kępień Date: Tue, 18 Mar 2025 05:19:01 +0000 (+0100) Subject: Use isctest.asyncserver in the "qmin" test X-Git-Tag: v9.21.7~47^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7faa34c6ee40653eeec23ef2df8093564cfc1891;p=thirdparty%2Fbind9.git 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). --- 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 00000000000..7042434e165 --- /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 d372c2003b2..7fa6a6c2c55 --- 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 - -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.`, + 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 index 00000000000..ebb0f7008f1 --- /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 00000000000..ebb0f7008f1 --- /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 00000000000..54a20e653c3 --- /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 00000000000..ebb0f7008f1 --- /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 00000000000..b1b1ac69aac --- /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 00000000000..ebb0f7008f1 --- /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 00000000000..e50c75c3481 --- /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 00000000000..e59589024c3 --- /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 b5ae73c3fa5..057bbb34d5b --- 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. +""" -# 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 index 00000000000..c3a5b529e77 --- /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 00000000000..cc4a3ebb6fe --- /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 00000000000..67635bbecdd --- /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 00000000000..27c5ee08540 --- /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 00000000000..f4058b306aa --- /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 00000000000..68e63ce032d --- /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 f5fe3cc03f8..ca43845a1d5 --- a/bin/tests/system/qmin/ans4/ans.py +++ b/bin/tests/system/qmin/ans4/ans.py @@ -1,345 +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. +""" -# 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 index 00000000000..4dda5787294 --- /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 00000000000..a5d4d08d6d8 --- /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 00000000000..152451a93fd --- /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 00000000000..091cca406cf --- /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 00000000000..fbc71d17c25 --- /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 00000000000..c610eb57263 --- /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)