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