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