Compare commits

..

No commits in common. "master" and "v1.4.0" have entirely different histories.

17 changed files with 204 additions and 444 deletions

3
.gitignore vendored
View File

@ -1,6 +1,3 @@
# virtualenv
.venv/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

6
.gitmodules vendored
View File

@ -0,0 +1,6 @@
[submodule "cymruwhois"]
path = cymruwhois
url = https://github.com/JustinAzoff/python-cymruwhois.git
[submodule "dnspython-orig"]
path = dnspython
url = https://github.com/rthalley/dnspython/

View File

@ -1,18 +1,10 @@
language: python
sudo: false
install: "pip install -r requirements.txt"
python:
- "3.3"
- "3.4"
- "3.5"
- "3.5-dev" # 3.5 development branch
- "nightly" # currently points to 3.6-dev
install: pip install --process-dependency-links dnsdiag
script: nosetests dnstraceroute.py
matrix:
fast_finish: true
include:
- python: "3.3"
- python: "3.4"
- python: "3.5"
- python: "3.6"
- python: "3.7"
dist: xenial
sudo: true
- python: "3.8-dev"
dist: xenial
sudo: true
- python: "pypy3"

View File

@ -1 +1,2 @@
include LICENSE README.md TODO.md public-servers.txt public-v4.txt rootservers.txt
include LICENSE README.md TODO.md public-servers.txt
include cymruwhois/*.py

View File

@ -1,4 +1,4 @@
[![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)]() [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffarrokhi%2Fdnsdiag.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffarrokhi%2Fdnsdiag?ref=badge_shield) [![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)
[![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
==================================================
@ -28,7 +28,10 @@ of view.
# prerequisites
This script requires python3 as well as latest
[dnspython](http://www.dnspython.org/) and
[cymruwhois](https://pythonhosted.org/cymruwhois/).
[cymruwhois](https://pythonhosted.org/cymruwhois/). Please note that
"dnstraceroute" requires a modified version of dnspython module. All required
third-party modules are included as GIT submodules. You just need to run `git
submodule update --init` and project directory to pull the required code.
# installation
@ -41,13 +44,13 @@ There are several ways that you can use this toolset. However using the sourceco
```
git clone https://github.com/farrokhi/dnsdiag.git
cd dnsdiag
pip3 install -r requirements.txt
git submodule update --init
```
2. You can alternatively install the package using pip:
```
pip3 install dnsdiag
pip3 install --process-dependency-links dnsdiag
```
## From Binary
@ -58,15 +61,15 @@ From time to time, binary version will be released for Windows, Mac OS X and Lin
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
% ./dnsping.py -c 3 -s 8.8.8.8 -t MX wikipedia.org
dnsping.py DNS: 8.8.8.8:53, hostname: wikipedia.org, rdatatype: MX
101 bytes from 8.8.8.8: seq=0 time=262.896 ms
101 bytes from 8.8.8.8: seq=1 time=305.608 ms
101 bytes from 8.8.8.8: seq=2 time=307.221 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
min=262.896 ms, avg=291.908 ms, max=307.221 ms, stddev=25.138 ms
```
This script calculates minimum, maximum and average response time as well as
jitter (stddev)
@ -78,16 +81,23 @@ 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
% ./dnstraceroute.py --expert -s 8.8.4.4 yahoo.com
dnstraceroute.py DNS: 8.8.4.4:53, hostname: yahoo.com, rdatatype: A
1 204.109.58.53 (204.109.58.53) 1 ms
2 208.79.80.5 (208.79.80.5) 1 ms
3 162.223.13.177 (162.223.13.177) 1 ms
4 208.79.80.254 (208.79.80.254) 7 ms
5 eqixva-google-gige.google.com (206.126.236.21) 7 ms
6 209.85.242.142 (209.85.242.142) 7 ms
7 72.14.236.148 (72.14.236.148) 8 ms
8 209.85.250.70 (209.85.250.70) 16 ms
9 74.125.37.222 (74.125.37.222) 16 ms
10 *
11 google-public-dns-b.google.com (8.8.4.4) 15 ms
=== Expert Hints ===
[*] public DNS server is next to a private IP address (possible hijacking)
[*] public DNS server is next to an invisible hop (probably a firewall)
```
Using `--expert` will instruct dnstraceroute to print expert hints (such as warnings of possible DNS traffic hijacking).
@ -97,21 +107,17 @@ 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-servers.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 -- --
% ./dnseval.py -f public-v4.txt -c3 ripe.net
server avg(ms) min(ms) max(ms) stddev(ms) lost(%) flags
----------------------------------------------------------------------------------
8.8.8.8 210.225 109.864 407.420 170.785 %0 QR RD RA
8.8.4.4 107.850 93.134 120.578 13.830 %0 QR RD RA
ns.ripe.net 118.911 114.874 123.389 4.275 %0 QR AA RD
4.2.2.1 104.380 102.449 106.588 2.083 %0 QR RD RA
4.2.2.2 131.056 99.143 193.711 54.264 %0 QR RD RA
4.2.2.3 98.956 97.463 100.310 1.429 %0 QR RD RA
4.2.2.4 223.173 97.418 463.728 208.398 %0 QR RD RA
4.2.2.5 104.290 97.264 117.878 11.770 %0 QR RD RA
```
### Author
@ -119,7 +125,7 @@ ns.ripe.net 174.855 160.949 187.458 10.099 %0 289
Babak Farrokhi
- twitter: [@farrokhi](https://twitter.com/farrokhi)
- github: [github.com/farrokhi](https://github.com/farrokhi/)
- github: [/farrokhi](https://github.com/farrokhi/)
- website: [farrokhi.net](https://farrokhi.net/)
@ -127,4 +133,8 @@ Babak Farrokhi
dnsdiag is released under a 2 clause BSD license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffarrokhi%2Fdnsdiag.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffarrokhi%2Fdnsdiag?ref=badge_large)
### Credits
- [@rthalley](https://github.com/rthalley) for invaluable [dnspython](https://github.com/rthalley/dnspython) library
- [@JustinAzoff](https://github.com/JustinAzoff) for [python-cymruwhois](https://github.com/JustinAzoff/python-cymruwhois) library
- [@bortzmeyer](https://github.com/bortzmeyer) for his feedback and patches

View File

@ -1,3 +1,6 @@
# TODO
- implement basic DNS client functionality into dnstraceroute to eliminate need
of modified dnspython module
- add support for python 2.x for the conservatives and faint-hearted ;-)
- dnsfingerprint.py tool to fingerprint DNS servers

1
cymruwhois Submodule

@ -0,0 +1 @@
Subproject commit a34543335cbef02b1b615e774ce5b6187afb0cc2

1
dns Symbolic link
View File

@ -0,0 +1 @@
dnspython/dns

View File

@ -25,61 +25,35 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import getopt
import ipaddress
import os
import signal
import socket
import sys
import time
import random
import string
from statistics import stdev
import dns.rdatatype
import dns.resolver
__author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD'
__version__ = "1.6.4"
__progname__ = os.path.basename(sys.argv[0])
__VERSION__ = 1.4
__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
print("""%s version %1.1f
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)
-m --cache-miss Force cache miss measurement by prepending a random hostname
-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__))
-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: 5)
-t --type DNS request record type (default: A)
""" % (__PROGNAME__, __VERSION__, __PROGNAME__))
sys.exit()
@ -140,23 +114,15 @@ def flags_to_text(flags):
return ' '.join(text_flags)
def random_string(min_length=5, max_length=10):
char_set = string.ascii_letters + string.digits
length = random.randint(min_length, max_length)
return ''.join(map(lambda unused: random.choice(char_set), range(length)))
def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=False, force_miss=False):
def dnsping(host, server, dnsrecord, timeout, count):
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'))
resolver.use_edns(edns=True, payload=0, ednsflags=8)
response_times = []
i = 0
@ -165,26 +131,15 @@ def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=Fal
if shutdown: # user pressed CTRL+C
break
try:
if force_miss:
fqdn = "_dnsdiag_%s_.%s" % (random_string(), host)
else:
fqdn = host
stime = time.perf_counter()
answers = resolver.query(fqdn, dnsrecord, tcp=use_tcp,
raise_on_no_answer=False) # todo: response validation in future
stime = time.time()
answers = resolver.query(host, dnsrecord) # todo: response validation in future
etime = time.time()
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer):
break
except dns.resolver.Timeout:
pass
except dns.resolver.NXDOMAIN:
etime = time.perf_counter()
if force_miss:
elapsed = (etime - stime) * 1000 # convert to milliseconds
response_times.append(elapsed)
else:
elapsed = answers.response.time * 1000 # convert to milliseconds
elapsed = (etime - stime) * 1000 # convert to milliseconds
response_times.append(elapsed)
r_sent = i + 1
@ -205,12 +160,10 @@ def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=Fal
r_avg = 0
r_stddev = 0
if answers is not None:
if answers:
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
return server, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags
def main():
@ -226,20 +179,14 @@ def main():
# defaults
dnsrecord = 'A'
count = 10
waittime = 2
waittime = 5
inputfilename = None
fromfile = False
use_tcp = False
use_edns = True
force_miss = False
verbose = False
color_mode = False
hostname = 'wikipedia.org'
try:
opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:TevCm",
["help", "file=", "count=", "type=", "wait=", "tcp", "edns", "verbose", "color",
"force-miss"])
opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:",
["help", "file=", "count=", "type=", "wait="])
except getopt.GetoptError as err:
print(err)
usage()
@ -259,49 +206,26 @@ def main():
fromfile = True
elif o in ("-w", "--wait"):
waittime = int(a)
elif o in ("-m", "--cache-miss"):
force_miss = True
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:
if inputfilename == '-':
# read from stdin
with sys.stdin as flist:
f = flist.read().splitlines()
else:
try:
with open(inputfilename, 'rt') as flist:
f = flist.read().splitlines()
except Exception as e:
print(e)
sys.exit(1)
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] # remove annoying blanks
f = [x for x in f if not x.startswith('#') and len(x)] # remove comments and empty entries
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) * '-')
print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) flags')
print((84 + width) * '-')
for server in f:
# check if we have a valid dns server address
if server.lstrip() == '': # deal with empty lines
@ -311,59 +235,27 @@ def main():
ipaddress.ip_address(server)
except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name
try:
resolver = socket.getaddrinfo(server, port=None)[1][4][0]
s = socket.getaddrinfo(server, port=None)[1][4][0]
except OSError:
print('Error: cannot resolve hostname:', server)
resolver = None
s = None
except:
pass
else:
resolver = server
s = server
if not resolver:
if not s:
continue
(s, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags) = dnsping(hostname, s, dnsrecord, waittime,
count)
try:
(resolver, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags, ttl, answers) = dnsping(
hostname,
resolver,
dnsrecord,
waittime,
count,
use_tcp=use_tcp,
use_edns=use_edns,
force_miss=force_miss
)
except dns.resolver.NXDOMAIN:
print('%-15s NXDOMAIN' % server)
continue
except Exception as e:
print('%s: %s' % (server, e))
continue
resolver = server.ljust(width + 1)
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" % (
resolver, 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.G, answer, color.N))
ans_index += 1
print("")
print("%s %-8.3f %-8.3f %-8.3f %-8.3f %%%-3d %25s" % (
s, r_avg, r_min, r_max, r_stddev, r_lost_percent, text_flags), flush=True)
except Exception as e:
print('%s: %s' % (server, e))
print('error: %s' % e)
sys.exit(1)

View File

@ -38,16 +38,14 @@ import dns.flags
import dns.rdatatype
import dns.resolver
__author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD'
__version__ = "1.6.4"
__progname__ = os.path.basename(sys.argv[0])
__VERSION__ = 1.4
__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
print("""%s version %1.1f
usage: %s [-h] [-q] [-v] [-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
@ -58,12 +56,10 @@ usage: %s [-ehqv] [-s server] [-p port] [-P port] [-S address] [-c count] [-t ty
-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, 0 for infinity)
-w --wait Maximum wait time for a reply (default: 2 seconds)
-i --interval Time between each request (default: 1 seconds)
-c --count Number of requests to send (default: 10)
-w --wait Maximum wait time for a reply (default: 5)
-t --type DNS request record type (default: A)
-e --edns Disable EDNS0 (default: Enabled)
""" % (__progname__, __version__, __progname__))
""" % (__PROGNAME__, __VERSION__, __PROGNAME__))
sys.exit(0)
@ -87,8 +83,7 @@ def main():
# defaults
dnsrecord = 'A'
count = 10
timeout = 2
interval = 1
timeout = 5
quiet = False
verbose = False
dnsserver = dns.resolver.get_default_resolver().nameservers[0]
@ -96,17 +91,16 @@ def main():
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"])
opts, args = getopt.getopt(sys.argv[1:], "qhc:s:t:w:vp:P:S:T46",
["help", "count=", "server=", "quiet", "type=", "wait=", "verbose",
"port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport="])
except getopt.GetoptError as err:
# print help information and exit:
print(err, file=sys.stderr) # will print something like "option -a not recognized"
print(err) # will print something like "option -a not recognized"
usage()
if args and len(args) == 1:
@ -118,7 +112,7 @@ def main():
if o in ("-h", "--help"):
usage()
elif o in ("-c", "--count"):
count = abs(int(a))
count = int(a)
elif o in ("-v", "--verbose"):
verbose = True
elif o in ("-s", "--server"):
@ -130,8 +124,6 @@ def main():
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"):
@ -140,12 +132,10 @@ def main():
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", flush=True)
print("WARNING: Source ports below 1024 are only available to superuser")
elif o in ("-S", "--srcip"):
src_ip = a
else:
@ -158,7 +148,7 @@ def main():
try:
dnsserver = socket.getaddrinfo(dnsserver, port=None, family=af)[1][4][0]
except OSError:
print('Error: cannot resolve hostname:', dnsserver, file=sys.stderr, flush=True)
print('Error: cannot resolve hostname:', dnsserver)
sys.exit(1)
resolver = dns.resolver.Resolver()
@ -168,61 +158,49 @@ def main():
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),
flush=True)
print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__PROGNAME__, dnsserver, dst_port, hostname, dnsrecord))
while not shutdown:
if 0 < count <= i:
for i in range(count):
if shutdown:
break
else:
i += 1
try:
stime = time.perf_counter()
stime = time.time()
answers = resolver.query(hostname, dnsrecord, source_port=src_port, source=src_ip, tcp=use_tcp,
raise_on_no_answer=False)
etime = time.perf_counter()
etime = time.time()
except dns.resolver.NoNameservers as e:
if not quiet:
print("No response to dns request", file=sys.stderr, flush=True)
print("No response to dns request")
if verbose:
print("error:", e, file=sys.stderr, flush=True)
print("error:", e)
sys.exit(1)
except dns.resolver.NXDOMAIN as e:
if not quiet:
print("Hostname does not exist", file=sys.stderr, flush=True)
print("Hostname does not exist")
if verbose:
print("Error:", e, file=sys.stderr, flush=True)
print("Error:", e)
sys.exit(1)
except dns.resolver.Timeout:
if not quiet:
print("Request timeout", flush=True)
print("Request timeout")
pass
except dns.resolver.NoAnswer:
if not quiet:
print("No answer", flush=True)
print("No answer")
pass
else:
elapsed = answers.response.time * 1000 # convert to milliseconds
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), flush=True)
"%d bytes from %s: seq=%-3d time=%3.3f ms" % (
len(str(answers.rrset)), dnsserver, i, elapsed))
if verbose:
print(answers.rrset, flush=True)
print("flags:", dns.flags.to_text(answers.response.flags), flush=True)
time_to_next = (stime + interval) - etime
if time_to_next > 0:
time.sleep(time_to_next)
print(answers.rrset)
print("flags:", dns.flags.to_text(answers.response.flags))
r_sent = i + 1
r_received = len(response_time)
@ -242,10 +220,9 @@ def main():
r_avg = 0
r_stddev = 0
print('\n--- %s dnsping statistics ---' % dnsserver, flush=True)
print('%d requests transmitted, %d responses received, %.0f%% lost' % (r_sent, r_received, r_lost_percent),
flush=True)
print('min=%.3f ms, avg=%.3f ms, max=%.3f ms, stddev=%.3f ms' % (r_min, r_avg, r_max, r_stddev), flush=True)
print('\n--- %s dnsping statistics ---' % dnsserver)
print('%d requests transmitted, %d responses received, %3.0f%% lost' % (r_sent, r_received, r_lost_percent))
print('min=%3.3f ms, avg=%3.3f ms, max=%3.3f ms, stddev=%3.3f ms' % (r_min, r_avg, r_max, r_stddev))
if __name__ == '__main__':

1
dnspython Submodule

@ -0,0 +1 @@
Subproject commit a55e23fbc4b960ca7f88342a89404fc07293096c

View File

@ -39,20 +39,13 @@ import dns.query
import dns.rdatatype
import dns.resolver
import cymruwhois
from cymruwhois import cymruwhois
# Global Variables
__author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD'
__version__ = "1.6.4"
__version__ = 1.4
_ttl = None
quiet = False
whois_cache = {}
shutdown = False
# Constants
__progname__ = os.path.basename(sys.argv[0])
WHOIS_CACHE = 'whois.cache'
class CustomSocket(socket.socket):
@ -71,6 +64,11 @@ def test_import():
pass
# Constants
__PROGNAME__ = os.path.basename(sys.argv[0])
WHOIS_CACHE = 'whois.cache'
class Colors(object):
N = '\033[m' # native
R = '\033[31m' # red
@ -87,58 +85,50 @@ class Colors(object):
self.B = ''
def whois_lookup(ip):
# Globarl Variables
shutdown = False
def whoisrecord(ip):
try:
global whois_cache
currenttime = time.time()
ts = currenttime
if ip in whois_cache:
asn, ts = whois_cache[ip]
if ip in whois:
asn, ts = whois[ip]
else:
ts = 0
if (currenttime - ts) > 36000:
c = cymruwhois.Client()
asn = c.lookup(ip)
whois_cache[ip] = (asn, currenttime)
whois[ip] = (asn, currenttime)
return asn
except Exception as e:
return e
def load_whois_cache(cachefile):
try:
pkl_file = open(WHOIS_CACHE, 'rb')
try:
pkl_file = open(cachefile, 'rb')
try:
whois = pickle.load(pkl_file)
pkl_file.close()
except Exception:
whois = {}
except IOError:
whois = pickle.load(pkl_file)
except EOFError:
whois = {}
return whois
def save_whois_cache(cachefile, whois_data):
pkl_file = open(cachefile, 'wb')
pickle.dump(whois_data, pkl_file)
pkl_file.close()
except IOError:
whois = {}
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('%s version %1.1f\n' % (__PROGNAME__, __version__))
print('usage: %s [-h] [-q] [-a] [-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(' -e --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(' -S --srcip Query source IP address (default: default interface address)')
print(' -c --count Maximum number of hops (default: 30)')
print(' -w --wait Maximum wait time for a reply (default: 2)')
print(' -w --wait Maximum wait time for a reply (default: 5)')
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()
@ -157,7 +147,6 @@ def expert_report(trace_path, color_mode):
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]
@ -167,15 +156,15 @@ def expert_report(trace_path, color_mode):
" %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:
if prev_hop == '*':
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:
if prev_hop 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:
if prev_hop 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
@ -183,18 +172,16 @@ def expert_report(trace_path, color_mode):
print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N))
def ping(resolver, hostname, dnsrecord, ttl, src_ip, use_edns=False):
def ping(resolver, hostname, dnsrecord, ttl):
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, source=src_ip, raise_on_no_answer=False)
resolver.query(hostname, dnsrecord)
except dns.resolver.NoNameservers as e:
if not quiet:
@ -212,8 +199,8 @@ def ping(resolver, hostname, dnsrecord, ttl, src_ip, use_edns=False):
pass
except SystemExit:
pass
except Exception as e:
print("unxpected error: ", e)
except:
print("unxpected error: ", sys.exc_info()[0])
sys.exit(1)
else:
reached = True
@ -235,21 +222,19 @@ def main():
dnsrecord = 'A'
count = 30
timeout = 2
timeout = 1
dnsserver = dns.resolver.get_default_resolver().nameservers[0]
dest_port = 53
src_ip = None
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:S:t:w:p:nexC",
opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:t:w:p:neC",
["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert",
"color", "srcip="])
"color"])
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
@ -265,14 +250,12 @@ def main():
usage()
elif o in ("-c", "--count"):
count = int(a)
elif o in ("-x", "--expert"):
elif o in ("-e", "--expert"):
expert_mode = True
elif o in ("-s", "--server"):
dnsserver = a
elif o in ("-q", "--quiet"):
quiet = True
elif o in ("-S", "--srcip"):
src_ip = a
elif o in ("-w", "--wait"):
timeout = int(a)
elif o in ("-t", "--type"):
@ -285,27 +268,14 @@ def main():
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.port = dest_port
resolver.lifetime = timeout
resolver.retry_servfail = 0
@ -316,8 +286,7 @@ def main():
trace_path = []
if not quiet:
print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, hostname, dnsrecord),
flush=True)
print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__PROGNAME__, dnsserver, dest_port, hostname, dnsrecord))
while True:
if shutdown:
@ -341,26 +310,26 @@ def main():
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, src_ip=src_ip, use_edns=use_edns)
stime = time.time()
thr = pool.submit(ping, resolver, hostname, dnsrecord, ttl)
try: # expect ICMP response
_, curr_addr = icmp_socket.recvfrom(512)
curr_addr = curr_addr[0]
except socket.error:
etime = time.perf_counter()
etime = time.time()
pass
finally:
etime = time.perf_counter()
etime = time.time()
icmp_socket.close()
reached = thr.result()
if reached:
curr_addr = dnsserver
stime = time.perf_counter() # need to recalculate elapsed time for last hop asynchronously
ping(resolver, hostname, dnsrecord, ttl, src_ip=src_ip, use_edns=use_edns)
etime = time.perf_counter()
stime = time.time() # need to recalculate elapsed time for last hop without waiting for an icmp error reply
ping(resolver, hostname, dnsrecord, ttl)
etime = time.time()
elapsed = abs(etime - stime) * 1000 # convert to milliseconds
@ -380,11 +349,11 @@ def main():
if curr_addr:
as_name = ""
if as_lookup:
asn = whois_lookup(curr_addr)
asn = whoisrecord(curr_addr)
as_name = ''
try:
if asn and asn.asn != "NA":
as_name = "[AS%s %s] " % (asn.asn, asn.owner)
as_name = "[%s %s] " % (asn.asn, asn.owner)
except AttributeError:
if shutdown:
sys.exit(0)
@ -392,18 +361,15 @@ def main():
c = color.N # default
if curr_addr != '*':
try:
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
except:
pass
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)
print("%d\t%s (%s%s%s) %s%d 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)
@ -420,7 +386,7 @@ def main():
if __name__ == '__main__':
try:
whois_cache = load_whois_cache(WHOIS_CACHE)
main()
finally:
save_whois_cache(WHOIS_CACHE, whois_cache)
pkl_file = open(WHOIS_CACHE, 'wb')
pickle.dump(whois, pkl_file)

View File

@ -1,42 +1,14 @@
#Cloudflare
1.0.0.1
1.1.1.1
2606:4700:4700::1001
2606:4700:4700::1111
#SafeDNS
195.46.39.39
195.46.39.40
#OpenDNS
208.67.220.220
208.67.222.222
2620:0:ccc::2
2620:0:ccd::2
#DYN DNS
216.146.35.35
216.146.36.36
#Level3
209.244.0.3
209.244.0.4
8.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
#freenom world
80.80.80.80
80.80.81.81
#Google
8.8.4.4
8.8.8.8
2001:4860:4860::8844
2001:4860:4860::8888
#PCH's Quad9
9.9.9.9
2620:fe::fe
149.112.112.112

View File

@ -1,36 +0,0 @@
#Cloudflare
1.0.0.1
1.1.1.1
#SafeDNS
195.46.39.39
195.46.39.40
#OpenDNS
208.67.220.220
208.67.222.222
#DYN DNS
216.146.35.35
216.146.36.36
#Level3
209.244.0.3
209.244.0.4
4.2.2.1
4.2.2.2
4.2.2.3
4.2.2.4
4.2.2.5
#freenom world
80.80.80.80
80.80.81.81
#Google
8.8.4.4
8.8.8.8
#PCH's Quad9
9.9.9.9
149.112.112.112

View File

@ -1,2 +0,0 @@
dnspython>=1.15.0
cymruwhois>=1.6

View File

@ -1,13 +0,0 @@
a.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

View File

@ -2,22 +2,14 @@ from setuptools import setup, find_packages
setup(
name="dnsdiag",
version="1.6.4",
version="1.3.5",
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.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet :: Name Service (DNS)",
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
@ -33,7 +25,7 @@ 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 performance",
keywords="dns traceroute ping",
url="https://dnsdiag.org/",
entry_points={
'console_scripts': [