Compare commits

...

42 Commits

Author SHA1 Message Date
3ecc777eb9
Ensure printed messages are flushed immediately (fixes #59)
Also send error messages to stderr
2019-03-15 17:47:48 +01:00
1d37debd96
Add -c0 support for infinite loop (fixes #57) 2019-03-15 17:36:39 +01:00
0a9b362587
Update classifiers to reflect support for newer python 3.x and pypy 2018-08-05 16:59:47 +04:30
a81525fc5f
Fix travis config to build 3.7 and 3.8-dev 2018-08-05 16:47:38 +04:30
3f8db03853
Fixing 3.7 build again 2018-08-05 16:45:55 +04:30
f58597f702
Add 3.8-dev target to test 2018-08-05 16:44:02 +04:30
133f5c7952
Trying to fix python 3.7 build on Travis 2018-08-05 16:41:18 +04:30
3e8e1e41d7
Add build-matrix support 2018-08-05 16:33:50 +04:30
befeb64a41
Test with more recent python versions and pypy 2018-08-05 16:28:12 +04:30
b0f1335664
Fix badges 2018-08-05 11:22:22 +04:30
0648619d2a
Merge branch 'master' of github.com:farrokhi/dnsdiag 2018-08-05 11:20:00 +04:30
151c383cc1
Merge pull request #56 from fossabot/master
Add license scan report and status
2018-08-05 11:16:14 +04:30
fossabot
b78ec5516c Add license scan report and status
Signed-off-by: fossabot <badges@fossa.io>
2018-08-04 23:35:44 -07:00
d3a1fd2308
Merge pull request #54 from Bashar/patch-4
updated with sort and comments
2018-05-15 18:24:47 +04:30
b8dbe54f1a
Merge pull request #55 from Bashar/patch-3
updated freenom and the missing quad9 with comments
2018-05-15 18:22:07 +04:30
Bashar Al-Abdulhadi
9e153c406d
updated with sort and comments 2018-05-15 16:47:35 +03:00
Bashar Al-Abdulhadi
0fe4786bba
updated freenom and the missing quad9 with comments 2018-05-15 16:44:57 +03:00
9b47a3084a
minor description change 2018-05-15 16:45:05 +04:30
b1b4f06730
Include sample server files 2018-04-03 17:08:14 +04:30
25c243efa1
Preparing for v1.6.4
General improvements and fixes
2018-04-03 17:01:50 +04:30
a0c26242ec
Add sample input file with IPv4 address of public resolvers
for people who are still on legacy networks from late 20th century.
2018-04-02 22:38:00 +04:30
e93120da19
Fix sample input filename in README 2018-04-02 22:36:02 +04:30
dc7f03eac3
Add CloudFlare's new resolver (v4/v6) (Fixes #51) 2018-04-02 11:52:29 +04:30
7390514877
Deal with failures to open input file (fixes #50) 2018-02-18 07:36:37 +03:30
bbff3b44f6
Use "dnsdiag" keyword when generating random hostnames 2018-01-23 17:36:02 +03:30
d93b87b2a2
Remove a leftover debug message 2018-01-23 17:14:23 +03:30
8a9acd9100
Add -m to force cache-miss measurement in dnseval (Closes #41)
Using `-m` causes dnseval to add a random hostname prefix to the given
domain name (format is "_dnseval_RANDOM_."). This will cause NXDOMAIN
and the query fails, but we do measure the response time anyway.
2018-01-23 15:20:36 +03:30
8283f4dbc2
Ability to have comments in resolvers list (Closes #43) 2018-01-23 12:25:46 +03:30
e3ddfff88e
User can specify source address (Fixes #46)
- also respect resolver port number (if specified by user)
2018-01-23 12:13:50 +03:30
1b9849c224
Use more accurate response time measurement method (fixes #44) 2018-01-23 11:32:40 +03:30
76b843d728
Merge pull request #42 from webernetz/patch-1
Update public-servers.txt
2017-11-27 23:03:21 +03:30
Johannes Weber
037457f0cc
Update public-servers.txt
Added the open DNS resolvers from Quad9 and OpenDNS. Both for IPv6 and legacy IP.
2017-11-27 20:31:46 +01:00
4c9aaf921b
Merge pull request #38 from will-h/master
Allow '-f -' to denote stdin
2017-11-04 20:12:25 +03:30
Will Hargrave
f1807d34ea Allow '-f -' to denote stdin 2017-11-01 21:34:56 +00:00
9caa006e5b
Change default ping interval to 1 second 2017-10-06 10:46:16 +03:30
5a99d58dd1
Ignore virtualenv directory 2017-10-06 10:38:00 +03:30
c92ea53e57
Use a more readable color in verbose mode 2017-05-02 14:45:03 +04:30
47dbc6afe3
Update installation notes to reflect recent changes 2017-05-02 14:18:52 +04:30
39e564e626
Use cymruwhois from pypi and remove submodule
- cymruwhois maintainer recovered his access to pypi and
  uploaded latest package. There is no need to use it as
  submodule anymore. So we added an external dependency.

- Refactor whois data caching in dnstraceroute and unbreak
  caching mechanism which was broken since previous commit
  due to a bug in time delta calculation.
2017-05-02 14:16:17 +04:30
3df692e6db
No more needed 2017-04-30 21:18:45 +04:30
2dcf0e7b78
Fix setuptools 2017-04-30 21:12:08 +04:30
a0f9cae673
Fix setuptool installation and dependecies 2017-04-30 19:35:36 +04:30
13 changed files with 266 additions and 128 deletions

3
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

View File

@ -1,10 +1,18 @@
language: python language: python
python: sudo: false
- "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" install: "pip install -r requirements.txt"
script: nosetests dnstraceroute.py 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,2 +1 @@
include LICENSE README.md TODO.md public-servers.txt include LICENSE README.md TODO.md public-servers.txt public-v4.txt rootservers.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)]() [![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)]() [![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)
DNS Diagnostics and Performance Measurement Tools DNS Diagnostics and Performance Measurement Tools
================================================== ==================================================
@ -29,8 +29,6 @@ of view.
This script requires python3 as well as latest This script requires python3 as well as latest
[dnspython](http://www.dnspython.org/) and [dnspython](http://www.dnspython.org/) and
[cymruwhois](https://pythonhosted.org/cymruwhois/). [cymruwhois](https://pythonhosted.org/cymruwhois/).
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 # installation
@ -43,13 +41,13 @@ There are several ways that you can use this toolset. However using the sourceco
``` ```
git clone https://github.com/farrokhi/dnsdiag.git git clone https://github.com/farrokhi/dnsdiag.git
cd dnsdiag cd dnsdiag
git submodule update --init pip3 install -r requirements.txt
``` ```
2. You can alternatively install the package using pip: 2. You can alternatively install the package using pip:
``` ```
pip3 install --process-dependency-links dnsdiag pip3 install dnsdiag
``` ```
## From Binary ## From Binary
@ -99,7 +97,7 @@ 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 of DNS servers. This script is meant for comparing response time of multiple
DNS servers at once: DNS servers at once:
``` ```
% ./dnseval.py -t AAAA -f public-v4.txt -c10 yahoo.com % ./dnseval.py -t AAAA -f public-servers.txt -c10 yahoo.com
server avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags 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.8.8 270.791 215.599 307.498 40.630 %0 298 QR -- -- RD RA -- --
@ -129,3 +127,4 @@ Babak Farrokhi
dnsdiag is released under a 2 clause BSD license. 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)

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

View File

@ -33,6 +33,8 @@ import signal
import socket import socket
import sys import sys
import time import time
import random
import string
from statistics import stdev from statistics import stdev
import dns.rdatatype import dns.rdatatype
@ -40,7 +42,7 @@ import dns.resolver
__author__ = 'Babak Farrokhi (babak@farrokhi.net)' __author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD' __license__ = 'BSD'
__version__ = "1.6.0" __version__ = "1.6.4"
__progname__ = os.path.basename(sys.argv[0]) __progname__ = os.path.basename(sys.argv[0])
shutdown = False shutdown = False
@ -67,15 +69,16 @@ def usage():
print("""%s version %s print("""%s version %s
usage: %s [-h] [-f server-list] [-c count] [-t type] [-w wait] hostname usage: %s [-h] [-f server-list] [-c count] [-t type] [-w wait] hostname
-h --help show this help -h --help Show this help
-f --file dns server list to use (default: system resolvers) -f --file DNS server list to use (default: system resolvers)
-c --count number of requests to send (default: 10) -c --count Number of requests to send (default: 10)
-w --wait maximum wait time for a reply (default: 2) -m --cache-miss Force cache miss measurement by prepending a random hostname
-t --type DNS request record type (default: A) -w --wait Maximum wait time for a reply (default: 2)
-T --tcp Use TCP instead of UDP -t --type DNS request record type (default: A)
-e --edns Disable EDNS0 (Default: Enabled) -T --tcp Use TCP instead of UDP
-C --color Print colorful output -e --edns Disable EDNS0 (Default: Enabled)
-v --verbose Print actual dns response -C --color Print colorful output
-v --verbose Print actual dns response
""" % (__progname__, __version__, __progname__)) """ % (__progname__, __version__, __progname__))
sys.exit() sys.exit()
@ -137,7 +140,13 @@ def flags_to_text(flags):
return ' '.join(text_flags) return ' '.join(text_flags)
def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=False): 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):
resolver = dns.resolver.Resolver() resolver = dns.resolver.Resolver()
resolver.nameservers = [server] resolver.nameservers = [server]
resolver.timeout = timeout resolver.timeout = timeout
@ -156,16 +165,26 @@ def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=Fal
if shutdown: # user pressed CTRL+C if shutdown: # user pressed CTRL+C
break break
try: try:
if force_miss:
fqdn = "_dnsdiag_%s_.%s" % (random_string(), host)
else:
fqdn = host
stime = time.perf_counter() stime = time.perf_counter()
answers = resolver.query(host, dnsrecord, tcp=use_tcp, answers = resolver.query(fqdn, dnsrecord, tcp=use_tcp,
raise_on_no_answer=False) # todo: response validation in future raise_on_no_answer=False) # todo: response validation in future
etime = time.perf_counter()
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer): except (dns.resolver.NoNameservers, dns.resolver.NoAnswer):
break break
except dns.resolver.Timeout: except dns.resolver.Timeout:
pass pass
except dns.resolver.NXDOMAIN:
etime = time.perf_counter()
if force_miss:
elapsed = (etime - stime) * 1000 # convert to milliseconds
response_times.append(elapsed)
else: else:
elapsed = (etime - stime) * 1000 # convert to milliseconds elapsed = answers.response.time * 1000 # convert to milliseconds
response_times.append(elapsed) response_times.append(elapsed)
r_sent = i + 1 r_sent = i + 1
@ -212,13 +231,15 @@ def main():
fromfile = False fromfile = False
use_tcp = False use_tcp = False
use_edns = True use_edns = True
force_miss = False
verbose = False verbose = False
color_mode = False color_mode = False
hostname = 'wikipedia.org' hostname = 'wikipedia.org'
try: try:
opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:TevC", opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:TevCm",
["help", "file=", "count=", "type=", "wait=", "tcp", "edns", "verbose", "color"]) ["help", "file=", "count=", "type=", "wait=", "tcp", "edns", "verbose", "color",
"force-miss"])
except getopt.GetoptError as err: except getopt.GetoptError as err:
print(err) print(err)
usage() usage()
@ -238,6 +259,8 @@ def main():
fromfile = True fromfile = True
elif o in ("-w", "--wait"): elif o in ("-w", "--wait"):
waittime = int(a) waittime = int(a)
elif o in ("-m", "--cache-miss"):
force_miss = True
elif o in ("-t", "--type"): elif o in ("-t", "--type"):
dnsrecord = a dnsrecord = a
elif o in ("-T", "--tcp"): elif o in ("-T", "--tcp"):
@ -256,14 +279,25 @@ def main():
try: try:
if fromfile: if fromfile:
with open(inputfilename, 'rt') as flist: if inputfilename == '-':
f = flist.read().splitlines() # 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)
else: else:
f = resolvers f = resolvers
if len(f) == 0: if len(f) == 0:
print("No nameserver specified") print("No nameserver specified")
f = [name.strip() for name in f] 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
width = maxlen(f) width = maxlen(f)
blanks = (width - 5) * ' ' blanks = (width - 5) * ' '
print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags') print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags')
@ -277,25 +311,29 @@ def main():
ipaddress.ip_address(server) ipaddress.ip_address(server)
except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name
try: try:
s = socket.getaddrinfo(server, port=None)[1][4][0] resolver = socket.getaddrinfo(server, port=None)[1][4][0]
except OSError: except OSError:
print('Error: cannot resolve hostname:', server) print('Error: cannot resolve hostname:', server)
s = None resolver = None
except: except:
pass pass
else: else:
s = server resolver = server
if not s: if not resolver:
continue continue
try: try:
(s, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags, ttl, answers) = dnsping(hostname, s, (resolver, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags, ttl, answers) = dnsping(
dnsrecord, hostname,
waittime, resolver,
count, dnsrecord,
use_tcp=use_tcp, waittime,
use_edns=use_edns) count,
use_tcp=use_tcp,
use_edns=use_edns,
force_miss=force_miss
)
except dns.resolver.NXDOMAIN: except dns.resolver.NXDOMAIN:
print('%-15s NXDOMAIN' % server) print('%-15s NXDOMAIN' % server)
continue continue
@ -303,7 +341,7 @@ def main():
print('%s: %s' % (server, e)) print('%s: %s' % (server, e))
continue continue
s = server.ljust(width + 1) resolver = server.ljust(width + 1)
text_flags = flags_to_text(flags) text_flags = flags_to_text(flags)
s_ttl = str(ttl) s_ttl = str(ttl)
@ -315,11 +353,12 @@ def main():
else: else:
l_color = color.N l_color = color.N
print("%s %-8.3f %-8.3f %-8.3f %-8.3f %s%%%-3d%s %-8s %21s" % ( 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) 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'): if verbose and hasattr(answers, 'response'):
ans_index = 1 ans_index = 1
for answer in answers.response.answer: for answer in answers.response.answer:
print("Answer %d [ %s%s%s ]" % (ans_index, color.B, answer, color.N)) print("Answer %d [ %s%s%s ]" % (ans_index, color.G, answer, color.N))
ans_index += 1 ans_index += 1
print("") print("")

View File

@ -40,7 +40,7 @@ import dns.resolver
__author__ = 'Babak Farrokhi (babak@farrokhi.net)' __author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD' __license__ = 'BSD'
__version__ = "1.6.0" __version__ = "1.6.4"
__progname__ = os.path.basename(sys.argv[0]) __progname__ = os.path.basename(sys.argv[0])
shutdown = False shutdown = False
@ -58,9 +58,9 @@ usage: %s [-ehqv] [-s server] [-p port] [-P port] [-S address] [-c count] [-t ty
-6 --ipv6 Use IPv6 as default network protocol -6 --ipv6 Use IPv6 as default network protocol
-P --srcport Query source port number (default: 0) -P --srcport Query source port number (default: 0)
-S --srcip Query source IP address (default: default interface address) -S --srcip Query source IP address (default: default interface address)
-c --count Number of requests to send (default: 10) -c --count Number of requests to send (default: 10, 0 for infinity)
-w --wait Maximum wait time for a reply (default: 2 seconds) -w --wait Maximum wait time for a reply (default: 2 seconds)
-i --interval Time between each request (default: 0 seconds) -i --interval Time between each request (default: 1 seconds)
-t --type DNS request record type (default: A) -t --type DNS request record type (default: A)
-e --edns Disable EDNS0 (default: Enabled) -e --edns Disable EDNS0 (default: Enabled)
""" % (__progname__, __version__, __progname__)) """ % (__progname__, __version__, __progname__))
@ -88,7 +88,7 @@ def main():
dnsrecord = 'A' dnsrecord = 'A'
count = 10 count = 10
timeout = 2 timeout = 2
interval = 0 interval = 1
quiet = False quiet = False
verbose = False verbose = False
dnsserver = dns.resolver.get_default_resolver().nameservers[0] dnsserver = dns.resolver.get_default_resolver().nameservers[0]
@ -106,7 +106,7 @@ def main():
"port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport=", "edns"]) "port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport=", "edns"])
except getopt.GetoptError as err: except getopt.GetoptError as err:
# print help information and exit: # print help information and exit:
print(err) # will print something like "option -a not recognized" print(err, file=sys.stderr) # will print something like "option -a not recognized"
usage() usage()
if args and len(args) == 1: if args and len(args) == 1:
@ -118,7 +118,7 @@ def main():
if o in ("-h", "--help"): if o in ("-h", "--help"):
usage() usage()
elif o in ("-c", "--count"): elif o in ("-c", "--count"):
count = int(a) count = abs(int(a))
elif o in ("-v", "--verbose"): elif o in ("-v", "--verbose"):
verbose = True verbose = True
elif o in ("-s", "--server"): elif o in ("-s", "--server"):
@ -145,7 +145,7 @@ def main():
elif o in ("-P", "--srcport"): elif o in ("-P", "--srcport"):
src_port = int(a) src_port = int(a)
if src_port < 1024: if src_port < 1024:
print("WARNING: Source ports below 1024 are only available to superuser") print("WARNING: Source ports below 1024 are only available to superuser", flush=True)
elif o in ("-S", "--srcip"): elif o in ("-S", "--srcip"):
src_ip = a src_ip = a
else: else:
@ -158,7 +158,7 @@ def main():
try: try:
dnsserver = socket.getaddrinfo(dnsserver, port=None, family=af)[1][4][0] dnsserver = socket.getaddrinfo(dnsserver, port=None, family=af)[1][4][0]
except OSError: except OSError:
print('Error: cannot resolve hostname:', dnsserver) print('Error: cannot resolve hostname:', dnsserver, file=sys.stderr, flush=True)
sys.exit(1) sys.exit(1)
resolver = dns.resolver.Resolver() resolver = dns.resolver.Resolver()
@ -174,11 +174,16 @@ def main():
response_time = [] response_time = []
i = 0 i = 0
print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dst_port, hostname, dnsrecord)) print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dst_port, hostname, dnsrecord),
flush=True)
for i in range(count): while not shutdown:
if shutdown:
if 0 < count <= i:
break break
else:
i += 1
try: try:
stime = time.perf_counter() stime = time.perf_counter()
answers = resolver.query(hostname, dnsrecord, source_port=src_port, source=src_ip, tcp=use_tcp, answers = resolver.query(hostname, dnsrecord, source_port=src_port, source=src_ip, tcp=use_tcp,
@ -186,34 +191,34 @@ def main():
etime = time.perf_counter() etime = time.perf_counter()
except dns.resolver.NoNameservers as e: except dns.resolver.NoNameservers as e:
if not quiet: if not quiet:
print("No response to dns request") print("No response to dns request", file=sys.stderr, flush=True)
if verbose: if verbose:
print("error:", e) print("error:", e, file=sys.stderr, flush=True)
sys.exit(1) sys.exit(1)
except dns.resolver.NXDOMAIN as e: except dns.resolver.NXDOMAIN as e:
if not quiet: if not quiet:
print("Hostname does not exist") print("Hostname does not exist", file=sys.stderr, flush=True)
if verbose: if verbose:
print("Error:", e) print("Error:", e, file=sys.stderr, flush=True)
sys.exit(1) sys.exit(1)
except dns.resolver.Timeout: except dns.resolver.Timeout:
if not quiet: if not quiet:
print("Request timeout") print("Request timeout", flush=True)
pass pass
except dns.resolver.NoAnswer: except dns.resolver.NoAnswer:
if not quiet: if not quiet:
print("No answer") print("No answer", flush=True)
pass pass
else: else:
elapsed = (etime - stime) * 1000 # convert to milliseconds elapsed = answers.response.time * 1000 # convert to milliseconds
response_time.append(elapsed) response_time.append(elapsed)
if not quiet: if not quiet:
print( print(
"%d bytes from %s: seq=%-3d time=%.3f ms" % ( "%d bytes from %s: seq=%-3d time=%.3f ms" % (
len(str(answers.rrset)), dnsserver, i, elapsed)) len(str(answers.rrset)), dnsserver, i, elapsed), flush=True)
if verbose: if verbose:
print(answers.rrset) print(answers.rrset, flush=True)
print("flags:", dns.flags.to_text(answers.response.flags)) print("flags:", dns.flags.to_text(answers.response.flags), flush=True)
time_to_next = (stime + interval) - etime time_to_next = (stime + interval) - etime
if time_to_next > 0: if time_to_next > 0:
@ -237,9 +242,10 @@ def main():
r_avg = 0 r_avg = 0
r_stddev = 0 r_stddev = 0
print('\n--- %s dnsping statistics ---' % dnsserver) print('\n--- %s dnsping statistics ---' % dnsserver, flush=True)
print('%d requests transmitted, %d responses received, %.0f%% lost' % (r_sent, r_received, r_lost_percent)) 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)) 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)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -39,13 +39,20 @@ import dns.query
import dns.rdatatype import dns.rdatatype
import dns.resolver import dns.resolver
from cymruwhois import cymruwhois import cymruwhois
# Global Variables
__author__ = 'Babak Farrokhi (babak@farrokhi.net)' __author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD' __license__ = 'BSD'
__version__ = "1.6.0" __version__ = "1.6.4"
_ttl = None _ttl = None
quiet = False quiet = False
whois_cache = {}
shutdown = False
# Constants
__progname__ = os.path.basename(sys.argv[0])
WHOIS_CACHE = 'whois.cache'
class CustomSocket(socket.socket): class CustomSocket(socket.socket):
@ -64,11 +71,6 @@ def test_import():
pass pass
# Constants
__progname__ = os.path.basename(sys.argv[0])
WHOIS_CACHE = 'whois.cache'
class Colors(object): class Colors(object):
N = '\033[m' # native N = '\033[m' # native
R = '\033[31m' # red R = '\033[31m' # red
@ -85,35 +87,41 @@ class Colors(object):
self.B = '' self.B = ''
# Globarl Variables def whois_lookup(ip):
shutdown = False
def whoisrecord(ip):
try: try:
currenttime = time.perf_counter() global whois_cache
currenttime = time.time()
ts = currenttime ts = currenttime
if ip in whois: if ip in whois_cache:
asn, ts = whois[ip] asn, ts = whois_cache[ip]
else: else:
ts = 0 ts = 0
if (currenttime - ts) > 36000: if (currenttime - ts) > 36000:
c = cymruwhois.Client() c = cymruwhois.Client()
asn = c.lookup(ip) asn = c.lookup(ip)
whois[ip] = (asn, currenttime) whois_cache[ip] = (asn, currenttime)
return asn return asn
except Exception as e: except Exception as e:
return e return e
try: def load_whois_cache(cachefile):
pkl_file = open(WHOIS_CACHE, 'rb')
try: try:
whois = pickle.load(pkl_file) pkl_file = open(cachefile, 'rb')
except EOFError: try:
whois = pickle.load(pkl_file)
pkl_file.close()
except Exception:
whois = {}
except IOError:
whois = {} whois = {}
except IOError: return whois
whois = {}
def save_whois_cache(cachefile, whois_data):
pkl_file = open(cachefile, 'wb')
pickle.dump(whois_data, pkl_file)
pkl_file.close()
def usage(): def usage():
@ -125,6 +133,7 @@ def usage():
print(' -a --asn Turn on AS# lookups for each hop encountered') print(' -a --asn Turn on AS# lookups for each hop encountered')
print(' -s --server DNS server to use (default: first system resolver)') print(' -s --server DNS server to use (default: first system resolver)')
print(' -p --port DNS server port number (default: 53)') 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(' -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: 2)')
print(' -t --type DNS request record type (default: A)') print(' -t --type DNS request record type (default: A)')
@ -174,7 +183,7 @@ def expert_report(trace_path, color_mode):
print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N)) print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N))
def ping(resolver, hostname, dnsrecord, ttl, use_edns=False): def ping(resolver, hostname, dnsrecord, ttl, src_ip, use_edns=False):
global _ttl global _ttl
reached = False reached = False
@ -185,7 +194,7 @@ def ping(resolver, hostname, dnsrecord, ttl, use_edns=False):
resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO')) resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO'))
try: try:
resolver.query(hostname, dnsrecord, raise_on_no_answer=False) resolver.query(hostname, dnsrecord, source=src_ip, raise_on_no_answer=False)
except dns.resolver.NoNameservers as e: except dns.resolver.NoNameservers as e:
if not quiet: if not quiet:
@ -229,6 +238,7 @@ def main():
timeout = 2 timeout = 2
dnsserver = dns.resolver.get_default_resolver().nameservers[0] dnsserver = dns.resolver.get_default_resolver().nameservers[0]
dest_port = 53 dest_port = 53
src_ip = None
hops = 0 hops = 0
as_lookup = False as_lookup = False
expert_mode = False expert_mode = False
@ -237,9 +247,9 @@ def main():
color_mode = False color_mode = False
try: try:
opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:t:w:p:nexC", opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:S:t:w:p:nexC",
["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert", ["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert",
"color"]) "color", "srcip="])
except getopt.GetoptError as err: except getopt.GetoptError as err:
# print help information and exit: # print help information and exit:
print(err) # will print something like "option -a not recognized" print(err) # will print something like "option -a not recognized"
@ -261,6 +271,8 @@ def main():
dnsserver = a dnsserver = a
elif o in ("-q", "--quiet"): elif o in ("-q", "--quiet"):
quiet = True quiet = True
elif o in ("-S", "--srcip"):
src_ip = a
elif o in ("-w", "--wait"): elif o in ("-w", "--wait"):
timeout = int(a) timeout = int(a)
elif o in ("-t", "--type"): elif o in ("-t", "--type"):
@ -293,6 +305,7 @@ def main():
resolver = dns.resolver.Resolver() resolver = dns.resolver.Resolver()
resolver.nameservers = [dnsserver] resolver.nameservers = [dnsserver]
resolver.timeout = timeout resolver.timeout = timeout
resolver.port = dest_port
resolver.lifetime = timeout resolver.lifetime = timeout
resolver.retry_servfail = 0 resolver.retry_servfail = 0
@ -329,7 +342,7 @@ def main():
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: # dispatch dns lookup to another thread with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: # dispatch dns lookup to another thread
stime = time.perf_counter() stime = time.perf_counter()
thr = pool.submit(ping, resolver, hostname, dnsrecord, ttl, use_edns=use_edns) thr = pool.submit(ping, resolver, hostname, dnsrecord, ttl, src_ip=src_ip, use_edns=use_edns)
try: # expect ICMP response try: # expect ICMP response
_, curr_addr = icmp_socket.recvfrom(512) _, curr_addr = icmp_socket.recvfrom(512)
@ -345,8 +358,8 @@ def main():
if reached: if reached:
curr_addr = dnsserver curr_addr = dnsserver
stime = time.perf_counter() # need to recalculate elapsed time for last hop without waiting for an icmp error reply stime = time.perf_counter() # need to recalculate elapsed time for last hop asynchronously
ping(resolver, hostname, dnsrecord, ttl, use_edns=use_edns) ping(resolver, hostname, dnsrecord, ttl, src_ip=src_ip, use_edns=use_edns)
etime = time.perf_counter() etime = time.perf_counter()
elapsed = abs(etime - stime) * 1000 # convert to milliseconds elapsed = abs(etime - stime) * 1000 # convert to milliseconds
@ -367,11 +380,11 @@ def main():
if curr_addr: if curr_addr:
as_name = "" as_name = ""
if as_lookup: if as_lookup:
asn = whoisrecord(curr_addr) asn = whois_lookup(curr_addr)
as_name = '' as_name = ''
try: try:
if asn and asn.asn != "NA": if asn and asn.asn != "NA":
as_name = "[%s %s] " % (asn.asn, asn.owner) as_name = "[AS%s %s] " % (asn.asn, asn.owner)
except AttributeError: except AttributeError:
if shutdown: if shutdown:
sys.exit(0) sys.exit(0)
@ -379,13 +392,16 @@ def main():
c = color.N # default c = color.N # default
if curr_addr != '*': if curr_addr != '*':
IP = ipaddress.ip_address(curr_addr) try:
if IP.is_private: IP = ipaddress.ip_address(curr_addr)
c = color.R if IP.is_private:
if IP.is_reserved: c = color.R
c = color.B if IP.is_reserved:
if curr_addr == dnsserver: c = color.B
c = color.G if curr_addr == dnsserver:
c = color.G
except:
pass
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%.3f ms" % (ttl, curr_name, c, curr_addr, color.N, as_name, elapsed), flush=True)
trace_path.append(curr_addr) trace_path.append(curr_addr)
@ -404,7 +420,7 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
try: try:
whois_cache = load_whois_cache(WHOIS_CACHE)
main() main()
finally: finally:
pkl_file = open(WHOIS_CACHE, 'wb') save_whois_cache(WHOIS_CACHE, whois_cache)
pickle.dump(whois, pkl_file)

