| 1 | #!/usr/bin/python |
|---|
| 2 | |
|---|
| 3 | # This script attempts to discover DNS configuration errors and inconsistencies between servers on your |
|---|
| 4 | # coporate network. Run the script with stdout redirected to a file to generate a nice HTML report. |
|---|
| 5 | # Example usage: |
|---|
| 6 | # $ ./dns-o-matic.py > report.html |
|---|
| 7 | # |
|---|
| 8 | # This script requires the python DNS and netaddr modules, which are |
|---|
| 9 | # probably already available in your favourite Linux distribution. |
|---|
| 10 | # From Ubuntu: |
|---|
| 11 | # $ apt-get install python-dns python-netaddr |
|---|
| 12 | # From Fedora: |
|---|
| 13 | # $ yum install python-pydns python-netaddr |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | import sys |
|---|
| 17 | import DNS |
|---|
| 18 | from netaddr import IPNetwork, IPAddress |
|---|
| 19 | from pprint import PrettyPrinter |
|---|
| 20 | |
|---|
| 21 | DNS.DiscoverNameServers() |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | def Request(domain,type,nameserver=None): |
|---|
| 25 | # try this upto three times because sometimes the connection to slovakia times out |
|---|
| 26 | for retry in range(3): |
|---|
| 27 | try: |
|---|
| 28 | if nameserver == None: |
|---|
| 29 | return DNS.Request(domain,qtype=type,timeout=10).req() |
|---|
| 30 | else: |
|---|
| 31 | return DNS.Request(domain,qtype=type,server=nameserver,timeout=10).req() |
|---|
| 32 | except DNS.Error: |
|---|
| 33 | if retry == 2: |
|---|
| 34 | return None |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | def RecursiveLookup(data,rec_lookup,type,nameserver,astart=None,ptrstart=None): |
|---|
| 38 | # exit when a request loop is detected |
|---|
| 39 | if type == 'PTR': |
|---|
| 40 | if ptrstart == None: |
|---|
| 41 | ptrstart = rec_lookup |
|---|
| 42 | elif ptrstart == rec_lookup: |
|---|
| 43 | return |
|---|
| 44 | else: |
|---|
| 45 | ptrstart = rec_lookup |
|---|
| 46 | else: |
|---|
| 47 | if astart == None: |
|---|
| 48 | astart = rec_lookup |
|---|
| 49 | elif astart == rec_lookup: |
|---|
| 50 | return |
|---|
| 51 | else: |
|---|
| 52 | astart = rec_lookup |
|---|
| 53 | |
|---|
| 54 | r = None |
|---|
| 55 | if type == 'PTR': |
|---|
| 56 | # reverse lookup request gives you all the hostnames that the given ip address resolves to |
|---|
| 57 | rec = IPAddress(rec_lookup) |
|---|
| 58 | r = Request(rec.reverse_dns,type,nameserver) |
|---|
| 59 | else: |
|---|
| 60 | # forward lookup request gives you all the ip addresses that the given hostname resolves to |
|---|
| 61 | r = Request(rec_lookup,type,nameserver) |
|---|
| 62 | |
|---|
| 63 | # we shouldn't ever time out |
|---|
| 64 | if r == None: |
|---|
| 65 | sys.stderr.write("ERROR: Timed out querying for %s records from %s\n" % (type, nameserver)) |
|---|
| 66 | sys.exit(1) |
|---|
| 67 | |
|---|
| 68 | # loop through all the answers |
|---|
| 69 | if r.header['status'] == 'NOERROR': |
|---|
| 70 | for rec_result,rec_type in [(x['data'],x['type']) for x in r.answers]: |
|---|
| 71 | |
|---|
| 72 | # despite only ever asking for PTR or A records, we sometimes get CNAMEs, so discard them |
|---|
| 73 | if rec_type not in set([DNS.Type.A, DNS.Type.PTR]): |
|---|
| 74 | continue |
|---|
| 75 | |
|---|
| 76 | # add to the list of answers |
|---|
| 77 | answer = { |
|---|
| 78 | 'ns' : nameserver, |
|---|
| 79 | 'lookup_of' : rec_lookup, |
|---|
| 80 | 'lookup_type' : type, |
|---|
| 81 | 'resolves_to' : rec_result, |
|---|
| 82 | 'status' : r.header['status'], |
|---|
| 83 | 'subrecords' : [], |
|---|
| 84 | 'problems' : [], |
|---|
| 85 | } |
|---|
| 86 | data.append(answer) |
|---|
| 87 | |
|---|
| 88 | # rinse and repeat |
|---|
| 89 | if type == 'PTR': |
|---|
| 90 | RecursiveLookup(answer['subrecords'],rec_result,'A', nameserver,astart,ptrstart) |
|---|
| 91 | else: |
|---|
| 92 | RecursiveLookup(answer['subrecords'],rec_result,'PTR',nameserver,astart,ptrstart) |
|---|
| 93 | else: |
|---|
| 94 | # add the error to the list of answers |
|---|
| 95 | answer = { |
|---|
| 96 | 'ns' : nameserver, |
|---|
| 97 | 'lookup_of' : rec_lookup, |
|---|
| 98 | 'lookup_type' : type, |
|---|
| 99 | 'resolves_to' : '', |
|---|
| 100 | 'status' : r.header['status'], |
|---|
| 101 | 'subrecords' : [], |
|---|
| 102 | 'problems' : [], |
|---|
| 103 | } |
|---|
| 104 | data.append(answer) |
|---|
| 105 | |
|---|
| 106 | |
|---|
| 107 | def GetNameServers(domain): |
|---|
| 108 | # start of authority request gives you the primary nameserver for a given domain |
|---|
| 109 | r = Request(domain,'SOA') |
|---|
| 110 | if r.header['status'] != 'NOERROR': |
|---|
| 111 | sys.stderr.write("ERROR: Received status of %s when attempting to query for SOA records\n" % (r.header['status'])) |
|---|
| 112 | sys.exit(1) |
|---|
| 113 | primary,email,serial,refresh,retry,expire,minimum = r.answers[0]['data'] |
|---|
| 114 | |
|---|
| 115 | # name server request on the primary name server gives you all the name servers for a given domain |
|---|
| 116 | r = Request(domain,'NS',primary) |
|---|
| 117 | if r.header['status'] != 'NOERROR': |
|---|
| 118 | sys.stderr.write("ERROR: Received status of %s when attempting to query for NS records\n" % (r.header['status'])) |
|---|
| 119 | sys.exit(1) |
|---|
| 120 | return [(x['data'],x['data']==primary) for x in r.answers] |
|---|
| 121 | |
|---|
| 122 | |
|---|
| 123 | def PrintNameServers(domain,nameservers): |
|---|
| 124 | print "<p>Available nameservers for %s:</p>" % domain |
|---|
| 125 | print "<ul>" |
|---|
| 126 | for ns,pri in nameservers: |
|---|
| 127 | if pri: |
|---|
| 128 | print "<li>%s - primary (holds the start of authority record)</li>" % ns |
|---|
| 129 | else: |
|---|
| 130 | print "<li>%s</li>" % ns |
|---|
| 131 | print "</ul>" |
|---|
| 132 | |
|---|
| 133 | |
|---|
| 134 | #def FindValidLoops(data): |
|---|
| 135 | # # do a depth-first search for request loops and mark these records as valid |
|---|
| 136 | # num = 0 |
|---|
| 137 | # for result in data['results']: |
|---|
| 138 | # new_num = FindValidLoops(result) |
|---|
| 139 | # if new_num > num: |
|---|
| 140 | # num = new_num |
|---|
| 141 | # if not data['status']: |
|---|
| 142 | # data['valid'] = True |
|---|
| 143 | # return 2 |
|---|
| 144 | # else: |
|---|
| 145 | # if num > 0: |
|---|
| 146 | # data['valid'] = True |
|---|
| 147 | # return num - 1 |
|---|
| 148 | # else: |
|---|
| 149 | # data['valid'] = False |
|---|
| 150 | # return 0 |
|---|
| 151 | |
|---|
| 152 | |
|---|
| 153 | def FindDanglingPointers(data): |
|---|
| 154 | for rec in data['subrecords']: |
|---|
| 155 | if rec['status'] == 'NXDOMAIN': |
|---|
| 156 | problem = { |
|---|
| 157 | 'icon':'error', |
|---|
| 158 | 'text':'Dangling pointer (PTR) record detected for %s! \n\n' % data['lookup_of'] + \ |
|---|
| 159 | 'Perhaps it was left behind when the address (A) record for %s was deleted.' % data['resolves_to'], |
|---|
| 160 | } |
|---|
| 161 | rec['problems'].append(problem) |
|---|
| 162 | |
|---|
| 163 | |
|---|
| 164 | def GetSubnetAnalysis(nameservers,subnet): |
|---|
| 165 | data = {'subnet':str(subnet), 'nameservers':[], 'addresses':[]} |
|---|
| 166 | |
|---|
| 167 | # make a note of which is the primary nameserver |
|---|
| 168 | primary = '' |
|---|
| 169 | for ns,pri in nameservers: |
|---|
| 170 | data['nameservers'].append({'name':ns, 'primary':pri}) |
|---|
| 171 | if pri: |
|---|
| 172 | primary = ns |
|---|
| 173 | |
|---|
| 174 | # iterate through all valid addresses in the subnet |
|---|
| 175 | sys.stderr.write("Processing Subnet: %s\n" % str(subnet)) |
|---|
| 176 | for ip in [str(x) for x in list(subnet) if x != subnet.broadcast and x != subnet.ip]: |
|---|
| 177 | sys.stderr.write(".") |
|---|
| 178 | |
|---|
| 179 | # request the crap out of the DNS servers |
|---|
| 180 | address = {'ip':ip, 'results':[]} |
|---|
| 181 | for ns,pri in nameservers: |
|---|
| 182 | RecursiveLookup(address['results'],ip,'PTR',ns) |
|---|
| 183 | |
|---|
| 184 | # forget about addresses that don't have records in *any* nameservers |
|---|
| 185 | for result in address['results']: |
|---|
| 186 | if result['status'] != 'NXDOMAIN': |
|---|
| 187 | data['addresses'].append(address) |
|---|
| 188 | break |
|---|
| 189 | sys.stderr.write("\n") |
|---|
| 190 | |
|---|
| 191 | # detect dangling pointers |
|---|
| 192 | for address in data['addresses']: |
|---|
| 193 | for result in address['results']: |
|---|
| 194 | FindDanglingPointers(result) |
|---|
| 195 | return data |
|---|
| 196 | |
|---|
| 197 | |
|---|
| 198 | def RecursivePrintResults(data,level=0): |
|---|
| 199 | # display the record we're looking up |
|---|
| 200 | if data['resolves_to'] != '': |
|---|
| 201 | print " <p style='padding-left:%spx'><span class='record'>%s</span> resolves to <span class='record'>%s</span>" % (`level * 20`, data['lookup_of'], data['resolves_to']) |
|---|
| 202 | else: |
|---|
| 203 | print " <p style='padding-left:%spx'><span class='record'>%s</span> does not resolve to anything!" % (`level * 20`, data['lookup_of']) |
|---|
| 204 | |
|---|
| 205 | # display any problems associated with the record |
|---|
| 206 | for prob in data['problems']: |
|---|
| 207 | print " <a rel='tooltip' title='%s'>" % prob['text'] |
|---|
| 208 | print " <img src='icons/%s.png' alt='%s' />" % (prob['icon'], prob['icon']) |
|---|
| 209 | print " </a>" |
|---|
| 210 | print " </p>" |
|---|
| 211 | |
|---|
| 212 | # recursively display subrecords |
|---|
| 213 | if len(data['subrecords']) > 0: |
|---|
| 214 | for rec in data['subrecords']: |
|---|
| 215 | RecursivePrintResults(rec,level + 1) |
|---|
| 216 | |
|---|
| 217 | |
|---|
| 218 | def PrintSubnetAnalysis(data): |
|---|
| 219 | subnet_id = "subnet-" + data['subnet'].replace('/','-').replace('.','-') |
|---|
| 220 | print "<h2 id='%s_heading'>Analysis of the %s subnet</h2>" % (subnet_id, data['subnet']) |
|---|
| 221 | print "<p><a class='debugs_on' id='%s_on' href='#%s_heading'>Show Debugs</a>" % (subnet_id, subnet_id) + \ |
|---|
| 222 | "<a class='debugs_off' id='%s_off' href='#%s_heading'>Hide Debugs</a></p>" % (subnet_id, subnet_id) |
|---|
| 223 | print "<table id='%s_table' border='1'>" % subnet_id |
|---|
| 224 | |
|---|
| 225 | heading = "<tr><th>address</th>" |
|---|
| 226 | for ns in data['nameservers']: |
|---|
| 227 | if ns['primary']: |
|---|
| 228 | heading += "<th>%s (SOA)</th>" % ns['name'] |
|---|
| 229 | else: |
|---|
| 230 | heading += "<th>%s</th>" % ns['name'] |
|---|
| 231 | heading += "</tr>" |
|---|
| 232 | |
|---|
| 233 | for i,address in enumerate(data['addresses']): |
|---|
| 234 | if i % 25 == 0: |
|---|
| 235 | print heading |
|---|
| 236 | print "<tr>\n <td><p>%s</p></td>" % address['ip'] |
|---|
| 237 | for ns in data['nameservers']: |
|---|
| 238 | print " <td>" |
|---|
| 239 | for result in [x for x in address['results'] if x['ns'] == ns['name']]: |
|---|
| 240 | RecursivePrintResults(result) |
|---|
| 241 | print " </td>" |
|---|
| 242 | print "</tr>" |
|---|
| 243 | print "</table>" |
|---|
| 244 | |
|---|
| 245 | # print the raw data structure for debugging |
|---|
| 246 | print "<pre id='%s_debugs' style='display:none;'>" % subnet_id |
|---|
| 247 | pp = PrettyPrinter(indent=2) |
|---|
| 248 | pp.pprint(data) |
|---|
| 249 | print "</pre>" |
|---|
| 250 | |
|---|
| 251 | |
|---|
| 252 | print """ |
|---|
| 253 | <html> |
|---|
| 254 | <head> |
|---|
| 255 | <title>DNS-o-Matic Report</title> |
|---|
| 256 | <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script> |
|---|
| 257 | <script type="text/javascript"> |
|---|
| 258 | jQuery(document).ready(function($) { |
|---|
| 259 | /* display the show debugs buttons by default */ |
|---|
| 260 | $("a.debugs_on").css('display', 'inline'); |
|---|
| 261 | |
|---|
| 262 | /* show debugs button click event callback */ |
|---|
| 263 | $("a.debugs_on").click(function() { |
|---|
| 264 | var subnet = "#" + $(this).attr('id').split('_')[0]; |
|---|
| 265 | $(subnet+"_debugs").css('display', 'block'); |
|---|
| 266 | $(subnet+"_table").css('display', 'none'); |
|---|
| 267 | $(subnet+"_on").css('display', 'none'); |
|---|
| 268 | $(subnet+"_off").css('display', 'inline'); |
|---|
| 269 | }); |
|---|
| 270 | |
|---|
| 271 | /* hide debugs button click event callback */ |
|---|
| 272 | $("a.debugs_off").click(function() { |
|---|
| 273 | var subnet = "#" + $(this).attr('id').split('_')[0]; |
|---|
| 274 | $(subnet+"_debugs").css('display', 'none'); |
|---|
| 275 | $(subnet+"_table").css('display', 'table'); |
|---|
| 276 | $(subnet+"_on").css('display', 'inline'); |
|---|
| 277 | $(subnet+"_off").css('display', 'none'); |
|---|
| 278 | }); |
|---|
| 279 | |
|---|
| 280 | /* mouseover tooltip event callbacks */ |
|---|
| 281 | $('a[rel=tooltip]').mouseover(function(e) { |
|---|
| 282 | /* steal the title text to use in the tooltip */ |
|---|
| 283 | var msg = $(this).attr('title').replace('\n','<br />'); |
|---|
| 284 | $(this).attr('title',''); |
|---|
| 285 | /* construct the tooltip */ |
|---|
| 286 | $(this).append('<div id="tooltip"><div class="tooltipBody">' + msg + '</div></div>'); |
|---|
| 287 | /* position and show the tooltip */ |
|---|
| 288 | $("#tooltip").css('top', e.pageY + 10 ); |
|---|
| 289 | $("#tooltip").css('left', e.pageX + 20 ); |
|---|
| 290 | }).mousemove(function(e) { |
|---|
| 291 | /* make the tooltip follow the cursor */ |
|---|
| 292 | $("#tooltip").css('top', e.pageY + 10 ); |
|---|
| 293 | $("#tooltip").css('left', e.pageX + 20 ); |
|---|
| 294 | }).mouseout(function() { |
|---|
| 295 | /* put the title text back and destroy the tooltip */ |
|---|
| 296 | $(this).attr('title',$(".tooltipBody").html()); |
|---|
| 297 | $(this).children("div#tooltip").remove(); |
|---|
| 298 | }); |
|---|
| 299 | }); |
|---|
| 300 | </script> |
|---|
| 301 | <style> |
|---|
| 302 | th { |
|---|
| 303 | font-family:sans-serif; |
|---|
| 304 | padding:5px 0px; |
|---|
| 305 | } |
|---|
| 306 | td { |
|---|
| 307 | font-family:sans-serif; |
|---|
| 308 | font-weight:normal; |
|---|
| 309 | font-size:0.8em; |
|---|
| 310 | color:#666; |
|---|
| 311 | padding:5px; |
|---|
| 312 | } |
|---|
| 313 | td p { |
|---|
| 314 | margin:0; |
|---|
| 315 | padding:0; |
|---|
| 316 | } |
|---|
| 317 | img { |
|---|
| 318 | height:0.8em; |
|---|
| 319 | border:0; |
|---|
| 320 | } |
|---|
| 321 | #tooltip { |
|---|
| 322 | position:absolute; |
|---|
| 323 | z-index:9999; |
|---|
| 324 | color:#fff; |
|---|
| 325 | font-size:10px; |
|---|
| 326 | width:275px; |
|---|
| 327 | } |
|---|
| 328 | #tooltip .tooltipBody { |
|---|
| 329 | background-color:#000; |
|---|
| 330 | padding:5px |
|---|
| 331 | } |
|---|
| 332 | .record { |
|---|
| 333 | font-weight:bold; |
|---|
| 334 | color:black; |
|---|
| 335 | } |
|---|
| 336 | .debugs_on, .debugs_off { |
|---|
| 337 | display:none; |
|---|
| 338 | } |
|---|
| 339 | </style> |
|---|
| 340 | </head> |
|---|
| 341 | <body> |
|---|
| 342 | <h1>DNS-o-Matic Report</h1> |
|---|
| 343 | """ |
|---|
| 344 | |
|---|
| 345 | domain = 'cse-servelec.com' |
|---|
| 346 | subnets = [ |
|---|
| 347 | IPNetwork('194.62.153.0/24'), |
|---|
| 348 | IPNetwork('194.62.154.0/24'), |
|---|
| 349 | ] |
|---|
| 350 | |
|---|
| 351 | nslist = GetNameServers(domain) |
|---|
| 352 | PrintNameServers(domain,nslist) |
|---|
| 353 | for subnet in subnets: |
|---|
| 354 | analysis = GetSubnetAnalysis(nslist, subnet) |
|---|
| 355 | PrintSubnetAnalysis(analysis) |
|---|
| 356 | |
|---|
| 357 | print """ |
|---|
| 358 | </body> |
|---|
| 359 | </html> |
|---|
| 360 | """ |
|---|