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