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
12############################################################################
13# ans.py: See README.anspy for details.
14############################################################################
15
16from __future__ import print_function
17import os
18import sys
19import signal
20import socket
21import select
22from datetime import datetime, timedelta
23import functools
24
25import dns, dns.message, dns.query
26from dns.rdatatype import *
27from dns.rdataclass import *
28from dns.rcode import *
29from dns.name import *
30
31############################################################################
32# set up the RRs to be returned in the next answer
33#
34# the message contains up to two pipe-separated ('|') fields.
35#
36# the first field of the message is a comma-separated list
37# of actions indicating what to put into the answer set
38# (e.g., a dname, a cname, another cname, etc)
39#
40# supported actions:
41# - cname (cname from the current name to a new one in the same domain)
42# - dname (dname to a new domain, plus a synthesized cname)
43# - xname ("external" cname, to a new name in a new domain)
44#
45# example: xname, dname, cname represents a CNAME to an external
46# domain which is then answered by a DNAME and synthesized
47# CNAME pointing to yet another domain, which is then answered
48# by a CNAME within the same domain, and finally an answer
49# to the query. each RR in the answer set has a corresponding
50# RRSIG. these signatures are not valid, but will exercise the
51# response parser.
52#
53# the second field is a comma-separated list of which RRs in the
54# answer set to include in the answer, in which order. if prepended
55# with 's', the number indicates which signature to include.
56#
57# examples: for the answer set "cname, cname, cname", an rr set
58# '1, s1, 2, s2, 3, s3, 4, s4' indicates that all four RRs should
59# be included in the answer, with siagntures, in the original
60# order, while 4, s4, 3, s3, 2, s2, 1, s1' indicates the order
61# should be reversed, 's3, s3, s3, s3' indicates that the third
62# RRSIG should be repeated four times and everything else should
63# be omitted, and so on.
64#
65# if there is no second field (i.e., no pipe symbol appears in
66# the line) , the default is to send all answers and signatures.
67# if a pipe symbol exists but the second field is empty, then
68# nothing is sent at all.
69############################################################################
70actions = []
71rrs = []
72def ctl_channel(msg):
73    global actions, rrs
74
75    msg = msg.splitlines().pop(0)
76    print ('received control message: %s' % msg)
77
78    msg = msg.split(b'|')
79    if len(msg) == 0:
80        return
81
82    actions = [x.strip() for x in msg[0].split(b',')]
83    n = functools.reduce(lambda n, act: (n + (2 if act == b'dname' else 1)), [0] + actions)
84
85    if len(msg) == 1:
86        rrs = []
87        for i in range(n):
88            for b in [False, True]:
89                rrs.append((i, b))
90        return
91
92    rlist = [x.strip() for x in msg[1].split(b',')]
93    rrs = []
94    for item in rlist:
95        if item[0] == b's'[0]:
96            i = int(item[1:].strip()) - 1
97            if i > n:
98                print ('invalid index %d' + (i + 1))
99                continue
100            rrs.append((int(item[1:]) - 1, True))
101        else:
102            i = int(item) - 1
103            if i > n:
104                print ('invalid index %d' % (i + 1))
105                continue
106            rrs.append((i, False))
107
108############################################################################
109# Respond to a DNS query.
110############################################################################
111def create_response(msg):
112    m = dns.message.from_wire(msg)
113    qname = m.question[0].name.to_text()
114    labels = qname.lower().split('.')
115    wantsigs = True if m.ednsflags & dns.flags.DO else False
116
117    # get qtype
118    rrtype = m.question[0].rdtype
119    typename = dns.rdatatype.to_text(rrtype)
120
121    # for 'www.example.com.'...
122    # - name is 'www'
123    # - domain is 'example.com.'
124    # - sld is 'example'
125    # - tld is 'com.'
126    name = labels.pop(0)
127    domain = '.'.join(labels)
128    sld = labels.pop(0)
129    tld = '.'.join(labels)
130
131    print ('query: ' + qname + '/' + typename)
132    print ('domain: ' + domain)
133
134    # default answers, depending on QTYPE.
135    # currently only A, AAAA, TXT and NS are supported.
136    ttl = 86400
137    additionalA = '10.53.0.4'
138    additionalAAAA = 'fd92:7065:b8e:ffff::4'
139    if typename == 'A':
140        final = '10.53.0.4'
141    elif typename == 'AAAA':
142        final = 'fd92:7065:b8e:ffff::4'
143    elif typename == 'TXT':
144        final = 'Some\ text\ here'
145    elif typename == 'NS':
146        domain = qname
147        final = ('ns1.%s' % domain)
148    else:
149        final = None
150
151    # RRSIG rdata - won't validate but will exercise response parsing
152    t = datetime.now()
153    delta = timedelta(30)
154    t1 = t - delta
155    t2 = t + delta
156    inception=t1.strftime('%Y%m%d000000')
157    expiry=t2.strftime('%Y%m%d000000')
158    sigdata='OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso='
159
160    # construct answer set.
161    answers = []
162    sigs = []
163    curdom = domain
164    curname = name
165    i = 0
166
167    for action in actions:
168        if name != 'test':
169            continue
170        if action == b'xname':
171            owner = curname + '.' + curdom
172            newname = 'cname%d' % i
173            i += 1
174            newdom = 'domain%d.%s' % (i, tld)
175            i += 1
176            target = newname + '.' + newdom
177            print ('add external CNAME %s to %s' % (owner, target))
178            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
179            rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \
180               (ttl, expiry, inception, domain, sigdata)
181            print ('add external RRISG(CNAME) %s to %s' % (owner, target))
182            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
183            curname = newname
184            curdom = newdom
185            continue
186
187        if action == b'cname':
188            owner = curname + '.' + curdom
189            newname = 'cname%d' % i
190            target = newname + '.' + curdom
191            i += 1
192            print ('add CNAME %s to %s' % (owner, target))
193            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
194            rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \
195                   (ttl, expiry, inception, domain, sigdata)
196            print ('add RRSIG(CNAME) %s to %s' % (owner, target))
197            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
198            curname = newname
199            continue
200
201        if action == b'dname':
202            owner = curdom
203            newdom = 'domain%d.%s' % (i, tld)
204            i += 1
205            print ('add DNAME %s to %s' % (owner, newdom))
206            answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom))
207            rrsig = 'DNAME 5 3 %d %s %s 12345 %s %s' % \
208                   (ttl, expiry, inception, domain, sigdata)
209            print ('add RRSIG(DNAME) %s to %s' % (owner, newdom))
210            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
211            owner = curname + '.' + curdom
212            target = curname + '.' + newdom
213            print ('add synthesized CNAME %s to %s' % (owner, target))
214            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
215            rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \
216                   (ttl, expiry, inception, domain, sigdata)
217            print ('add synthesized RRSIG(CNAME) %s to %s' % (owner, target))
218            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
219            curdom = newdom
220            continue
221
222    # now add the final answer
223    owner = curname + '.' + curdom
224    answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final))
225    rrsig = '%s 5 3 %d %s %s 12345 %s %s' % \
226               (typename, ttl, expiry, inception, domain, sigdata)
227    sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
228
229    # prepare the response and convert to wire format
230    r = dns.message.make_response(m)
231
232    if name != 'test':
233        r.answer.append(answers[-1])
234        if wantsigs:
235            r.answer.append(sigs[-1])
236    else:
237        for (i, sig) in rrs:
238            if sig and not wantsigs:
239                continue
240            elif sig:
241                r.answer.append(sigs[i])
242            else:
243                r.answer.append(answers[i])
244
245    if typename != 'NS':
246        r.authority.append(dns.rrset.from_text(domain, ttl, IN, "NS",
247                                               ("ns1.%s" % domain)))
248    r.additional.append(dns.rrset.from_text(('ns1.%s' % domain), 86400,
249                                             IN, A, additionalA))
250    r.additional.append(dns.rrset.from_text(('ns1.%s' % domain), 86400,
251                                             IN, AAAA, additionalAAAA))
252
253    r.flags |= dns.flags.AA
254    r.use_edns()
255    return r.to_wire()
256
257def sigterm(signum, frame):
258    print ("Shutting down now...")
259    os.remove('ans.pid')
260    running = False
261    sys.exit(0)
262
263############################################################################
264# Main
265#
266# Set up responder and control channel, open the pid file, and start
267# the main loop, listening for queries on the query channel or commands
268# on the control channel and acting on them.
269############################################################################
270ip4 = "10.53.0.4"
271ip6 = "fd92:7065:b8e:ffff::4"
272
273try: port=int(os.environ['PORT'])
274except: port=5300
275
276try: ctrlport=int(os.environ['EXTRAPORT1'])
277except: ctrlport=5300
278
279query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
280query4_socket.bind((ip4, port))
281
282havev6 = True
283try:
284    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
285    try:
286        query6_socket.bind((ip6, port))
287    except:
288        query6_socket.close()
289        havev6 = False
290except:
291    havev6 = False
292
293ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
294ctrl_socket.bind((ip4, ctrlport))
295ctrl_socket.listen(5)
296
297signal.signal(signal.SIGTERM, sigterm)
298
299f = open('ans.pid', 'w')
300pid = os.getpid()
301print (pid, file=f)
302f.close()
303
304running = True
305
306print ("Listening on %s port %d" % (ip4, port))
307if havev6:
308    print ("Listening on %s port %d" % (ip6, port))
309print ("Control channel on %s port %d" % (ip4, ctrlport))
310print ("Ctrl-c to quit")
311
312if havev6:
313    input = [query4_socket, query6_socket, ctrl_socket]
314else:
315    input = [query4_socket, ctrl_socket]
316
317while running:
318    try:
319        inputready, outputready, exceptready = select.select(input, [], [])
320    except select.error as e:
321        break
322    except socket.error as e:
323        break
324    except KeyboardInterrupt:
325        break
326
327    for s in inputready:
328        if s == ctrl_socket:
329            # Handle control channel input
330            conn, addr = s.accept()
331            print ("Control channel connected")
332            while True:
333                msg = conn.recv(65535)
334                if not msg:
335                    break
336                ctl_channel(msg)
337            conn.close()
338        if s == query4_socket or s == query6_socket:
339            print ("Query received on %s" %
340                    (ip4 if s == query4_socket else ip6))
341            # Handle incoming queries
342            msg = s.recvfrom(65535)
343            rsp = create_response(msg[0])
344            if rsp:
345                s.sendto(rsp, msg[1])
346    if not running:
347        break
348