root/trunk/misc/dns-o-matic.py

Revision 161, 10.8 KB (checked in by mbooth, 5 months ago)

Show error text in a fancy tooltip if javascript is enabled in the browser. (The built in Firefox tooltips don't stay visible for long enough to read them.)

  • Property svn:executable set to *
Line 
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
16import sys
17import DNS
18from netaddr import IPNetwork, IPAddress
19from pprint import PrettyPrinter
20
21DNS.DiscoverNameServers()
22
23
24def 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
37def 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
107def 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
123def 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
153def 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
164def 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
198def 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
218def 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
252print """
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
345domain = 'cse-servelec.com'
346subnets = [
347           IPNetwork('194.62.153.0/24'),
348           IPNetwork('194.62.154.0/24'),
349           ]
350
351nslist = GetNameServers(domain)
352PrintNameServers(domain,nslist)
353for subnet in subnets:
354    analysis = GetSubnetAnalysis(nslist, subnet)
355    PrintSubnetAnalysis(analysis)
356
357print """
358</body>
359</html>
360"""
Note: See TracBrowser for help on using the browser.