pax_global_header00006660000000000000000000000064131020530040014476gustar00rootroot0000000000000052 comment=47dbc6afe392883ec4fb3a0631a405b1a4a3c3a9 dnsdiag-1.6.3/000077500000000000000000000000001310205300400131165ustar00rootroot00000000000000dnsdiag-1.6.3/.gitignore000066400000000000000000000014171310205300400151110ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class .idea/ whois.cache # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints dnsdiag-1.6.3/.gitmodules000066400000000000000000000000001310205300400152610ustar00rootroot00000000000000dnsdiag-1.6.3/.travis.yml000066400000000000000000000003311310205300400152240ustar00rootroot00000000000000language: python python: - "3.3" - "3.4" - "3.5" - "3.5-dev" # 3.5 development branch - "nightly" # currently points to 3.6-dev install: "pip install -r requirements.txt" script: nosetests dnstraceroute.py dnsdiag-1.6.3/LICENSE000066400000000000000000000024211310205300400141220ustar00rootroot00000000000000Copyright (c) 2016, Babak Farrokhi All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. dnsdiag-1.6.3/MANIFEST.in000066400000000000000000000000651310205300400146550ustar00rootroot00000000000000include LICENSE README.md TODO.md public-servers.txt dnsdiag-1.6.3/README.md000066400000000000000000000137031310205300400144010ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/farrokhi/dnsdiag.svg)](https://travis-ci.org/farrokhi/dnsdiag) [![PyPI](https://img.shields.io/pypi/v/dnsdiag.svg?maxAge=8600)](https://pypi.python.org/pypi/dnsdiag/) [![PyPI](https://img.shields.io/pypi/l/dnsdiag.svg?maxAge=8600)]() [![PyPI](https://img.shields.io/pypi/pyversions/dnsdiag.svg?maxAge=8600)]() [![GitHub stars](https://img.shields.io/github/stars/farrokhi/dnsdiag.svg?style=social&label=Star&maxAge=8600)](https://github.com/farrokhi/dnsdiag/stargazers) DNS Diagnostics and Performance Measurement Tools ================================================== Ever been wondering if your ISP is [hijacking your DNS traffic](https://decentralize.today/is-your-isp-hijacking-your-dns-traffic-f3eb7ccb0ee7#.fevks5wyc)? Ever observed any misbehavior with your DNS responses? Ever been redirected to wrong address and suspected something is wrong with your DNS? Here we have a [set of tools](http://github.com/farrokhi/dnsdiag) to perform basic audits on your DNS requests and responses to make sure your DNS is working as you expect. You can measure the response time of any given DNS server for arbitrary requests using `dnsping`. Just like traditional ping utility, it gives you similar functionality for DNS requests. You can also trace the path your DNS request takes to destination to make sure it is not being redirected or hijacked. This can be done by comparing different DNS queries being sent to the same DNS server using `dnstraceroute` and observe if there is any difference between the path. `dnseval` evaluates multiple DNS resolvers and helps you choose the best DNS server for your network. While it is highly recommended to use your own DNS resolver and never trust any third-party DNS server, but in case you need to choose the best DNS forwarder for your network, `dnseval` lets you compare different DNS servers from performance (latency) and reliability (loss) point of view. # prerequisites This script requires python3 as well as latest [dnspython](http://www.dnspython.org/) and [cymruwhois](https://pythonhosted.org/cymruwhois/). # installation There are several ways that you can use this toolset. However using the sourcecode is always recommended. ## From Source Code 1. You can checkout this git repo and its submodules ``` git clone https://github.com/farrokhi/dnsdiag.git cd dnsdiag pip3 install -r requirements.txt ``` 2. You can alternatively install the package using pip: ``` pip3 install dnsdiag ``` ## From Binary From time to time, binary version will be released for Windows, Mac OS X and Linux platforms. You can grab the latest release from [releases page](https://github.com/farrokhi/dnsdiag/releases). # dnsping dnsping pings a DNS resolver by sending an arbitrary DNS query for given number of times: ``` % ./dnsping.py -c 3 -t AAAA -s 8.8.8.8 dnsdiag.org dnsping.py DNS: 8.8.8.8:53, hostname: dnsdiag.org, rdatatype: AAAA 4 bytes from 8.8.8.8: seq=0 time=123.509 ms 4 bytes from 8.8.8.8: seq=1 time=115.726 ms 4 bytes from 8.8.8.8: seq=2 time=117.351 ms --- 8.8.8.8 dnsping statistics --- 3 requests transmitted, 3 responses received, 0% lost min=115.726 ms, avg=118.862 ms, max=123.509 ms, stddev=4.105 ms ``` This script calculates minimum, maximum and average response time as well as jitter (stddev) # dnstraceroute dnstraceroute is a traceroute utility to figure out the path that your DNS request is passing through to get to its destination. You may want to compare it to your actual network traceroute and make sure your DNS traffic is not routed to any unwanted path. ``` % ./dnstraceroute.py --expert -C -t A -s 8.8.4.4 facebook.com dnstraceroute.py DNS: 8.8.4.4:53, hostname: facebook.com, rdatatype: A 1 192.168.0.1 (192.168.0.1) 1 ms 2 192.168.28.177 (192.168.28.177) 4 ms 3 192.168.0.1 (192.168.0.1) 693 ms 4 172.19.4.17 (172.19.4.17) 3 ms 5 google-public-dns-b.google.com (8.8.4.4) 8 ms === Expert Hints === [*] public DNS server is next to a private IP address (possible hijacking) ``` Using `--expert` will instruct dnstraceroute to print expert hints (such as warnings of possible DNS traffic hijacking). # dnseval dnseval is a bulk ping utility that sends an arbitrary DNS query to a give list of DNS servers. This script is meant for comparing response time of multiple DNS servers at once: ``` % ./dnseval.py -t AAAA -f public-v4.txt -c10 yahoo.com server avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags ------------------------------------------------------------------------------------------------------ 8.8.8.8 270.791 215.599 307.498 40.630 %0 298 QR -- -- RD RA -- -- 8.8.4.4 222.955 171.753 307.251 60.481 %10 291 QR -- -- RD RA -- -- ns.ripe.net 174.855 160.949 187.458 10.099 %0 289 QR -- -- RD RA -- -- 4.2.2.1 172.798 163.892 189.918 7.823 %0 287 QR -- -- RD RA -- -- 4.2.2.2 178.594 169.158 184.696 5.067 %0 285 QR -- -- RD RA -- -- 4.2.2.3 153.574 138.509 173.439 12.015 %0 284 QR -- -- RD RA -- -- 4.2.2.4 153.182 141.023 162.323 6.700 %0 282 QR -- -- RD RA -- -- 4.2.2.5 154.840 141.557 163.889 7.195 %0 281 QR -- -- RD RA -- -- 209.244.0.3 156.270 147.320 161.365 3.958 %0 279 QR -- -- RD RA -- -- 209.244.0.4 159.329 151.283 163.726 3.958 %0 278 QR -- -- RD RA -- -- 195.46.39.39 171.098 163.612 181.147 5.067 %0 276 QR -- -- RD RA -- -- 195.46.39.40 175.335 160.920 185.618 8.726 %0 274 QR -- -- RD RA -- -- ``` ### Author Babak Farrokhi - twitter: [@farrokhi](https://twitter.com/farrokhi) - github: [github.com/farrokhi](https://github.com/farrokhi/) - website: [farrokhi.net](https://farrokhi.net/) ### License dnsdiag is released under a 2 clause BSD license. dnsdiag-1.6.3/TODO.md000066400000000000000000000000741310205300400142060ustar00rootroot00000000000000# TODO - dnsfingerprint.py tool to fingerprint DNS servers dnsdiag-1.6.3/dnseval.py000077500000000000000000000240621310205300400151330ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os import getopt import ipaddress import signal import socket import sys import time from statistics import stdev import dns.rdatatype import dns.resolver __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __version__ = "1.6.3" __progname__ = os.path.basename(sys.argv[0]) shutdown = False resolvers = dns.resolver.get_default_resolver().nameservers class Colors(object): N = '\033[m' # native R = '\033[31m' # red G = '\033[32m' # green O = '\033[33m' # orange B = '\033[34m' # blue def __init__(self, mode): if not mode: self.N = '' self.R = '' self.G = '' self.O = '' self.B = '' def usage(): print("""%s version %s usage: %s [-h] [-f server-list] [-c count] [-t type] [-w wait] hostname -h --help show this help -f --file dns server list to use (default: system resolvers) -c --count number of requests to send (default: 10) -w --wait maximum wait time for a reply (default: 2) -t --type DNS request record type (default: A) -T --tcp Use TCP instead of UDP -e --edns Disable EDNS0 (Default: Enabled) -C --color Print colorful output -v --verbose Print actual dns response """ % (__progname__, __version__, __progname__)) sys.exit() def signal_handler(sig, frame): global shutdown if shutdown: # pressed twice, so exit immediately sys.exit(0) shutdown = True # pressed once, exit gracefully def maxlen(names): sn = sorted(names, key=len) return len(sn[-1]) def _order_flags(table): return sorted(table.items(), reverse=True) def flags_to_text(flags): # Standard DNS flags QR = 0x8000 AA = 0x0400 TC = 0x0200 RD = 0x0100 RA = 0x0080 AD = 0x0020 CD = 0x0010 # EDNS flags DO = 0x8000 _by_text = { 'QR': QR, 'AA': AA, 'TC': TC, 'RD': RD, 'RA': RA, 'AD': AD, 'CD': CD } _by_value = dict([(y, x) for x, y in _by_text.items()]) _flags_order = _order_flags(_by_value) _by_value = dict([(y, x) for x, y in _by_text.items()]) order = sorted(_by_value.items(), reverse=True) text_flags = [] for k, v in order: if flags & k != 0: text_flags.append(v) else: text_flags.append('--') return ' '.join(text_flags) def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=False): resolver = dns.resolver.Resolver() resolver.nameservers = [server] resolver.timeout = timeout resolver.lifetime = timeout resolver.retry_servfail = 0 flags = 0 ttl = None answers = None if use_edns: resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO')) response_times = [] i = 0 for i in range(count): if shutdown: # user pressed CTRL+C break try: stime = time.perf_counter() answers = resolver.query(host, dnsrecord, tcp=use_tcp, raise_on_no_answer=False) # todo: response validation in future etime = time.perf_counter() except (dns.resolver.NoNameservers, dns.resolver.NoAnswer): break except dns.resolver.Timeout: pass else: elapsed = (etime - stime) * 1000 # convert to milliseconds response_times.append(elapsed) r_sent = i + 1 r_received = len(response_times) r_lost = r_sent - r_received r_lost_percent = (100 * r_lost) / r_sent if response_times: r_min = min(response_times) r_max = max(response_times) r_avg = sum(response_times) / r_received if len(response_times) > 1: r_stddev = stdev(response_times) else: r_stddev = 0 else: r_min = 0 r_max = 0 r_avg = 0 r_stddev = 0 if answers is not None: flags = answers.response.flags if len(answers.response.answer) > 0: ttl = answers.response.answer[0].ttl return server, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags, ttl, answers def main(): try: signal.signal(signal.SIGTSTP, signal.SIG_IGN) # ignore CTRL+Z signal.signal(signal.SIGINT, signal_handler) # catch CTRL+C except AttributeError: # Some systems (e.g. Windows) may not support all signals pass if len(sys.argv) == 1: usage() # defaults dnsrecord = 'A' count = 10 waittime = 2 inputfilename = None fromfile = False use_tcp = False use_edns = True verbose = False color_mode = False hostname = 'wikipedia.org' try: opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:TevC", ["help", "file=", "count=", "type=", "wait=", "tcp", "edns", "verbose", "color"]) except getopt.GetoptError as err: print(err) usage() if args and len(args) == 1: hostname = args[0] else: usage() for o, a in opts: if o in ("-h", "--help"): usage() elif o in ("-c", "--count"): count = int(a) elif o in ("-f", "--file"): inputfilename = a fromfile = True elif o in ("-w", "--wait"): waittime = int(a) elif o in ("-t", "--type"): dnsrecord = a elif o in ("-T", "--tcp"): use_tcp = True elif o in ("-e", "--edns"): use_edns = False elif o in ("-C", "--color"): color_mode = True elif o in ("-v", "--verbose"): verbose = True else: print("Invalid option: %s" % o) usage() color = Colors(color_mode) try: if fromfile: with open(inputfilename, 'rt') as flist: f = flist.read().splitlines() else: f = resolvers if len(f) == 0: print("No nameserver specified") f = [name.strip() for name in f] width = maxlen(f) blanks = (width - 5) * ' ' print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags') print((93 + width) * '-') for server in f: # check if we have a valid dns server address if server.lstrip() == '': # deal with empty lines continue server = server.replace(' ', '') try: ipaddress.ip_address(server) except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name try: s = socket.getaddrinfo(server, port=None)[1][4][0] except OSError: print('Error: cannot resolve hostname:', server) s = None except: pass else: s = server if not s: continue try: (s, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags, ttl, answers) = dnsping(hostname, s, dnsrecord, waittime, count, use_tcp=use_tcp, use_edns=use_edns) except dns.resolver.NXDOMAIN: print('%-15s NXDOMAIN' % server) continue except Exception as e: print('%s: %s' % (server, e)) continue s = server.ljust(width + 1) text_flags = flags_to_text(flags) s_ttl = str(ttl) if s_ttl == "None": s_ttl = "N/A" if r_lost_percent > 0: l_color = color.O else: l_color = color.N print("%s %-8.3f %-8.3f %-8.3f %-8.3f %s%%%-3d%s %-8s %21s" % ( s, r_avg, r_min, r_max, r_stddev, l_color, r_lost_percent, color.N, s_ttl, text_flags), flush=True) if verbose and hasattr(answers, 'response'): ans_index = 1 for answer in answers.response.answer: print("Answer %d [ %s%s%s ]" % (ans_index, color.B, answer, color.N)) ans_index += 1 print("") except Exception as e: print('%s: %s' % (server, e)) sys.exit(1) if __name__ == '__main__': main() dnsdiag-1.6.3/dnsping.py000077500000000000000000000205061310205300400151400ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import getopt import ipaddress import os import signal import socket import sys import time from statistics import stdev import dns.flags import dns.rdatatype import dns.resolver __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __version__ = "1.6.3" __progname__ = os.path.basename(sys.argv[0]) shutdown = False def usage(): print("""%s version %s usage: %s [-ehqv] [-s server] [-p port] [-P port] [-S address] [-c count] [-t type] [-w wait] hostname -h --help Show this help -q --quiet Quiet -v --verbose Print actual dns response -s --server DNS server to use (default: first entry from /etc/resolv.conf) -p --port DNS server port number (default: 53) -T --tcp Use TCP instead of UDP -4 --ipv4 Use IPv4 as default network protocol -6 --ipv6 Use IPv6 as default network protocol -P --srcport Query source port number (default: 0) -S --srcip Query source IP address (default: default interface address) -c --count Number of requests to send (default: 10) -w --wait Maximum wait time for a reply (default: 2 seconds) -i --interval Time between each request (default: 0 seconds) -t --type DNS request record type (default: A) -e --edns Disable EDNS0 (default: Enabled) """ % (__progname__, __version__, __progname__)) sys.exit(0) def signal_handler(sig, frame): global shutdown if shutdown: # pressed twice, so exit immediately sys.exit(0) shutdown = True # pressed once, exit gracefully def main(): try: signal.signal(signal.SIGTSTP, signal.SIG_IGN) # ignore CTRL+Z signal.signal(signal.SIGINT, signal_handler) # custom CTRL+C handler except AttributeError: # OS Does not support some signals, probably windows pass if len(sys.argv) == 1: usage() # defaults dnsrecord = 'A' count = 10 timeout = 2 interval = 0 quiet = False verbose = False dnsserver = dns.resolver.get_default_resolver().nameservers[0] dst_port = 53 src_port = 0 src_ip = None use_tcp = False use_edns = True af = socket.AF_INET hostname = 'wikipedia.org' try: opts, args = getopt.getopt(sys.argv[1:], "qhc:s:t:w:i:vp:P:S:T46e", ["help", "count=", "server=", "quiet", "type=", "wait=", "interval=", "verbose", "port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport=", "edns"]) except getopt.GetoptError as err: # print help information and exit: print(err) # will print something like "option -a not recognized" usage() if args and len(args) == 1: hostname = args[0] else: usage() for o, a in opts: if o in ("-h", "--help"): usage() elif o in ("-c", "--count"): count = int(a) elif o in ("-v", "--verbose"): verbose = True elif o in ("-s", "--server"): dnsserver = a elif o in ("-p", "--port"): dst_port = int(a) elif o in ("-q", "--quiet"): quiet = True verbose = False elif o in ("-w", "--wait"): timeout = int(a) elif o in ("-i", "--interval"): interval = int(a) elif o in ("-t", "--type"): dnsrecord = a elif o in ("-T", "--tcp"): use_tcp = True elif o in ("-4", "--ipv4"): af = socket.AF_INET elif o in ("-6", "--ipv6"): af = socket.AF_INET6 elif o in ("-e", "--edns"): use_edns = False elif o in ("-P", "--srcport"): src_port = int(a) if src_port < 1024: print("WARNING: Source ports below 1024 are only available to superuser") elif o in ("-S", "--srcip"): src_ip = a else: usage() # check if we have a valid dns server address try: ipaddress.ip_address(dnsserver) except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name try: dnsserver = socket.getaddrinfo(dnsserver, port=None, family=af)[1][4][0] except OSError: print('Error: cannot resolve hostname:', dnsserver) sys.exit(1) resolver = dns.resolver.Resolver() resolver.nameservers = [dnsserver] resolver.timeout = timeout resolver.lifetime = timeout resolver.port = dst_port resolver.retry_servfail = 0 if use_edns: resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO')) response_time = [] i = 0 print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dst_port, hostname, dnsrecord)) for i in range(count): if shutdown: break try: stime = time.perf_counter() answers = resolver.query(hostname, dnsrecord, source_port=src_port, source=src_ip, tcp=use_tcp, raise_on_no_answer=False) etime = time.perf_counter() except dns.resolver.NoNameservers as e: if not quiet: print("No response to dns request") if verbose: print("error:", e) sys.exit(1) except dns.resolver.NXDOMAIN as e: if not quiet: print("Hostname does not exist") if verbose: print("Error:", e) sys.exit(1) except dns.resolver.Timeout: if not quiet: print("Request timeout") pass except dns.resolver.NoAnswer: if not quiet: print("No answer") pass else: elapsed = (etime - stime) * 1000 # convert to milliseconds response_time.append(elapsed) if not quiet: print( "%d bytes from %s: seq=%-3d time=%.3f ms" % ( len(str(answers.rrset)), dnsserver, i, elapsed)) if verbose: print(answers.rrset) print("flags:", dns.flags.to_text(answers.response.flags)) time_to_next = (stime + interval) - etime if time_to_next > 0: time.sleep(time_to_next) r_sent = i + 1 r_received = len(response_time) r_lost = r_sent - r_received r_lost_percent = (100 * r_lost) / r_sent if response_time: r_min = min(response_time) r_max = max(response_time) r_avg = sum(response_time) / r_received if len(response_time) > 1: r_stddev = stdev(response_time) else: r_stddev = 0 else: r_min = 0 r_max = 0 r_avg = 0 r_stddev = 0 print('\n--- %s dnsping statistics ---' % dnsserver) print('%d requests transmitted, %d responses received, %.0f%% lost' % (r_sent, r_received, r_lost_percent)) print('min=%.3f ms, avg=%.3f ms, max=%.3f ms, stddev=%.3f ms' % (r_min, r_avg, r_max, r_stddev)) if __name__ == '__main__': main() dnsdiag-1.6.3/dnstraceroute.py000077500000000000000000000315701310205300400163630ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import concurrent.futures import getopt import ipaddress import os import pickle import signal import socket import sys import time import dns.query import dns.rdatatype import dns.resolver import cymruwhois # Global Variables __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __version__ = "1.6.3" _ttl = None quiet = False whois_cache = {} shutdown = False # Constants __progname__ = os.path.basename(sys.argv[0]) WHOIS_CACHE = 'whois.cache' class CustomSocket(socket.socket): def __init__(self, *args, **kwargs): super(CustomSocket, self).__init__(*args, **kwargs) def sendto(self, *args, **kwargs): global _ttl if _ttl: self.setsockopt(socket.SOL_IP, socket.IP_TTL, _ttl) super(CustomSocket, self).sendto(*args, **kwargs) def test_import(): # passing this test means imports were successful pass class Colors(object): N = '\033[m' # native R = '\033[31m' # red G = '\033[32m' # green O = '\033[33m' # orange B = '\033[34m' # blue def __init__(self, mode): if not mode: self.N = '' self.R = '' self.G = '' self.O = '' self.B = '' def whois_lookup(ip): try: global whois_cache currenttime = time.time() ts = currenttime if ip in whois_cache: asn, ts = whois_cache[ip] else: ts = 0 if (currenttime - ts) > 36000: c = cymruwhois.Client() asn = c.lookup(ip) whois_cache[ip] = (asn, currenttime) return asn except Exception as e: return e def load_whois_cache(cachefile): try: pkl_file = open(cachefile, 'rb') try: whois = pickle.load(pkl_file) pkl_file.close() except Exception: whois = {} except IOError: whois = {} return whois def save_whois_cache(cachefile, whois_data): pkl_file = open(cachefile, 'wb') pickle.dump(whois_data, pkl_file) pkl_file.close() def usage(): print('%s version %s\n' % (__progname__, __version__)) print('usage: %s [-aeqhCx] [-s server] [-p port] [-c count] [-t type] [-w wait] hostname' % __progname__) print(' -h --help Show this help') print(' -q --quiet Quiet') print(' -x --expert Print expert hints if available') print(' -a --asn Turn on AS# lookups for each hop encountered') print(' -s --server DNS server to use (default: first system resolver)') print(' -p --port DNS server port number (default: 53)') print(' -c --count Maximum number of hops (default: 30)') print(' -w --wait Maximum wait time for a reply (default: 2)') print(' -t --type DNS request record type (default: A)') print(' -C --color Print colorful output') print(' -e --edns Disable EDNS0 (Default: Enabled)') print(' ') sys.exit() def signal_handler(sig, frame): global shutdown if shutdown: # pressed twice, so exit immediately sys.exit(0) shutdown = True # pressed once, exit gracefully def expert_report(trace_path, color_mode): color = Colors(color_mode) print("\n%s=== Expert Hints ===%s" % (color.B, color.N)) if len(trace_path) == 0: print(" [*] empty trace - should not happen") return private_network_radius = 4 # number of hops we assume we are still inside our local network prev_hop = None if len(trace_path) > 1: prev_hop = trace_path[-2] if len(trace_path) < 2: print( " %s[*]%s path too short (possible DNS hijacking, unless it is a local DNS resolver)" % (color.R, color.N)) return if prev_hop == '*' and len(trace_path) > private_network_radius: print(" %s[*]%s public DNS server is next to an invisible hop (probably a firewall)" % (color.R, color.N)) return if prev_hop and len(trace_path) > private_network_radius and ipaddress.ip_address(prev_hop).is_private: print(" %s[*]%s public DNS server is next to a private IP address (possible hijacking)" % (color.R, color.N)) return if prev_hop and len(trace_path) > private_network_radius and ipaddress.ip_address(prev_hop).is_reserved: print(" %s[*]%s public DNS server is next to a reserved IP address (possible hijacking)" % (color.R, color.N)) return # no expert info available print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N)) def ping(resolver, hostname, dnsrecord, ttl, use_edns=False): global _ttl reached = False dns.query.socket_factory = CustomSocket _ttl = ttl if use_edns: resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO')) try: resolver.query(hostname, dnsrecord, raise_on_no_answer=False) except dns.resolver.NoNameservers as e: if not quiet: print("no or bad response:", e) sys.exit(1) except dns.resolver.NXDOMAIN as e: if not quiet: print("Invalid hostname:", e) sys.exit(1) except dns.resolver.Timeout: pass except dns.resolver.NoAnswer: if not quiet: print("invalid answer") pass except SystemExit: pass except Exception as e: print("unxpected error: ", e) sys.exit(1) else: reached = True return reached def main(): global quiet try: signal.signal(signal.SIGTSTP, signal.SIG_IGN) # ignore CTRL+Z signal.signal(signal.SIGINT, signal_handler) # custom CTRL+C handler except AttributeError: # not all signals are supported on all platforms pass if len(sys.argv) == 1: usage() dnsrecord = 'A' count = 30 timeout = 2 dnsserver = dns.resolver.get_default_resolver().nameservers[0] dest_port = 53 hops = 0 as_lookup = False expert_mode = False should_resolve = True use_edns = True color_mode = False try: opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:t:w:p:nexC", ["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert", "color"]) except getopt.GetoptError as err: # print help information and exit: print(err) # will print something like "option -a not recognized" usage() if args and len(args) == 1: hostname = args[0] else: usage() for o, a in opts: if o in ("-h", "--help"): usage() elif o in ("-c", "--count"): count = int(a) elif o in ("-x", "--expert"): expert_mode = True elif o in ("-s", "--server"): dnsserver = a elif o in ("-q", "--quiet"): quiet = True elif o in ("-w", "--wait"): timeout = int(a) elif o in ("-t", "--type"): dnsrecord = a elif o in ("-p", "--port"): dest_port = int(a) elif o in ("-C", "--color"): color_mode = True elif o in ("-n"): should_resolve = False elif o in ("-a", "--asn"): as_lookup = True elif o in ("-e", "--edns"): use_edns = False else: usage() color = Colors(color_mode) # check if we have a valid dns server address try: ipaddress.ip_address(dnsserver) except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name try: dnsserver = socket.getaddrinfo(dnsserver, port=None, family=socket.AF_INET)[1][4][0] except OSError: print('Error: cannot resolve hostname:', dnsserver) sys.exit(1) resolver = dns.resolver.Resolver() resolver.nameservers = [dnsserver] resolver.timeout = timeout resolver.lifetime = timeout resolver.retry_servfail = 0 icmp = socket.getprotobyname('icmp') ttl = 1 reached = False trace_path = [] if not quiet: print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, hostname, dnsrecord), flush=True) while True: if shutdown: break # some platforms permit opening a DGRAM socket for ICMP without root permission # if not availble, we will fall back to RAW which explicitly requires root permission try: icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) except OSError: try: icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, icmp) except OSError: print("Error: Unable to create ICMP socket with unprivileged user. Please run as root.") sys.exit(1) icmp_socket.bind(("", dest_port)) icmp_socket.settimeout(timeout) curr_addr = None curr_host = None with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: # dispatch dns lookup to another thread stime = time.perf_counter() thr = pool.submit(ping, resolver, hostname, dnsrecord, ttl, use_edns=use_edns) try: # expect ICMP response _, curr_addr = icmp_socket.recvfrom(512) curr_addr = curr_addr[0] except socket.error: etime = time.perf_counter() pass finally: etime = time.perf_counter() icmp_socket.close() reached = thr.result() if reached: curr_addr = dnsserver stime = time.perf_counter() # need to recalculate elapsed time for last hop without waiting for an icmp error reply ping(resolver, hostname, dnsrecord, ttl, use_edns=use_edns) etime = time.perf_counter() elapsed = abs(etime - stime) * 1000 # convert to milliseconds if should_resolve: try: if curr_addr: curr_name = socket.gethostbyaddr(curr_addr)[0] except socket.error: curr_name = curr_addr except SystemExit: pass except: print("unxpected error: ", sys.exc_info()[0]) else: curr_name = curr_addr if curr_addr: as_name = "" if as_lookup: asn = whois_lookup(curr_addr) as_name = '' try: if asn and asn.asn != "NA": as_name = "[AS%s %s] " % (asn.asn, asn.owner) except AttributeError: if shutdown: sys.exit(0) pass c = color.N # default if curr_addr != '*': IP = ipaddress.ip_address(curr_addr) if IP.is_private: c = color.R if IP.is_reserved: c = color.B if curr_addr == dnsserver: c = color.G print("%d\t%s (%s%s%s) %s%.3f ms" % (ttl, curr_name, c, curr_addr, color.N, as_name, elapsed), flush=True) trace_path.append(curr_addr) else: print("%d\t *" % ttl, flush=True) trace_path.append("*") ttl += 1 hops += 1 if (hops >= count) or (curr_addr == dnsserver) or reached: break if expert_mode and not shutdown: expert_report(trace_path, color_mode) if __name__ == '__main__': try: whois_cache = load_whois_cache(WHOIS_CACHE) main() finally: save_whois_cache(WHOIS_CACHE, whois_cache) dnsdiag-1.6.3/public-servers.txt000066400000000000000000000002601310205300400166220ustar00rootroot000000000000008.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844 4.2.2.1 4.2.2.2 4.2.2.3 4.2.2.4 4.2.2.5 209.244.0.3 209.244.0.4 195.46.39.39 195.46.39.40 216.146.35.35 216.146.36.36 dnsdiag-1.6.3/requirements.txt000066400000000000000000000000421310205300400163760ustar00rootroot00000000000000dnspython>=1.15.0 cymruwhois>=1.6 dnsdiag-1.6.3/rootservers.txt000066400000000000000000000003671310205300400162620ustar00rootroot00000000000000a.root-servers.net b.root-servers.net c.root-servers.net d.root-servers.net e.root-servers.net f.root-servers.net g.root-servers.net h.root-servers.net i.root-servers.net j.root-servers.net k.root-servers.net l.root-servers.net m.root-servers.net dnsdiag-1.6.3/setup.py000066400000000000000000000025511310205300400146330ustar00rootroot00000000000000from setuptools import setup, find_packages setup( name="dnsdiag", version="1.6.3", packages=find_packages(), scripts=["dnseval.py", "dnsping.py", "dnstraceroute.py"], install_requires=['dnspython>=1.15.0', 'cymruwhois>=1.6'], classifiers=[ "Topic :: System :: Networking", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3.4", "Topic :: Internet :: Name Service (DNS)", "Development Status :: 5 - Production/Stable", "Operating System :: OS Independent", ], author="Babak Farrokhi", author_email="babak@farrokhi.net", description="DNS Diagnostics and measurement tools (ping, traceroute)", long_description=""" DNSDiag provides a handful of tools to measure and diagnose your DNS performance and integrity. Using dnsping, dnstraceroute and dnseval tools you can measure your DNS response quality from delay and loss perspective as well as tracing the path your DNS query takes to get to DNS server. """, license="BSD", keywords="dns traceroute ping", url="https://dnsdiag.org/", entry_points={ 'console_scripts': [ 'dnsping = dnsping:main', 'dnstraceroute = dnstraceroute:main', 'dnseval = dnseval:main', ] } )