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 12from __future__ import print_function 13import os 14import sys 15import signal 16import socket 17import select 18from datetime import datetime, timedelta 19import time 20import functools 21 22import dns, dns.message, dns.query, dns.flags 23from dns.rdatatype import * 24from dns.rdataclass import * 25from dns.rcode import * 26from dns.name import * 27 28 29# Log query to file 30def logquery(type, qname): 31 with open("qlog", "a") as f: 32 f.write("%s %s\n", type, qname) 33 34def endswith(domain, labels): 35 return domain.endswith("." + labels) or domain == labels 36 37############################################################################ 38# Respond to a DNS query. 39# For good. it serves: 40# icky.ptang.zoop.boing.good. NS a.bit.longer.ns.name. 41# icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.1 42# more.icky.icky.icky.ptang.zoop.boing.good. A 192.0.2.2 43# it responds properly (with NODATA empty response) to non-empty terminals 44# 45# For slow. it works the same as for good., but each response is delayed by 400 milliseconds 46# 47# For bad. it works the same as for good., but returns NXDOMAIN to non-empty terminals 48# 49# For ugly. it works the same as for good., but returns garbage to non-empty terminals 50# 51# For stale. it serves: 52# a.b.stale. IN TXT hooray (resolver did do qname minimization) 53############################################################################ 54def create_response(msg): 55 m = dns.message.from_wire(msg) 56 qname = m.question[0].name.to_text() 57 lqname = qname.lower() 58 labels = lqname.split('.') 59 60 # get qtype 61 rrtype = m.question[0].rdtype 62 typename = dns.rdatatype.to_text(rrtype) 63 if typename == "A" or typename == "AAAA": 64 typename = "ADDR" 65 bad = False 66 slow = False 67 ugly = False 68 69 # log this query 70 with open("query.log", "a") as f: 71 f.write("%s %s\n" % (typename, lqname)) 72 print("%s %s" % (typename, lqname), end=" ") 73 74 r = dns.message.make_response(m) 75 r.set_rcode(NOERROR) 76 77 ip6req = False 78 79 if endswith(lqname, "bad."): 80 bad = True 81 suffix = "bad." 82 lqname = lqname[:-4] 83 elif endswith(lqname, "ugly."): 84 ugly = True 85 suffix = "ugly." 86 lqname = lqname[:-5] 87 elif endswith(lqname, "good."): 88 suffix = "good." 89 lqname = lqname[:-5] 90 elif endswith(lqname, "slow."): 91 slow = True 92 suffix = "slow." 93 lqname = lqname[:-5] 94 elif endswith(lqname, "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa."): 95 ip6req = True 96 elif endswith(lqname, "b.stale."): 97 if lqname == "a.b.stale.": 98 if rrtype == TXT: 99 # Direct query. 100 r.answer.append(dns.rrset.from_text(lqname, 1, IN, TXT, "hooray")) 101 r.flags |= dns.flags.AA 102 elif rrtype == NS: 103 # NS a.b. 104 r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.a.b.stale.")) 105 r.additional.append(dns.rrset.from_text("ns.a.b.stale.", 1, IN, A, "10.53.0.3")) 106 r.flags |= dns.flags.AA 107 elif rrtype == SOA: 108 # SOA a.b. 109 r.answer.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) 110 r.flags |= dns.flags.AA 111 else: 112 # NODATA. 113 r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "a.b.stale. hostmaster.a.b.stale. 1 2 3 4 5")) 114 elif lqname == "b.stale.": 115 if rrtype == NS: 116 # NS b. 117 r.answer.append(dns.rrset.from_text(lqname, 1, IN, NS, "ns.b.stale.")) 118 r.additional.append(dns.rrset.from_text("ns.b.stale.", 1, IN, A, "10.53.0.4")) 119 r.flags |= dns.flags.AA 120 elif rrtype == SOA: 121 # SOA b. 122 r.answer.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) 123 r.flags |= dns.flags.AA 124 else: 125 # NODATA. 126 r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) 127 else: 128 r.authority.append(dns.rrset.from_text(lqname, 1, IN, SOA, "b.stale. hostmaster.b.stale. 1 2 3 4 5")) 129 r.set_rcode(NXDOMAIN) 130 # NXDOMAIN. 131 return r 132 else: 133 r.set_rcode(REFUSED) 134 return r 135 136 # Good/bad differs only in how we treat non-empty terminals 137 if lqname == "icky.icky.icky.ptang.zoop.boing." and rrtype == A: 138 r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.1")) 139 r.flags |= dns.flags.AA 140 elif lqname == "more.icky.icky.icky.ptang.zoop.boing." and rrtype == A: 141 r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, A, "192.0.2.2")) 142 r.flags |= dns.flags.AA 143 elif lqname == "icky.ptang.zoop.boing." and rrtype == NS: 144 r.answer.append(dns.rrset.from_text(lqname + suffix, 1, IN, NS, "a.bit.longer.ns.name."+suffix)) 145 r.flags |= dns.flags.AA 146 elif endswith(lqname, "icky.ptang.zoop.boing."): 147 r.authority.append(dns.rrset.from_text("icky.ptang.zoop.boing." + suffix, 1, IN, SOA, "ns2." + suffix + " hostmaster.arpa. 2018050100 1 1 1 1")) 148 if bad or not endswith("more.icky.icky.icky.ptang.zoop.boing.", lqname): 149 r.set_rcode(NXDOMAIN) 150 if ugly: 151 r.set_rcode(FORMERR) 152 elif ip6req: 153 r.flags |= dns.flags.AA 154 if lqname == "test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa." and rrtype == TXT: 155 r.answer.append(dns.rrset.from_text("test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 1, IN, TXT, "long_ip6_name")) 156 elif endswith("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4.1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", lqname): 157 #NODATA answer 158 r.authority.append(dns.rrset.from_text("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, SOA, "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16")) 159 else: 160 # NXDOMAIN 161 r.authority.append(dns.rrset.from_text("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", 60, IN, SOA, "ns4.good. hostmaster.arpa. 2018050100 120 30 320 16")) 162 r.set_rcode(NXDOMAIN) 163 else: 164 r.set_rcode(REFUSED) 165 166 if slow: 167 time.sleep(0.4) 168 return r 169 170 171def sigterm(signum, frame): 172 print ("Shutting down now...") 173 os.remove('ans.pid') 174 running = False 175 sys.exit(0) 176 177############################################################################ 178# Main 179# 180# Set up responder and control channel, open the pid file, and start 181# the main loop, listening for queries on the query channel or commands 182# on the control channel and acting on them. 183############################################################################ 184ip4 = "10.53.0.4" 185ip6 = "fd92:7065:b8e:ffff::4" 186 187try: port=int(os.environ['PORT']) 188except: port=5300 189 190query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 191query4_socket.bind((ip4, port)) 192 193havev6 = True 194try: 195 query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 196 try: 197 query6_socket.bind((ip6, port)) 198 except: 199 query6_socket.close() 200 havev6 = False 201except: 202 havev6 = False 203 204signal.signal(signal.SIGTERM, sigterm) 205 206f = open('ans.pid', 'w') 207pid = os.getpid() 208print (pid, file=f) 209f.close() 210 211running = True 212 213print ("Listening on %s port %d" % (ip4, port)) 214if havev6: 215 print ("Listening on %s port %d" % (ip6, port)) 216print ("Ctrl-c to quit") 217 218if havev6: 219 input = [query4_socket, query6_socket] 220else: 221 input = [query4_socket] 222 223while running: 224 try: 225 inputready, outputready, exceptready = select.select(input, [], []) 226 except select.error as e: 227 break 228 except socket.error as e: 229 break 230 except KeyboardInterrupt: 231 break 232 233 for s in inputready: 234 if s == query4_socket or s == query6_socket: 235 print ("Query received on %s" % 236 (ip4 if s == query4_socket else ip6), end=" ") 237 # Handle incoming queries 238 msg = s.recvfrom(65535) 239 rsp = create_response(msg[0]) 240 if rsp: 241 print(dns.rcode.to_text(rsp.rcode())) 242 s.sendto(rsp.to_wire(), msg[1]) 243 else: 244 print("NO RESPONSE") 245 if not running: 246 break 247