2016-02-06 11:43:47 +03:30
|
|
|
#!/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
|
2016-05-07 15:08:15 +04:30
|
|
|
import ipaddress
|
2016-04-09 11:37:42 +04:30
|
|
|
import os
|
2016-02-10 09:23:06 +03:30
|
|
|
import signal
|
2016-05-07 15:08:15 +04:30
|
|
|
import socket
|
2016-02-10 09:23:06 +03:30
|
|
|
import sys
|
2016-02-06 11:43:47 +03:30
|
|
|
import time
|
|
|
|
from statistics import stdev
|
|
|
|
|
2016-04-12 13:33:54 +04:30
|
|
|
import dns.rdatatype
|
|
|
|
import dns.resolver
|
|
|
|
|
2016-06-12 12:15:09 +04:30
|
|
|
__VERSION__ = 1.4
|
2016-04-09 11:37:42 +04:30
|
|
|
__PROGNAME__ = os.path.basename(sys.argv[0])
|
2016-05-07 14:47:54 +04:30
|
|
|
shutdown = False
|
2016-02-06 11:43:47 +03:30
|
|
|
|
2016-04-10 16:02:23 +04:30
|
|
|
resolvers = dns.resolver.get_default_resolver().nameservers
|
2016-02-10 09:23:06 +03:30
|
|
|
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
def usage():
|
2016-04-25 11:25:18 +04:30
|
|
|
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)
|
|
|
|
-w --wait maximum wait time for a reply (default: 5)
|
|
|
|
-t --type DNS request record type (default: A)
|
2016-06-14 18:31:58 +04:30
|
|
|
-T --tcp Use TCP instead of UDP
|
2016-06-26 16:42:19 +04:30
|
|
|
-e --edns Use EDNS0
|
2016-04-25 11:25:18 +04:30
|
|
|
""" % (__PROGNAME__, __VERSION__, __PROGNAME__))
|
2016-05-09 13:34:28 +04:30
|
|
|
sys.exit()
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
|
|
|
|
def signal_handler(sig, frame):
|
2016-05-07 14:47:54 +04:30
|
|
|
global shutdown
|
|
|
|
if shutdown: # pressed twice, so exit immediately
|
2016-05-09 13:34:28 +04:30
|
|
|
sys.exit(0)
|
2016-05-07 14:47:54 +04:30
|
|
|
shutdown = True # pressed once, exit gracefully
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
|
2016-04-14 20:51:13 +04:30
|
|
|
def maxlen(names):
|
|
|
|
sn = sorted(names, key=len)
|
|
|
|
return len(sn[-1])
|
2016-04-10 15:36:29 +04:30
|
|
|
|
|
|
|
|
2016-05-25 20:36:14 +04:30
|
|
|
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('--')
|
|
|
|
|
2016-06-14 16:39:44 +04:30
|
|
|
return ' '.join(text_flags)
|
2016-05-25 20:36:14 +04:30
|
|
|
|
|
|
|
|
2016-06-26 16:42:19 +04:30
|
|
|
def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=False):
|
2016-02-06 11:43:47 +03:30
|
|
|
resolver = dns.resolver.Resolver()
|
|
|
|
resolver.nameservers = [server]
|
|
|
|
resolver.timeout = timeout
|
|
|
|
resolver.lifetime = timeout
|
|
|
|
resolver.retry_servfail = 0
|
2016-05-25 20:36:14 +04:30
|
|
|
flags = 0
|
|
|
|
answers = None
|
2016-06-26 16:42:19 +04:30
|
|
|
if use_edns:
|
|
|
|
resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO'))
|
2016-02-06 11:43:47 +03:30
|
|
|
|
2016-06-14 16:39:44 +04:30
|
|
|
response_times = []
|
2016-02-06 11:43:47 +03:30
|
|
|
i = 0
|
|
|
|
|
|
|
|
for i in range(count):
|
2016-06-14 16:39:44 +04:30
|
|
|
if shutdown: # user pressed CTRL+C
|
2016-02-06 11:43:47 +03:30
|
|
|
break
|
|
|
|
try:
|
|
|
|
stime = time.time()
|
2016-06-26 16:42:19 +04:30
|
|
|
answers = resolver.query(host, dnsrecord, tcp=use_tcp,
|
|
|
|
raise_on_no_answer=False) # todo: response validation in future
|
2016-02-06 11:43:47 +03:30
|
|
|
etime = time.time()
|
|
|
|
except (dns.resolver.NoNameservers, dns.resolver.NoAnswer):
|
|
|
|
break
|
|
|
|
except dns.resolver.Timeout:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
elapsed = (etime - stime) * 1000 # convert to milliseconds
|
2016-06-14 16:39:44 +04:30
|
|
|
response_times.append(elapsed)
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
r_sent = i + 1
|
2016-06-14 16:39:44 +04:30
|
|
|
r_received = len(response_times)
|
2016-02-06 11:43:47 +03:30
|
|
|
r_lost = r_sent - r_received
|
|
|
|
r_lost_percent = (100 * r_lost) / r_sent
|
2016-06-14 16:39:44 +04:30
|
|
|
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)
|
2016-05-07 14:47:54 +04:30
|
|
|
else:
|
|
|
|
r_stddev = 0
|
2016-02-06 11:43:47 +03:30
|
|
|
else:
|
|
|
|
r_min = 0
|
|
|
|
r_max = 0
|
|
|
|
r_avg = 0
|
|
|
|
r_stddev = 0
|
|
|
|
|
2016-06-15 16:26:58 +04:30
|
|
|
if answers is not None:
|
2016-05-25 20:36:14 +04:30
|
|
|
flags = answers.response.flags
|
|
|
|
|
2016-06-14 16:39:44 +04:30
|
|
|
return server, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2016-04-14 20:51:13 +04:30
|
|
|
try:
|
|
|
|
signal.signal(signal.SIGTSTP, signal.SIG_IGN) # ignore CTRL+Z
|
2016-06-14 16:39:44 +04:30
|
|
|
signal.signal(signal.SIGINT, signal_handler) # catch CTRL+C
|
|
|
|
except AttributeError: # Some systems (e.g. Windows) may not support all signals
|
2016-04-14 20:51:13 +04:30
|
|
|
pass
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
if len(sys.argv) == 1:
|
|
|
|
usage()
|
|
|
|
|
2016-06-14 16:39:44 +04:30
|
|
|
# defaults
|
2016-02-06 11:43:47 +03:30
|
|
|
dnsrecord = 'A'
|
|
|
|
count = 10
|
|
|
|
waittime = 5
|
2016-02-10 09:23:06 +03:30
|
|
|
inputfilename = None
|
|
|
|
fromfile = False
|
2016-06-14 18:31:58 +04:30
|
|
|
use_tcp = False
|
2016-06-26 16:42:19 +04:30
|
|
|
use_edns = False
|
2016-02-06 11:43:47 +03:30
|
|
|
hostname = 'wikipedia.org'
|
|
|
|
|
|
|
|
try:
|
2016-06-26 16:42:19 +04:30
|
|
|
opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:Te",
|
|
|
|
["help", "file=", "count=", "type=", "wait=", "tcp", "edns"])
|
2016-02-06 11:43:47 +03:30
|
|
|
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"):
|
2016-02-10 09:23:06 +03:30
|
|
|
inputfilename = a
|
|
|
|
fromfile = True
|
2016-02-06 11:43:47 +03:30
|
|
|
elif o in ("-w", "--wait"):
|
|
|
|
waittime = int(a)
|
|
|
|
elif o in ("-t", "--type"):
|
|
|
|
dnsrecord = a
|
2016-06-14 18:31:58 +04:30
|
|
|
elif o in ("-T", "--tcp"):
|
|
|
|
use_tcp = True
|
2016-06-26 16:42:19 +04:30
|
|
|
elif o in ("-e", "--edns"):
|
|
|
|
use_edns = True
|
2016-02-06 11:43:47 +03:30
|
|
|
else:
|
2016-02-10 09:23:06 +03:30
|
|
|
print("Invalid option: %s" % o)
|
2016-02-06 11:43:47 +03:30
|
|
|
usage()
|
|
|
|
|
|
|
|
try:
|
2016-02-10 09:23:06 +03:30
|
|
|
if fromfile:
|
2016-04-10 15:36:29 +04:30
|
|
|
with open(inputfilename, 'rt') as flist:
|
|
|
|
f = flist.read().splitlines()
|
2016-02-10 09:23:06 +03:30
|
|
|
else:
|
|
|
|
f = resolvers
|
2016-04-10 16:02:23 +04:30
|
|
|
if len(f) == 0:
|
|
|
|
print("No nameserver specified")
|
2016-04-14 20:51:13 +04:30
|
|
|
|
2016-05-07 14:47:54 +04:30
|
|
|
f = [name.strip() for name in f]
|
2016-04-14 20:51:13 +04:30
|
|
|
width = maxlen(f)
|
2016-04-10 15:36:29 +04:30
|
|
|
blanks = (width - 5) * ' '
|
2016-06-01 13:22:43 +04:30
|
|
|
print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) flags')
|
2016-06-14 18:46:45 +04:30
|
|
|
print((84 + width) * '─')
|
2016-02-06 11:43:47 +03:30
|
|
|
for server in f:
|
2016-05-07 15:08:15 +04:30
|
|
|
# check if we have a valid dns server address
|
2016-05-16 14:17:39 +04:30
|
|
|
if server.lstrip() == '': # deal with empty lines
|
|
|
|
continue
|
2016-05-23 18:36:47 +04:30
|
|
|
server = server.replace(' ', '')
|
2016-05-07 15:08:15 +04:30
|
|
|
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
|
2016-05-23 18:36:47 +04:30
|
|
|
except:
|
|
|
|
pass
|
2016-05-07 15:08:15 +04:30
|
|
|
else:
|
|
|
|
s = server
|
|
|
|
|
|
|
|
if not s:
|
2016-02-06 11:43:47 +03:30
|
|
|
continue
|
2016-05-14 13:22:02 +04:30
|
|
|
(s, r_avg, r_min, r_max, r_stddev, r_lost_percent, flags) = dnsping(hostname, s, dnsrecord, waittime,
|
2016-06-26 16:42:19 +04:30
|
|
|
count, use_tcp=use_tcp,
|
|
|
|
use_edns=use_edns)
|
2016-04-10 15:36:29 +04:30
|
|
|
|
2016-05-07 15:08:15 +04:30
|
|
|
s = server.ljust(width + 1)
|
2016-05-25 20:36:14 +04:30
|
|
|
text_flags = flags_to_text(flags)
|
2016-06-01 13:22:43 +04:30
|
|
|
print("%s %-8.3f %-8.3f %-8.3f %-8.3f %%%-3d %25s" % (
|
2016-05-25 20:36:14 +04:30
|
|
|
s, r_avg, r_min, r_max, r_stddev, r_lost_percent, text_flags), flush=True)
|
2016-04-10 15:36:29 +04:30
|
|
|
|
2016-02-06 11:43:47 +03:30
|
|
|
except Exception as e:
|
|
|
|
print('error: %s' % e)
|
2016-05-09 13:34:28 +04:30
|
|
|
sys.exit(1)
|
2016-02-06 11:43:47 +03:30
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|