ans.py revision 1.1.1.1
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 http://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 miliseconds
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    if lqname.endswith("bad."):
72        bad = True
73        suffix = "bad."
74        lqname = lqname[:-4]
75    elif lqname.endswith("ugly."):
76        ugly = True
77        suffix = "ugly."
78        lqname = lqname[:-5]
79    elif lqname.endswith("good."):
80        suffix = "good."
81        lqname = lqname[:-5]
82    elif lqname.endswith("slow."):
83        slow = True
84        suffix = "slow."
85        lqname = lqname[:-5]
86    else:
87        r.set_rcode(REFUSED)
88        return r
89
90    # Good/bad differs only in how we treat non-empty terminals
91    if lqname == "icky.icky.icky.ptang.zoop.boing." and rrtype == A:
92        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.1"))
93        r.flags |= dns.flags.AA
94    elif lqname == "more.icky.icky.icky.ptang.zoop.boing." and rrtype == A:
95        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2"))
96        r.flags |= dns.flags.AA
97    elif lqname == "icky.ptang.zoop.boing." and rrtype == NS:
98        r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name."+suffix))
99        r.flags |= dns.flags.AA
100    elif lqname.endswith("icky.ptang.zoop.boing."):
101        r.authority.append(dns.rrset.from_text("icky.ptang.zoop.boing." + suffix, 1, IN, SOA, "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1"))
102        if bad or not "more.icky.icky.icky.ptang.zoop.boing.".endswith(lqname):
103            r.set_rcode(NXDOMAIN)
104        if ugly:
105            r.set_rcode(FORMERR)
106    else:
107        r.set_rcode(REFUSED)
108
109    if slow:
110        time.sleep(0.4)
111    return r
112
113
114def sigterm(signum, frame):
115    print ("Shutting down now...")
116    os.remove('ans.pid')
117    running = False
118    sys.exit(0)
119
120############################################################################
121# Main
122#
123# Set up responder and control channel, open the pid file, and start
124# the main loop, listening for queries on the query channel or commands
125# on the control channel and acting on them.
126############################################################################
127ip4 = "10.53.0.4"
128ip6 = "fd92:7065:b8e:ffff::4"
129
130try: port=int(os.environ['PORT'])
131except: port=5300
132
133query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
134query4_socket.bind((ip4, port))
135
136havev6 = True
137try:
138    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
139    try:
140        query6_socket.bind((ip6, port))
141    except:
142        query6_socket.close()
143        havev6 = False
144except:
145    havev6 = False
146
147signal.signal(signal.SIGTERM, sigterm)
148
149f = open('ans.pid', 'w')
150pid = os.getpid()
151print (pid, file=f)
152f.close()
153
154running = True
155
156print ("Listening on %s port %d" % (ip4, port))
157if havev6:
158    print ("Listening on %s port %d" % (ip6, port))
159print ("Ctrl-c to quit")
160
161if havev6:
162    input = [query4_socket, query6_socket]
163else:
164    input = [query4_socket]
165
166while running:
167    try:
168        inputready, outputready, exceptready = select.select(input, [], [])
169    except select.error as e:
170        break
171    except socket.error as e:
172        break
173    except KeyboardInterrupt:
174        break
175
176    for s in inputready:
177        if s == query4_socket or s == query6_socket:
178            print ("Query received on %s" %
179                    (ip4 if s == query4_socket else ip6), end=" ")
180            # Handle incoming queries
181            msg = s.recvfrom(65535)
182            rsp = create_response(msg[0])
183            if rsp:
184                print(dns.rcode.to_text(rsp.rcode()))
185                s.sendto(rsp.to_wire(), msg[1])
186            else:
187                print("NO RESPONSE")
188    if not running:
189        break
190