############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. ############################################################################ ############################################################################ # ans.py: See README.anspy for details. ############################################################################ from __future__ import print_function import os import sys import signal import socket import select from datetime import datetime, timedelta import functools import dns, dns.message, dns.query from dns.rdatatype import * from dns.rdataclass import * from dns.rcode import * from dns.name import * ############################################################################ # set up the RRs to be returned in the next answer # # the message contains up to two pipe-separated ('|') fields. # # the first field of the message is a comma-separated list # of actions indicating what to put into the answer set # (e.g., a dname, a cname, another cname, etc) # # supported actions: # - cname (cname from the current name to a new one in the same domain) # - dname (dname to a new domain, plus a synthesized cname) # - xname ("external" cname, to a new name in a new domain) # # example: xname, dname, cname represents a CNAME to an external # domain which is then answered by a DNAME and synthesized # CNAME pointing to yet another domain, which is then answered # by a CNAME within the same domain, and finally an answer # to the query. each RR in the answer set has a corresponding # RRSIG. these signatures are not valid, but will exercise the # response parser. # # the second field is a comma-separated list of which RRs in the # answer set to include in the answer, in which order. if prepended # with 's', the number indicates which signature to include. # # examples: for the answer set "cname, cname, cname", an rr set # '1, s1, 2, s2, 3, s3, 4, s4' indicates that all four RRs should # be included in the answer, with siagntures, in the origninal # order, while 4, s4, 3, s3, 2, s2, 1, s1' indicates the order # should be reversed, 's3, s3, s3, s3' indicates that the third # RRSIG should be repeated four times and everything else should # be omitted, and so on. # # if there is no second field (i.e., no pipe symbol appears in # the line) , the default is to send all answers and signatures. # if a pipe symbol exists but the second field is empty, then # nothing is sent at all. ############################################################################ actions = [] rrs = [] def ctl_channel(msg): global actions, rrs msg = msg.splitlines().pop(0) print ('received control message: %s' % msg) msg = msg.split(b'|') if len(msg) == 0: return actions = [x.strip() for x in msg[0].split(b',')] n = functools.reduce(lambda n, act: (n + (2 if act == b'dname' else 1)), [0] + actions) if len(msg) == 1: rrs = [] for i in range(n): for b in [False, True]: rrs.append((i, b)) return rlist = [x.strip() for x in msg[1].split(b',')] rrs = [] for item in rlist: if item[0] == b's'[0]: i = int(item[1:].strip()) - 1 if i > n: print ('invalid index %d' + (i + 1)) continue rrs.append((int(item[1:]) - 1, True)) else: i = int(item) - 1 if i > n: print ('invalid index %d' % (i + 1)) continue rrs.append((i, False)) ############################################################################ # Respond to a DNS query. ############################################################################ def create_response(msg): m = dns.message.from_wire(msg) qname = m.question[0].name.to_text() labels = qname.lower().split('.') wantsigs = True if m.ednsflags & dns.flags.DO else False # get qtype rrtype = m.question[0].rdtype typename = dns.rdatatype.to_text(rrtype) # for 'www.example.com.'... # - name is 'www' # - domain is 'example.com.' # - sld is 'example' # - tld is 'com.' name = labels.pop(0) domain = '.'.join(labels) sld = labels.pop(0) tld = '.'.join(labels) print ('query: ' + qname + '/' + typename) print ('domain: ' + domain) # default answers, depending on QTYPE. # currently only A, AAAA, TXT and NS are supported. ttl = 86400 additionalA = '10.53.0.4' additionalAAAA = 'fd92:7065:b8e:ffff::4' if typename == 'A': final = '10.53.0.4' elif typename == 'AAAA': final = 'fd92:7065:b8e:ffff::4' elif typename == 'TXT': final = 'Some\ text\ here' elif typename == 'NS': domain = qname final = ('ns1.%s' % domain) else: final = None # RRSIG rdata - won't validate but will exercise response parsing t = datetime.now() delta = timedelta(30) t1 = t - delta t2 = t + delta inception=t1.strftime('%Y%m%d000000') expiry=t2.strftime('%Y%m%d000000') sigdata='OCXH2De0yE4NMTl9UykvOsJ4IBGs/ZIpff2rpaVJrVG7jQfmj50otBAp A0Zo7dpBU4ofv0N/F2Ar6LznCncIojkWptEJIAKA5tHegf/jY39arEpO cevbGp6DKxFhlkLXNcw7k9o7DSw14OaRmgAjXdTFbrl4AiAa0zAttFko Tso=' # construct answer set. answers = [] sigs = [] curdom = domain curname = name i = 0 for action in actions: if name != 'test': continue if action == b'xname': owner = curname + '.' + curdom newname = 'cname%d' % i i += 1 newdom = 'domain%d.%s' % (i, tld) i += 1 target = newname + '.' + newdom print ('add external CNAME %s to %s' % (owner, target)) answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \ (ttl, expiry, inception, domain, sigdata) print ('add external RRISG(CNAME) %s to %s' % (owner, target)) sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) curname = newname curdom = newdom continue if action == b'cname': owner = curname + '.' + curdom newname = 'cname%d' % i target = newname + '.' + curdom i += 1 print ('add CNAME %s to %s' % (owner, target)) answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \ (ttl, expiry, inception, domain, sigdata) print ('add RRSIG(CNAME) %s to %s' % (owner, target)) sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) curname = newname continue if action == b'dname': owner = curdom newdom = 'domain%d.%s' % (i, tld) i += 1 print ('add DNAME %s to %s' % (owner, newdom)) answers.append(dns.rrset.from_text(owner, ttl, IN, DNAME, newdom)) rrsig = 'DNAME 5 3 %d %s %s 12345 %s %s' % \ (ttl, expiry, inception, domain, sigdata) print ('add RRSIG(DNAME) %s to %s' % (owner, newdom)) sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) owner = curname + '.' + curdom target = curname + '.' + newdom print ('add synthesized CNAME %s to %s' % (owner, target)) answers.append(dns.rrset.from_text(owner, ttl, IN, CNAME, target)) rrsig = 'CNAME 5 3 %d %s %s 12345 %s %s' % \ (ttl, expiry, inception, domain, sigdata) print ('add synthesized RRSIG(CNAME) %s to %s' % (owner, target)) sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) curdom = newdom continue # now add the final answer owner = curname + '.' + curdom answers.append(dns.rrset.from_text(owner, ttl, IN, rrtype, final)) rrsig = '%s 5 3 %d %s %s 12345 %s %s' % \ (typename, ttl, expiry, inception, domain, sigdata) sigs.append(dns.rrset.from_text(owner, ttl, IN, RRSIG, rrsig)) # prepare the response and convert to wire format r = dns.message.make_response(m) if name != 'test': r.answer.append(answers[-1]) if wantsigs: r.answer.append(sigs[-1]) else: for (i, sig) in rrs: if sig and not wantsigs: continue elif sig: r.answer.append(sigs[i]) else: r.answer.append(answers[i]) if typename != 'NS': r.authority.append(dns.rrset.from_text(domain, ttl, IN, "NS", ("ns1.%s" % domain))) r.additional.append(dns.rrset.from_text(('ns1.%s' % domain), 86400, IN, A, additionalA)) r.additional.append(dns.rrset.from_text(('ns1.%s' % domain), 86400, IN, AAAA, additionalAAAA)) r.flags |= dns.flags.AA r.use_edns() return r.to_wire() def sigterm(signum, frame): print ("Shutting down now...") os.remove('ans.pid') running = False sys.exit(0) ############################################################################ # Main # # Set up responder and control channel, open the pid file, and start # the main loop, listening for queries on the query channel or commands # on the control channel and acting on them. ############################################################################ ip4 = "10.53.0.4" ip6 = "fd92:7065:b8e:ffff::4" try: port=int(os.environ['PORT']) except: port=5300 try: ctrlport=int(os.environ['EXTRAPORT1']) except: ctrlport=5300 query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) query4_socket.bind((ip4, port)) havev6 = True try: query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) try: query6_socket.bind((ip6, port)) except: query6_socket.close() havev6 = False except: havev6 = False ctrl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ctrl_socket.bind((ip4, ctrlport)) ctrl_socket.listen(5) signal.signal(signal.SIGTERM, sigterm) f = open('ans.pid', 'w') pid = os.getpid() print (pid, file=f) f.close() running = True print ("Listening on %s port %d" % (ip4, port)) if havev6: print ("Listening on %s port %d" % (ip6, port)) print ("Control channel on %s port %d" % (ip4, ctrlport)) print ("Ctrl-c to quit") if havev6: input = [query4_socket, query6_socket, ctrl_socket] else: input = [query4_socket, ctrl_socket] while running: try: inputready, outputready, exceptready = select.select(input, [], []) except select.error as e: break except socket.error as e: break except KeyboardInterrupt: break for s in inputready: if s == ctrl_socket: # Handle control channel input conn, addr = s.accept() print ("Control channel connected") while True: msg = conn.recv(65535) if not msg: break ctl_channel(msg) conn.close() if s == query4_socket or s == query6_socket: print ("Query received on %s" % (ip4 if s == query4_socket else ip6)) # Handle incoming queries msg = s.recvfrom(65535) rsp = create_response(msg[0]) if rsp: s.sendto(rsp, msg[1]) if not running: break