ans.py revision 1.1.1.4
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# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name.
43# icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.1
44# more.icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.2
45# it responds properly (with NODATA empty response) to non-empty terminals
46#
47# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
48#
49# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
50#
51# For ugly. it works the same as for good., but returns garbage to non-empty terminals
52#
53# For stale. it serves:
54# a.b.stale. IN TXT hooray (resolver did do qname minimization)
55############################################################################
56def create_response(msg):
57    m = dns.message.from_wire(msg)
58    qname = m.question[0].name.to_text()
59    lqname = qname.lower()
60    labels = lqname.split(".")
61    suffix = ""
62
63    # get qtype
64    rrtype = m.question[0].rdtype
65    typename = dns.rdatatype.to_text(rrtype)
66    if typename == "A" or typename == "AAAA":
67        typename = "ADDR"
68    bad = False
69    slow = False
70    ugly = False
71
72    # log this query
73    with open("query.log", "a") as f:
74        f.write("%s %s\n" % (typename, lqname))
75        print("%s %s" % (typename, lqname), end=" ")
76
77    r = dns.message.make_response(m)
78    r.set_rcode(NOERROR)
79
80    ip6req = False
81
82    if endswith(lqname, "bad."):
83        bad = True
84        suffix = "bad."
85        lqname = lqname[:-4]
86    elif endswith(lqname, "ugly."):
87        ugly = True
88        suffix = "ugly."
89        lqname = lqname[:-5]
90    elif endswith(lqname, "good."):
91        suffix = "good."
92        lqname = lqname[:-5]
93    elif endswith(lqname, "slow."):
94        slow = True
95        suffix = "slow."
96        lqname = lqname[:-5]
97    elif endswith(lqname, "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."):
98        ip6req = True
99    elif endswith(lqname, "b.stale."):
100        if lqname == "a.b.stale.":
101            if rrtype == TXT:
102                # Direct query.
103                r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "hooray"))
104                r.flags |= dns.flags.AA
105            elif rrtype == NS:
106                # NS a.b.
107                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale."))
108                r.additional.append(
109                    dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")
110                )
111                r.flags |= dns.flags.AA
112            elif rrtype == SOA:
113                # SOA a.b.
114                r.answer.append(
115                    dns.rrset.from_text(
116                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
117                    )
118                )
119                r.flags |= dns.flags.AA
120            else:
121                # NODATA.
122                r.authority.append(
123                    dns.rrset.from_text(
124                        lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5"
125                    )
126                )
127        elif lqname == "b.stale.":
128            if rrtype == NS:
129                # NS b.
130                r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.b.stale."))
131                r.additional.append(
132                    dns.rrset.from_text("ns.b.stale.", 1, IN, A, "10.53.0.4")
133                )
134                r.flags |= dns.flags.AA
135            elif rrtype == SOA:
136                # SOA b.
137                r.answer.append(
138                    dns.rrset.from_text(
139                        lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
140                    )
141                )
142                r.flags |= dns.flags.AA
143            else:
144                # NODATA.
145                r.authority.append(
146                    dns.rrset.from_text(
147                        lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
148                    )
149                )
150        else:
151            r.authority.append(
152                dns.rrset.from_text(
153                    lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5"
154                )
155            )
156            r.set_rcode(NXDOMAIN)
157            # NXDOMAIN.
158        return r
159    else:
160        r.set_rcode(REFUSED)
161        return r
162
163    # Good/bad differs only in how we treat non-empty terminals
164    if lqname == "icky.icky.icky.ptang.zoop.boing." and rrtype == A:
165        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.1"))
166        r.flags |= dns.flags.AA
167    elif lqname == "more.icky.icky.icky.ptang.zoop.boing." and rrtype == A:
168        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
169        r.flags |= dns.flags.AA
170    elif lqname == "icky.ptang.zoop.boing." and rrtype == NS:
171        r.answer.append(
172            dns.rrset.from_text(
173                lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name." + suffix
174            )
175        )
176        r.flags |= dns.flags.AA
177    elif endswith(lqname, "icky.ptang.zoop.boing."):
178        r.authority.append(
179            dns.rrset.from_text(
180                "icky.ptang.zoop.boing." + suffix,
181                1,
182                IN,
183                SOA,
184                "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1",
185            )
186        )
187        if bad or not endswith("more.icky.icky.icky.ptang.zoop.boing.", lqname):
188            r.set_rcode(NXDOMAIN)
189        if ugly:
190            r.set_rcode(FORMERR)
191    elif ip6req:
192        r.flags |= dns.flags.AA
193        if (
194            lqname
195            == "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."
196            and rrtype == TXT
197        ):
198            r.answer.append(
199                dns.rrset.from_text(
200                    "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.",
201                    1,
202                    IN,
203                    TXT,
204                    "long_ip6_name",
205                )
206            )
207        elif endswith(
208            "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.",
209            lqname,
210        ):
211            # NODATA answer
212            r.authority.append(
213                dns.rrset.from_text(
214                    "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
215                    60,
216                    IN,
217                    SOA,
218                    "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16",
219                )
220            )
221        else:
222            # NXDOMAIN
223            r.authority.append(
224                dns.rrset.from_text(
225                    "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.",
226                    60,
227                    IN,
228                    SOA,
229                    "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16",
230                )
231            )
232            r.set_rcode(NXDOMAIN)
233    else:
234        r.set_rcode(REFUSED)
235
236    if slow:
237        time.sleep(0.4)
238    return r
239
240
241def sigterm(signum, frame):
242    print("Shutting down now...")
243    os.remove("ans.pid")
244    running = False
245    sys.exit(0)
246
247
248############################################################################
249# Main
250#
251# Set up responder and control channel, open the pid file, and start
252# the main loop, listening for queries on the query channel or commands
253# on the control channel and acting on them.
254############################################################################
255ip4 = "10.53.0.4"
256ip6 = "fd92:7065:b8e:ffff::4"
257
258try:
259    port = int(os.environ["PORT"])
260except:
261    port = 5300
262
263query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
264query4_socket.bind((ip4, port))
265
266havev6 = True
267try:
268    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
269    try:
270        query6_socket.bind((ip6, port))
271    except:
272        query6_socket.close()
273        havev6 = False
274except:
275    havev6 = False
276
277signal.signal(signal.SIGTERM, sigterm)
278
279f = open("ans.pid", "w")
280pid = os.getpid()
281print(pid, file=f)
282f.close()
283
284running = True
285
286print("Listening on %s port %d" % (ip4, port))
287if havev6:
288    print("Listening on %s port %d" % (ip6, port))
289print("Ctrl-c to quit")
290
291if havev6:
292    input = [query4_socket, query6_socket]
293else:
294    input = [query4_socket]
295
296while running:
297    try:
298        inputready, outputready, exceptready = select.select(input, [], [])
299    except select.error as e:
300        break
301    except socket.error as e:
302        break
303    except KeyboardInterrupt:
304        break
305
306    for s in inputready:
307        if s == query4_socket or s == query6_socket:
308            print(
309                "Query received on %s" % (ip4 if s == query4_socket else ip6), end=" "
310            )
311            # Handle incoming queries
312            msg = s.recvfrom(65535)
313            rsp = create_response(msg[0])
314            if rsp:
315                print(dns.rcode.to_text(rsp.rcode()))
316                s.sendto(rsp.to_wire(), msg[1])
317            else:
318                print("NO RESPONSE")
319    if not running:
320        break
321