ans.py revision 1.1.1.3
1############################################################################
2# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
3#
4# This Source Code Form is subject to the terms of the Mozilla Public
5# License, v. 2.0. If a copy of the MPL was not distributed with this
6# file, you can obtain one at https://mozilla.org/MPL/2.0/.
7#
8# See the COPYRIGHT file distributed with this work for additional
9# information regarding copyright ownership.
10############################################################################
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############################################################################
35# Respond to a DNS query.
36# For good. it serves:
37# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name.
38# icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.1
39# more.icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.2
40# it responds properly (with NODATA empty response) to non-empty terminals
41#
42# For slow. it works the same as for good., but each response is delayed by 400 milliseconds
43#
44# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals
45#
46# For ugly. it works the same as for good., but returns garbage to non-empty terminals
47############################################################################
48def create_response(msg):
49    m = dns.message.from_wire(msg)
50    qname = m.question[0].name.to_text()
51    lqname = qname.lower()
52    labels = lqname.split('.')
53
54    # get qtype
55    rrtype = m.question[0].rdtype
56    typename = dns.rdatatype.to_text(rrtype)
57    if typename == "A" or typename == "AAAA":
58        typename = "ADDR"
59    bad = False
60    slow = False
61    ugly = False
62
63    # log this query
64    with open("query.log", "a") as f:
65        f.write("%s %s\n" % (typename, lqname))
66        print("%s %s" % (typename, lqname), end=" ")
67
68    r = dns.message.make_response(m)
69    r.set_rcode(NOERROR)
70
71    ip6req = False
72
73    if lqname.endswith("bad."):
74        bad = True
75        suffix = "bad."
76        lqname = lqname[:-4]
77    elif lqname.endswith("ugly."):
78        ugly = True
79        suffix = "ugly."
80        lqname = lqname[:-5]
81    elif lqname.endswith("good."):
82        suffix = "good."
83        lqname = lqname[:-5]
84    elif lqname.endswith("slow."):
85        slow = True
86        suffix = "slow."
87        lqname = lqname[:-5]
88    elif lqname.endswith("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."):
89        ip6req = True
90    else:
91        r.set_rcode(REFUSED)
92        return r
93
94    # Good/bad differs only in how we treat non-empty terminals
95    if lqname == "icky.icky.icky.ptang.zoop.boing." and rrtype == A:
96        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.1"))
97        r.flags |= dns.flags.AA
98    elif lqname == "more.icky.icky.icky.ptang.zoop.boing." and rrtype == A:
99        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
100        r.flags |= dns.flags.AA
101    elif lqname == "icky.ptang.zoop.boing." and rrtype == NS:
102        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name."+suffix))
103        r.flags |= dns.flags.AA
104    elif lqname.endswith("icky.ptang.zoop.boing."):
105        r.authority.append(dns.rrset.from_text("icky.ptang.zoop.boing." + suffix, 1, IN, SOA, "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1"))
106        if bad or not "more.icky.icky.icky.ptang.zoop.boing.".endswith(lqname):
107            r.set_rcode(NXDOMAIN)
108        if ugly:
109            r.set_rcode(FORMERR)
110    elif ip6req:
111        r.flags |= dns.flags.AA
112        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:
113            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"))
114        elif "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.".endswith(lqname):
115            #NODATA answer
116            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"))
117        else:
118            # NXDOMAIN
119            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"))
120            r.set_rcode(NXDOMAIN)
121    else:
122        r.set_rcode(REFUSED)
123
124    if slow:
125        time.sleep(0.4)
126    return r
127
128
129def sigterm(signum, frame):
130    print ("Shutting down now...")
131    os.remove('ans.pid')
132    running = False
133    sys.exit(0)
134
135############################################################################
136# Main
137#
138# Set up responder and control channel, open the pid file, and start
139# the main loop, listening for queries on the query channel or commands
140# on the control channel and acting on them.
141############################################################################
142ip4 = "10.53.0.4"
143ip6 = "fd92:7065:b8e:ffff::4"
144
145try: port=int(os.environ['PORT'])
146except: port=5300
147
148query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
149query4_socket.bind((ip4, port))
150
151havev6 = True
152try:
153    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
154    try:
155        query6_socket.bind((ip6, port))
156    except:
157        query6_socket.close()
158        havev6 = False
159except:
160    havev6 = False
161
162signal.signal(signal.SIGTERM, sigterm)
163
164f = open('ans.pid', 'w')
165pid = os.getpid()
166print (pid, file=f)
167f.close()
168
169running = True
170
171print ("Listening on %s port %d" % (ip4, port))
172if havev6:
173    print ("Listening on %s port %d" % (ip6, port))
174print ("Ctrl-c to quit")
175
176if havev6:
177    input = [query4_socket, query6_socket]
178else:
179    input = [query4_socket]
180
181while running:
182    try:
183        inputready, outputready, exceptready = select.select(input, [], [])
184    except select.error as e:
185        break
186    except socket.error as e:
187        break
188    except KeyboardInterrupt:
189        break
190
191    for s in inputready:
192        if s == query4_socket or s == query6_socket:
193            print ("Query received on %s" %
194                    (ip4 if s == query4_socket else ip6), end=" ")
195            # Handle incoming queries
196            msg = s.recvfrom(65535)
197            rsp = create_response(msg[0])
198            if rsp:
199                print(dns.rcode.to_text(rsp.rcode()))
200                s.sendto(rsp.to_wire(), msg[1])
201            else:
202                print("NO RESPONSE")
203    if not running:
204        break
205