--- /dev/null
+; 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.
-# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
-#
-# SPDX-License-Identifier: MPL-2.0
-#
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, you can obtain one at https://mozilla.org/MPL/2.0/.
-#
-# See the COPYRIGHT file distributed with this work for additional
-# information regarding copyright ownership.
+"""
+Copyright (C) Internet Systems Consortium, Inc. ("ISC")
-from __future__ import print_function
-import os
-import sys
-import signal
-import socket
-import select
-from datetime import datetime, timedelta
-import time
-import functools
-
-import dns, dns.message, dns.query, dns.flags
-from dns.rdatatype import *
-from dns.rdataclass import *
-from dns.rcode import *
-from dns.name import *
-
-
-# Log query to file
-def logquery(type, qname):
- with open("qlog", "a") as f:
- f.write("%s %s\n", type, qname)
-
-
-def endswith(domain, labels):
- return domain.endswith("." + labels) or domain == labels
-
-
-############################################################################
-# Respond to a DNS query.
-# For good. it serves:
-# ns2.good. IN A 10.53.0.2
-# zoop.boing.good. NS ns3.good.
-# ns3.good. IN A 10.53.0.3
-# too.many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.good. A 192.0.2.2
-# it responds properly (with NODATA empty response) to non-empty terminals
-#
-# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
-#
-# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
-#
-# For ugly. it works the same as for good., but returns garbage to non-empty terminals
-#
-# For 1.0.0.2.ip6.arpa it serves
-# 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa. IN PTR nee.com.
-# 8.2.6.0.1.0.0.2.ip6.arpa IN NS ns3.good
-# 1.0.0.2.ip6.arpa. IN NS ns2.good
-# ip6.arpa. IN NS ns2.good
-#
-# For stale. it serves:
-# a.b. NS ns.a.b.stale.
-# ns.a.b.stale. IN A 10.53.0.3
-# b. NS ns.b.stale.
-# ns.b.stale. IN A 10.53.0.4
-############################################################################
-def create_response(msg):
- m = dns.message.from_wire(msg)
- qname = m.question[0].name.to_text()
- lqname = qname.lower()
- labels = lqname.split(".")
-
- # get qtype
- rrtype = m.question[0].rdtype
- typename = dns.rdatatype.to_text(rrtype)
- if typename == "A" or typename == "AAAA":
- typename = "ADDR"
- bad = False
- ugly = False
- slow = False
-
- # log this query
- with open("query.log", "a") as f:
- f.write("%s %s\n" % (typename, lqname))
- print("%s %s" % (typename, lqname), end=" ")
-
- r = dns.message.make_response(m)
- r.set_rcode(NOERROR)
-
- if endswith(lqname, "1.0.0.2.ip6.arpa."):
- # Direct query - give direct answer
- if endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."):
- # Delegate to ns3
- r.authority.append(
- dns.rrset.from_text(
- "8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, NS, "ns3.good."
- )
- )
- r.additional.append(
- dns.rrset.from_text("ns3.good.", 60, IN, A, "10.53.0.3")
- )
- elif (
- lqname
- == "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa."
- and rrtype == PTR
- ):
- # Direct query - give direct answer
- r.answer.append(
- dns.rrset.from_text(
- "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.",
- 1,
- IN,
- PTR,
- "nee.com.",
- )
- )
- r.flags |= dns.flags.AA
- elif lqname == "1.0.0.2.ip6.arpa." and rrtype == NS:
- # NS query at the apex
- r.answer.append(
- dns.rrset.from_text("1.0.0.2.ip6.arpa.", 30, IN, NS, "ns2.good.")
- )
- r.flags |= dns.flags.AA
- elif endswith(
- "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0.1.0.0.2.ip6.arpa.",
- lqname,
- ):
- # NODATA answer
- r.authority.append(
- dns.rrset.from_text(
- "1.0.0.2.ip6.arpa.",
- 30,
- IN,
- SOA,
- "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- else:
- # NXDOMAIN
- r.authority.append(
- dns.rrset.from_text(
- "1.0.0.2.ip6.arpa.",
- 30,
- IN,
- SOA,
- "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- r.set_rcode(NXDOMAIN)
- return r
- elif endswith(lqname, "ip6.arpa."):
- if lqname == "ip6.arpa." and rrtype == NS:
- # NS query at the apex
- r.answer.append(dns.rrset.from_text("ip6.arpa.", 30, IN, NS, "ns2.good."))
- r.flags |= dns.flags.AA
- elif endswith("1.0.0.2.ip6.arpa.", lqname):
- # NODATA answer
- r.authority.append(
- dns.rrset.from_text(
- "ip6.arpa.",
- 30,
- IN,
- SOA,
- "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- else:
- # NXDOMAIN
- r.authority.append(
- dns.rrset.from_text(
- "ip6.arpa.",
- 30,
- IN,
- SOA,
- "ns2.good. hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- r.set_rcode(NXDOMAIN)
- return r
- elif endswith(lqname, "stale."):
- if endswith(lqname, "a.b.stale."):
- # Delegate to ns.a.b.stale.
- r.authority.append(
- dns.rrset.from_text("a.b.stale.", 2, IN, NS, "ns.a.b.stale.")
- )
- r.additional.append(
- dns.rrset.from_text("ns.a.b.stale.", 2, IN, A, "10.53.0.3")
- )
- elif endswith(lqname, "b.stale."):
- # Delegate to ns.b.stale.
- r.authority.append(
- dns.rrset.from_text("b.stale.", 2, IN, NS, "ns.b.stale.")
- )
- r.additional.append(
- dns.rrset.from_text("ns.b.stale.", 2, IN, A, "10.53.0.4")
- )
- elif lqname == "stale." and rrtype == NS:
- # NS query at the apex.
- r.answer.append(dns.rrset.from_text("stale.", 2, IN, NS, "ns2.stale."))
- r.flags |= dns.flags.AA
- elif lqname == "stale." and rrtype == SOA:
- # SOA query at the apex.
- r.answer.append(
- dns.rrset.from_text(
- "stale.", 2, IN, SOA, "ns2.stale. hostmaster.stale. 1 2 3 4 5"
- )
- )
- r.flags |= dns.flags.AA
- elif lqname == "stale.":
- # NODATA answer
- r.authority.append(
- dns.rrset.from_text(
- "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
- )
- )
- r.flags |= dns.flags.AA
- elif lqname == "ns2.stale.":
- if rrtype == A:
- r.additional.append(
- dns.rrset.from_text("ns.b.stale.", 2, IN, A, "10.53.0.2")
- )
- else:
- r.authority.append(
- dns.rrset.from_text(
- "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
- )
- )
- r.flags |= dns.flags.AA
- else:
- # NXDOMAIN
- r.authority.append(
- dns.rrset.from_text(
- "stale.", 2, IN, SOA, "ns2.stale. hostmaster.arpa. 1 2 3 4 5"
- )
- )
- r.set_rcode(NXDOMAIN)
- return r
- elif endswith(lqname, "bad."):
- bad = True
- suffix = "bad."
- lqname = lqname[:-4]
- elif endswith(lqname, "ugly."):
- ugly = True
- suffix = "ugly."
- lqname = lqname[:-5]
- elif endswith(lqname, "good."):
- suffix = "good."
- lqname = lqname[:-5]
- elif endswith(lqname, "slow."):
- slow = True
- suffix = "slow."
- lqname = lqname[:-5]
- elif endswith(lqname, "fwd."):
- suffix = "fwd."
- lqname = lqname[:-4]
- else:
- r.set_rcode(REFUSED)
- return r
-
- # Good/bad/ugly differs only in how we treat non-empty terminals
- if endswith(lqname, "zoop.boing."):
- r.authority.append(
- dns.rrset.from_text("zoop.boing." + suffix, 1, IN, NS, "ns3." + suffix)
- )
- elif (
- lqname == "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z."
- and rrtype == A
- ):
- r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
- r.flags |= dns.flags.AA
- elif lqname == "" and rrtype == NS:
- r.answer.append(dns.rrset.from_text(suffix, 30, IN, NS, "ns2." + suffix))
- r.flags |= dns.flags.AA
- elif lqname == "ns2.":
- r.flags |= dns.flags.AA
- if rrtype == A:
- r.answer.append(
- dns.rrset.from_text("ns2." + suffix, 30, IN, A, "10.53.0.2")
- )
- elif rrtype == AAAA:
- r.answer.append(
- dns.rrset.from_text(
- "ns2." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::2"
- )
- )
- else:
- r.authority.append(
- dns.rrset.from_text(
- suffix,
- 30,
- IN,
- SOA,
- "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- elif lqname == "ns3.":
- r.flags |= dns.flags.AA
- if rrtype == A:
- r.answer.append(
- dns.rrset.from_text("ns3." + suffix, 30, IN, A, "10.53.0.3")
- )
- elif lqname == "ns3." and rrtype == AAAA:
- r.answer.append(
- dns.rrset.from_text(
- "ns3." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::3"
- )
- )
- else:
- r.authority.append(
- dns.rrset.from_text(
- suffix,
- 30,
- IN,
- SOA,
- "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- elif lqname == "ns4.":
- r.flags |= dns.flags.AA
- if rrtype == A:
- r.answer.append(
- dns.rrset.from_text("ns4." + suffix, 30, IN, A, "10.53.0.4")
- )
- elif rrtype == AAAA:
- r.answer.append(
- dns.rrset.from_text(
- "ns4." + suffix, 30, IN, AAAA, "fd92:7065:b8e:ffff::4"
- )
- )
- else:
- r.authority.append(
- dns.rrset.from_text(
- suffix,
- 30,
- IN,
- SOA,
- "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- elif lqname == "a.bit.longer.ns.name." and rrtype == A:
- r.answer.append(
- dns.rrset.from_text("a.bit.longer.ns.name." + suffix, 1, IN, A, "10.53.0.4")
- )
- r.flags |= dns.flags.AA
- elif lqname == "a.bit.longer.ns.name." and rrtype == AAAA:
- r.answer.append(
- dns.rrset.from_text(
- "a.bit.longer.ns.name." + suffix, 1, IN, AAAA, "fd92:7065:b8e:ffff::4"
- )
- )
- r.flags |= dns.flags.AA
- else:
- r.authority.append(
- dns.rrset.from_text(
- suffix,
- 1,
- IN,
- SOA,
- "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
- )
- )
- if bad or not (
- endswith("icky.icky.icky.ptang.zoop.boing.", lqname)
- or endswith(
- "many.labels.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.",
- lqname,
- )
- or endswith("a.bit.longer.ns.name.", lqname)
- ):
- r.set_rcode(NXDOMAIN)
- if ugly:
- r.set_rcode(FORMERR)
- if slow:
- time.sleep(0.2)
- return r
-
-
-def sigterm(signum, frame):
- print("Shutting down now...")
- os.remove("ans.pid")
- running = False
- sys.exit(0)
-
-
-############################################################################
-# Main
-#
-# Set up responder and control channel, open the pid file, and start
-# the main loop, listening for queries on the query channel or commands
-# on the control channel and acting on them.
-############################################################################
-ip4 = "10.53.0.2"
-ip6 = "fd92:7065:b8e:ffff::2"
-
-try:
- port = int(os.environ["PORT"])
-except:
- port = 5300
-
-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-query4_socket.bind((ip4, port))
-
-havev6 = True
-try:
- query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
- try:
- query6_socket.bind((ip6, port))
- except:
- query6_socket.close()
- havev6 = False
-except:
- havev6 = False
-
-signal.signal(signal.SIGTERM, sigterm)
-
-f = open("ans.pid", "w")
-pid = os.getpid()
-print(pid, file=f)
-f.close()
-
-running = True
-
-print("Listening on %s port %d" % (ip4, port))
-if havev6:
- print("Listening on %s port %d" % (ip6, port))
-print("Ctrl-c to quit")
-
-if havev6:
- input = [query4_socket, query6_socket]
-else:
- input = [query4_socket]
-
-while running:
- try:
- inputready, outputready, exceptready = select.select(input, [], [])
- except select.error as e:
- break
- except socket.error as e:
- break
- except KeyboardInterrupt:
- break
-
- for s in inputready:
- if s == query4_socket or s == query6_socket:
- print(
- "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
- )
- # Handle incoming queries
- msg = s.recvfrom(65535)
- rsp = create_response(msg[0])
- if rsp:
- print(dns.rcode.to_text(rsp.rcode()))
- s.sendto(rsp.to_wire(), msg[1])
- else:
- print("NO RESPONSE")
- if not running:
- break
+SPDX-License-Identifier: MPL-2.0
+
+This Source Code Form is subject to the terms of the Mozilla Public
+License, v. 2.0. If a copy of the MPL was not distributed with this
+file, you can obtain one at https://mozilla.org/MPL/2.0/.
+
+See the COPYRIGHT file distributed with this work for additional
+information regarding copyright ownership.
+"""
+
+from typing import AsyncGenerator
+
+import dns.message
+import dns.name
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+
+from isctest.asyncserver import (
+ AsyncDnsServer,
+ DnsResponseSend,
+ DomainHandler,
+ QueryContext,
+ ResponseAction,
+)
+
+from qmin_ans import (
+ DelayedResponseHandler,
+ EntRcodeChanger,
+ QueryLogHandler,
+ log_query,
+)
+
+
+class QueryLogger(QueryLogHandler):
+ domains = ["1.0.0.2.ip6.arpa.", "fwd.", "good."]
+
+
+class BadHandler(EntRcodeChanger):
+ domains = ["bad."]
+ rcode = dns.rcode.NXDOMAIN
+
+
+class UglyHandler(EntRcodeChanger):
+ domains = ["ugly."]
+ rcode = dns.rcode.FORMERR
+
+
+class SlowHandler(DelayedResponseHandler):
+ domains = ["slow."]
+ delay = 0.2
+
+
+def send_delegation(
+ qctx: QueryContext, zone_cut: dns.name.Name, target_addr: str
+) -> ResponseAction:
+ """
+ Delegate `zone_cut` to a single in-bailiwick name server, `ns.<zone_cut>`,
+ with a single IPv4 glue record (provided in `target_addr`) included in the
+ ADDITIONAL section.
+ """
+ ns_name = "ns." + zone_cut.to_text()
+ ns_rrset = dns.rrset.from_text(
+ zone_cut, 2, dns.rdataclass.IN, dns.rdatatype.NS, ns_name
+ )
+ a_rrset = dns.rrset.from_text(
+ ns_name, 2, dns.rdataclass.IN, dns.rdatatype.A, target_addr
+ )
+
+ response = dns.message.make_response(qctx.query)
+ response.set_rcode(dns.rcode.NOERROR)
+ response.authority.append(ns_rrset)
+ response.additional.append(a_rrset)
+
+ return DnsResponseSend(response, authoritative=False)
+
+
+class StaleHandler(DomainHandler):
+ """
+ `a.b.stale` is a subdomain of `b.stale` and these two subdomains need to be
+ delegated to different name servers. Therefore, their delegations cannot
+ be placed in the zone file because the zone cut at `b.stale` would occlude
+ the one at `a.b.stale`. Generate these delegations dynamically depending
+ on the QNAME.
+ """
+
+ domains = ["stale."]
+
+ async def get_responses(
+ self, qctx: QueryContext
+ ) -> AsyncGenerator[ResponseAction, None]:
+ log_query(qctx)
+ a_b_stale = dns.name.from_text("a.b.stale.")
+ b_stale = dns.name.from_text("b.stale.")
+ if qctx.qname.is_subdomain(a_b_stale):
+ yield send_delegation(qctx, a_b_stale, "10.53.0.3")
+ elif qctx.qname.is_subdomain(b_stale):
+ yield send_delegation(qctx, b_stale, "10.53.0.4")
+
+
+if __name__ == "__main__":
+ server = AsyncDnsServer()
+ server.install_response_handler(QueryLogger())
+ server.install_response_handler(BadHandler())
+ server.install_response_handler(UglyHandler())
+ server.install_response_handler(SlowHandler())
+ server.install_response_handler(StaleHandler())
+ server.run()
--- /dev/null
+good.db
\ No newline at end of file
--- /dev/null
+good.db
\ No newline at end of file
--- /dev/null
+; 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
--- /dev/null
+good.db
\ No newline at end of file
--- /dev/null
+; 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
--- /dev/null
+good.db
\ No newline at end of file
--- /dev/null
+; 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.
--- /dev/null
+; 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
-# 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()
--- /dev/null
+; 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.
--- /dev/null
+; 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.
--- /dev/null
+; 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.
--- /dev/null
+; 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.
--- /dev/null
+; 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"
--- /dev/null
+; 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"
-# 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()
--- /dev/null
+; 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
--- /dev/null
+; 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
--- /dev/null
+; 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
--- /dev/null
+; 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
--- /dev/null
+; 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
--- /dev/null
+"""
+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)