xref: /netbsd-src/external/mpl/bind/dist/bin/tests/system/qmin/ans3/ans.py (revision 4a8a51fcad8393387bdac69675a48d9d46f4fb89)
1# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
2#
3# SPDX-License-Identifier: MPL-2.0
4#
5# This Source Code Form is subject to the terms of the Mozilla Public
6# License, v. 2.0.  If a copy of the MPL was not distributed with this
7# file, you can obtain one at https://mozilla.org/MPL/2.0/.
8#
9# See the COPYRIGHT file distributed with this work for additional
10# information regarding copyright ownership.
11
12from __future__ import print_function
13import os
14import sys
15import signal
16import socket
17import select
18from datetime import datetime, timedelta
19import time
20import functools
21
22import dns, dns.message, dns.query, dns.flags
23from dns.rdatatype import *
24from dns.rdataclass import *
25from dns.rcode import *
26from dns.name import *
27
28
29# Log query to file
30def logquery(type, qname):
31    with open("qlog", "a") as f:
32        f.write("%s %s\n", type, qname)
33
34
35def endswith(domain, labels):
36    return domain.endswith("." + labels) or domain == labels
37
38
39############################################################################
40# Respond to a DNS query.
41# For good. it serves:
42# zoop.boing.good. NS ns3.good.
43# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name.good.
44# it responds properly (with NODATA empty response) to non-empty terminals
45#
46# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
47#
48# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
49#
50# For ugly. it works the same as for good., but returns garbage to non-empty terminals
51#
52# For stale. it serves:
53# a.b.stale. IN TXT peekaboo (resolver did not do qname minimization)
54############################################################################
55def create_response(msg):
56    m = dns.message.from_wire(msg)
57    qname = m.question[0].name.to_text()
58    lqname = qname.lower()
59    labels = lqname.split(".")
60    suffix = ""
61
62    # get qtype
63    rrtype = m.question[0].rdtype
64    typename = dns.rdatatype.to_text(rrtype)
65    if typename == "A" or typename == "AAAA":
66        typename = "ADDR"
67    bad = False
68    ugly = False
69    slow = False
70
71    # log this query
72    with open("query.log", "a") as f:
73        f.write("%s %s\n" % (typename, lqname))
74        print("%s %s" % (typename, lqname), end=" ")
75
76    r = dns.message.make_response(m)
77    r.set_rcode(NOERROR)
78
79    ip6req = False
80
81    if endswith(lqname, "bad."):
82        bad = True
83        suffix = "bad."
84        lqname = lqname[:-4]
85    elif endswith(lqname, "ugly."):
86        ugly = True
87        suffix = "ugly."
88        lqname = lqname[:-5]
89    elif endswith(lqname, "good."):
90        suffix = "good."
91        lqname = lqname[:-5]
92    elif endswith(lqname, "slow."):
93        slow = True
94        suffix = "slow."
95        lqname = lqname[:-5]
96    elif endswith(lqname, "8.2.6.0.1.0.0.2.ip6.arpa."):
97        ip6req = True
98    elif endswith(lqname, "a.b.stale."):
99        if lqname == "a.b.stale.":
100            if rrtype == TXT:
101                # Direct query.
102                r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "peekaboo"))
103                r.flags |= dns.flags.AA
104            elif rrtype == NS:
105                # NS a.b.
106                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale."))
107                r.additional.append(
108                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")
109                )
110                r.flags |= dns.flags.AA
111            elif rrtype == SOA:
112                # SOA a.b.
113                r.answer.append(
114                    dns.rrset.from_text(
115                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
116                    )
117                )
118                r.flags |= dns.flags.AA
119            else:
120                # NODATA.
121                r.authority.append(
122                    dns.rrset.from_text(
123                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
124                    )
125                )
126        else:
127            r.authority.append(
128                dns.rrset.from_text(
129                    lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
130                )
131            )
132            r.set_rcode(NXDOMAIN)
133            # NXDOMAIN.
134        return r
135    else:
136        r.set_rcode(REFUSED)
137        return r
138
139    # Good/bad differs only in how we treat non-empty terminals
140    if lqname == "zoop.boing." and rrtype == NS:
141        r.answer.append(
142            dns.rrset.from_text(lqname + suffix, 1, IN, NS, "ns3." + suffix)
143        )
144        r.flags |= dns.flags.AA
145    elif endswith(lqname, "icky.ptang.zoop.boing."):
146        r.authority.append(
147            dns.rrset.from_text(
148                "icky.ptang.zoop.boing." + suffix,
149                1,
150                IN,
151                NS,
152                "a.bit.longer.ns.name." + suffix,
153            )
154        )
155    elif endswith("icky.ptang.zoop.boing.", lqname):
156        r.authority.append(
157            dns.rrset.from_text(
158                "zoop.boing." + suffix,
159                1,
160                IN,
161                SOA,
162                "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
163            )
164        )
165        if bad:
166            r.set_rcode(NXDOMAIN)
167        if ugly:
168            r.set_rcode(FORMERR)
169    elif endswith(lqname, "zoop.boing."):
170        r.authority.append(
171            dns.rrset.from_text(
172                "zoop.boing." + suffix,
173                1,
174                IN,
175                SOA,
176                "ns3." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
177            )
178        )
179        r.set_rcode(NXDOMAIN)
180    elif ip6req:
181        r.authority.append(
182            dns.rrset.from_text(
183                "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, NS, "ns4.good."
184            )
185        )
186        r.additional.append(dns.rrset.from_text("ns4.good.", 60, IN, A, "10.53.0.4"))
187    else:
188        r.set_rcode(REFUSED)
189
190    if slow:
191        time.sleep(0.4)
192    return r
193
194
195def sigterm(signum, frame):
196    print("Shutting down now...")
197    os.remove("ans.pid")
198    running = False
199    sys.exit(0)
200
201
202############################################################################
203# Main
204#
205# Set up responder and control channel, open the pid file, and start
206# the main loop, listening for queries on the query channel or commands
207# on the control channel and acting on them.
208############################################################################
209ip4 = "10.53.0.3"
210ip6 = "fd92:7065:b8e:ffff::3"
211
212try:
213    port = int(os.environ["PORT"])
214except:
215    port = 5300
216
217query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
218query4_socket.bind((ip4, port))
219
220havev6 = True
221try:
222    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
223    try:
224        query6_socket.bind((ip6, port))
225    except:
226        query6_socket.close()
227        havev6 = False
228except:
229    havev6 = False
230
231signal.signal(signal.SIGTERM, sigterm)
232
233f = open("ans.pid", "w")
234pid = os.getpid()
235print(pid, file=f)
236f.close()
237
238running = True
239
240print("Listening on %s port %d" % (ip4, port))
241if havev6:
242    print("Listening on %s port %d" % (ip6, port))
243print("Ctrl-c to quit")
244
245if havev6:
246    input = [query4_socket, query6_socket]
247else:
248    input = [query4_socket]
249
250while running:
251    try:
252        inputready, outputready, exceptready = select.select(input, [], [])
253    except select.error as e:
254        break
255    except socket.error as e:
256        break
257    except KeyboardInterrupt:
258        break
259
260    for s in inputready:
261        if s == query4_socket or s == query6_socket:
262            print(
263                "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
264            )
265            # Handle incoming queries
266            msg = s.recvfrom(65535)
267            rsp = create_response(msg[0])
268            if rsp:
269                print(dns.rcode.to_text(rsp.rcode()))
270                s.sendto(rsp.to_wire(), msg[1])
271            else:
272                print("NO RESPONSE")
273    if not running:
274        break
275