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