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 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", 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) 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";