xref: /netbsd-src/external/mpl/bind/dist/bin/tests/system/chain/ans4/ans.py (revision fed34e531e9e810cc74ff0ec7d8a513a3f8972ce)
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
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 = []
72
73
74def ctl_channel(msg):
75    global actions, rrs
76
77    msg = msg.splitlines().pop(0)
78    print("received control message: %s" % msg)
79
80    msg = msg.split(b"|")
81    if len(msg) == 0:
82        return
83
84    actions = [x.strip() for x in msg[0].split(b",")]
85    n = functools.reduce(
86        lambda n, act: (n + (2 if act == b"dname" else 1)), [0] + actions
87    )
88
89    if len(msg) == 1:
90        rrs = []
91        for i in range(n):
92            for b in [False, True]:
93                rrs.append((i, b))
94        return
95
96    rlist = [x.strip() for x in msg[1].split(b",")]
97    rrs = []
98    for item in rlist:
99        if item[0] == b"s"[0]:
100            i = int(item[1:].strip()) - 1
101            if i > n:
102                print("invalid index %d" + (i + 1))
103                continue
104            rrs.append((int(item[1:]) - 1, True))
105        else:
106            i = int(item) - 1
107            if i > n:
108                print("invalid index %d" % (i + 1))
109                continue
110            rrs.append((i, False))
111
112
113############################################################################
114# Respond to a DNS query.
115############################################################################
116def create_response(msg):
117    m = dns.message.from_wire(msg)
118    qname = m.question[0].name.to_text()
119    labels = qname.lower().split(".")
120    wantsigs = True if m.ednsflags & dns.flags.DO else False
121
122    # get qtype
123    rrtype = m.question[0].rdtype
124    typename = dns.rdatatype.to_text(rrtype)
125
126    # for 'www.example.com.'...
127    # - name is 'www'
128    # - domain is 'example.com.'
129    # - sld is 'example'
130    # - tld is 'com.'
131    name = labels.pop(0)
132    domain = ".".join(labels)
133    sld = labels.pop(0)
134    tld = ".".join(labels)
135
136    print("query: " + qname + "/" + typename)
137    print("domain: " + domain)
138
139    # default answers, depending on QTYPE.
140    # currently only A, AAAA, TXT and NS are supported.
141    ttl = 86400
142    additionalA = "10.53.0.4"
143    additionalAAAA = "fd92:7065:b8e:ffff::4"
144    if typename == "A":
145        final = "10.53.0.4"
146    elif typename == "AAAA":
147        final = "fd92:7065:b8e:ffff::4"
148    elif typename == "TXT":
149        final = "Some\ text\ here"
150    elif typename == "NS":
151        domain = qname
152        final = "ns1.%s" % domain
153    else:
154        final = None
155
156    # RRSIG rdata - won't validate but will exercise response parsing
157    t = datetime.now()
158    delta = timedelta(30)
159    t1 = t - delta
160    t2 = t + delta
161    inception = t1.strftime("%Y%m%d000000")
162    expiry = t2.strftime("%Y%m%d000000")
163    sigdata = "OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso="
164
165    # construct answer set.
166    answers = []
167    sigs = []
168    curdom = domain
169    curname = name
170    i = 0
171
172    for action in actions:
173        if name != "test":
174            continue
175        if action == b"xname":
176            owner = curname + "." + curdom
177            newname = "cname%d" % i
178            i += 1
179            newdom = "domain%d.%s" % (i, tld)
180            i += 1
181            target = newname + "." + newdom
182            print("add external CNAME %s to %s" % (owner, target))
183            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
184            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
185                ttl,
186                expiry,
187                inception,
188                domain,
189                sigdata,
190            )
191            print("add external RRISG(CNAME) %s to %s" % (owner, target))
192            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
193            curname = newname
194            curdom = newdom
195            continue
196
197        if action == b"cname":
198            owner = curname + "." + curdom
199            newname = "cname%d" % i
200            target = newname + "." + curdom
201            i += 1
202            print("add CNAME %s to %s" % (owner, target))
203            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
204            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
205                ttl,
206                expiry,
207                inception,
208                domain,
209                sigdata,
210            )
211            print("add RRSIG(CNAME) %s to %s" % (owner, target))
212            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
213            curname = newname
214            continue
215
216        if action == b"dname":
217            owner = curdom
218            newdom = "domain%d.%s" % (i, tld)
219            i += 1
220            print("add DNAME %s to %s" % (owner, newdom))
221            answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom))
222            rrsig = "DNAME 5 3 %d %s %s 12345 %s %s" % (
223                ttl,
224                expiry,
225                inception,
226                domain,
227                sigdata,
228            )
229            print("add RRSIG(DNAME) %s to %s" % (owner, newdom))
230            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
231            owner = curname + "." + curdom
232            target = curname + "." + newdom
233            print("add synthesized CNAME %s to %s" % (owner, target))
234            answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target))
235            rrsig = "CNAME 5 3 %d %s %s 12345 %s %s" % (
236                ttl,
237                expiry,
238                inception,
239                domain,
240                sigdata,
241            )
242            print("add synthesized RRSIG(CNAME) %s to %s" % (owner, target))
243            sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
244            curdom = newdom
245            continue
246
247    # now add the final answer
248    owner = curname + "." + curdom
249    answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final))
250    rrsig = "%s 5 3 %d %s %s 12345 %s %s" % (
251        typename,
252        ttl,
253        expiry,
254        inception,
255        domain,
256        sigdata,
257    )
258    sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig))
259
260    # prepare the response and convert to wire format
261    r = dns.message.make_response(m)
262
263    if name != "test":
264        r.answer.append(answers[-1])
265        if wantsigs:
266            r.answer.append(sigs[-1])
267    else:
268        for i, sig in rrs:
269            if sig and not wantsigs:
270                continue
271            elif sig:
272                r.answer.append(sigs[i])
273            else:
274                r.answer.append(answers[i])
275
276    if typename != "NS":
277        r.authority.append(
278            dns.rrset.from_text(domain, ttl, IN, "NS", ("ns1.%s" % domain))
279        )
280    r.additional.append(
281        dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, A, additionalA)
282    )
283    r.additional.append(
284        dns.rrset.from_text(("ns1.%s" % domain), 86400, IN, AAAA, additionalAAAA)
285    )
286
287    r.flags |= dns.flags.AA
288    r.use_edns()
289    return r.to_wire()
290
291
292def sigterm(signum, frame):
293    print("Shutting down now...")
294    os.remove("ans.pid")
295    running = False
296    sys.exit(0)
297
298
299############################################################################
300# Main
301#
302# Set up responder and control channel, open the pid file, and start
303# the main loop, listening for queries on the query channel or commands
304# on the control channel and acting on them.
305############################################################################
306ip4 = "10.53.0.4"
307ip6 = "fd92:7065:b8e:ffff::4"
308
309try:
310    port = int(os.environ["PORT"])
311except:
312    port = 5300
313
314try:
315    ctrlport = int(os.environ["EXTRAPORT1"])
316except:
317    ctrlport = 5300
318
319query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
320query4_socket.bind((ip4, port))
321
322havev6 = True
323try:
324    query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
325    try:
326        query6_socket.bind((ip6, port))
327    except:
328        query6_socket.close()
329        havev6 = False
330except:
331    havev6 = False
332
333ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
334ctrl_socket.bind((ip4, ctrlport))
335ctrl_socket.listen(5)
336
337signal.signal(signal.SIGTERM, sigterm)
338
339f = open("ans.pid", "w")
340pid = os.getpid()
341print(pid, file=f)
342f.close()
343
344running = True
345
346print("Listening on %s port %d" % (ip4, port))
347if havev6:
348    print("Listening on %s port %d" % (ip6, port))
349print("Control channel on %s port %d" % (ip4, ctrlport))
350print("Ctrl-c to quit")
351
352if havev6:
353    input = [query4_socket, query6_socket, ctrl_socket]
354else:
355    input = [query4_socket, ctrl_socket]
356
357while running:
358    try:
359        inputready, outputready, exceptready = select.select(input, [], [])
360    except select.error as e:
361        break
362    except socket.error as e:
363        break
364    except KeyboardInterrupt:
365        break
366
367    for s in inputready:
368        if s == ctrl_socket:
369            # Handle control channel input
370            conn, addr = s.accept()
371            print("Control channel connected")
372            while True:
373                msg = conn.recv(65535)
374                if not msg:
375                    break
376                ctl_channel(msg)
377            conn.close()
378        if s == query4_socket or s == query6_socket:
379            print("Query received on %s" % (ip4 if s == query4_socket else ip6))
380            # Handle incoming queries
381            msg = s.recvfrom(65535)
382            rsp = create_response(msg[0])
383            if rsp:
384                s.sendto(rsp, msg[1])
385    if not running:
386        break
387