View File

@ -1,15 +1,42 @@
8.8.8.8 #Cloudflare
8.8.4.4 1.0.0.1
2001:4860:4860::8888 1.1.1.1
2001:4860:4860::8844 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
4.2.2.1 4.2.2.1
4.2.2.2 4.2.2.2
4.2.2.3 4.2.2.3
4.2.2.4 4.2.2.4
4.2.2.5 4.2.2.5
209.244.0.3
209.244.0.4 #freenom world
195.46.39.39 80.80.80.80
195.46.39.40 80.80.81.81
216.146.35.35 #Google
216.146.36.36 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

36
public-v4.txt Normal file
View File

@ -0,0 +1,36 @@
#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 +1,2 @@
dnspython==1.15.0 dnspython>=1.15.0
cymruwhois>=1.6

View File

@ -2,14 +2,22 @@ from setuptools import setup, find_packages
setup( setup(
name="dnsdiag", name="dnsdiag",
version="1.6.0", version="1.6.4",
packages=find_packages(), packages=find_packages(),
scripts=["dnseval.py", "dnsping.py", "dnstraceroute.py"],
install_requires=['dnspython>=1.15.0', 'cymruwhois>=1.6'],
classifiers=[ classifiers=[
"Topic :: System :: Networking", "Topic :: System :: Networking",
"Environment :: Console", "Environment :: Console",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4", "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)", "Topic :: Internet :: Name Service (DNS)",
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent", "Operating System :: OS Independent",
@ -25,7 +33,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. as well as tracing the path your DNS query takes to get to DNS server.
""", """,
license="BSD", license="BSD",
keywords="dns traceroute ping", keywords="dns traceroute ping performance",
url="https://dnsdiag.org/", url="https://dnsdiag.org/",
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [