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