diff -Nru dnsdiag-1.7.0/build-pkgs.sh dnsdiag-2.0.2/build-pkgs.sh --- dnsdiag-1.7.0/build-pkgs.sh 1970-01-01 00:00:00.000000000 +0000 +++ dnsdiag-2.0.2/build-pkgs.sh 2021-05-12 10:02:49.000000000 +0000 @@ -0,0 +1,74 @@ +#!/bin/sh + +set -e + +## display an error message and exit(1) +die() { + echo "[ERROR] $*" 1>&2 + exit 1 +} + +msg() { + echo "[STATUS] $*" 1>&2 +} + +checkbin() { + which "${1}" > /dev/null 2>&1 || die "${1} is not installed" +} + +## validate required tools +checkbin "virtualenv" +checkbin "python3" + +## constants +if [ "Windows_NT" = "${OS}" ]; then ## windows compatibility shims + PLATFORM='windows' +else + PLATFORM=$(uname -s | tr 'A-Z' 'a-z') +fi +ARCH=$(uname -m) +DDVER=$(grep version setup.py | awk -F\" '{print $2}') +PKG_NAME="dnsdiag-${DDVER}.${PLATFORM}-${ARCH}-bin" +PKG_PATH="pkg/${PKG_NAME}" + +msg "Starting to build package for ${PLATFORM}-${ARCH}" + +## main +msg "Initializing virtualenv" +virtualenv -q --clear .venv +if [ -f .venv/bin/activate ]; then # *nix + . .venv/bin/activate +elif [ -f .venv/Scripts/activate ]; then # windows + . .venv/Scripts/activate +fi + +msg "Installing dependencies" +pip3 install -q pyinstaller || die "Failed to install pyinstaller" +pip3 install -q -r requirements.txt || die "Failed to install dependencies" + +mkdir -p "${PKG_PATH}" || die "Cannot create dir hierarcy: ${PKG_PATH}" + +for i in dnsping.py dnstraceroute.py dnseval.py; do + msg "Building package for ${i}" + pyinstaller ${i} -y --onefile --clean \ + --log-level=ERROR \ + --distpath="${PKG_PATH}" \ + --hidden-import=dns \ + --hidden-import=requests +done + +msg "Adding extra files..." +for i in public-servers.txt public-v4.txt rootservers.txt; do + cp ${i} "${PKG_PATH}/" +done + +cd pkg +if [ "${PLATFORM}" = "windows" ]; then + msg "Creating archive: ${PKG_NAME}.zip" + powershell Compress-Archive -Force "${PKG_NAME}" "${PKG_NAME}.zip" + else + msg "Creating tarball: ${PKG_NAME}.tar.gz" + tar cf "${PKG_NAME}".tar "${PKG_NAME}" || die "Failed to build archive (tar)" + gzip -9f "${PKG_NAME}.tar" || die "Failed to build archive (gzip)" +fi +rm -fr "${PKG_NAME}" diff -Nru dnsdiag-1.7.0/debian/changelog dnsdiag-2.0.2/debian/changelog --- dnsdiag-1.7.0/debian/changelog 2021-11-23 15:49:52.000000000 +0000 +++ dnsdiag-2.0.2/debian/changelog 2022-01-19 12:52:50.000000000 +0000 @@ -1,3 +1,13 @@ +dnsdiag (2.0.2-1) unstable; urgency=medium + + * New upstream version 2.0.2 + * Bump debhelper-compat version to 13 + * Add Rules-Requires-Root: no to debian/control + * Remove patch applied upstream + * Ignore binary cache file + + -- Ana Custura Wed, 19 Jan 2022 12:52:50 +0000 + dnsdiag (1.7.0-1.1) unstable; urgency=medium * Non-maintainer upload. diff -Nru dnsdiag-1.7.0/debian/control dnsdiag-2.0.2/debian/control --- dnsdiag-1.7.0/debian/control 2021-11-23 15:49:52.000000000 +0000 +++ dnsdiag-2.0.2/debian/control 2022-01-19 12:41:47.000000000 +0000 @@ -3,11 +3,12 @@ Priority: optional Maintainer: Ana Custura Uploaders: Debian Python Modules Team -Build-Depends: debhelper-compat (= 11), dh-python, python3, python3-setuptools, dh-exec +Build-Depends: debhelper-compat (= 13), dh-python, python3, python3-setuptools, dh-exec Standards-Version: 4.4.1 Homepage: https://dnsdiag.org/ Vcs-Git: https://salsa.debian.org/python-team/modules/dnsdiag.git Vcs-Browser: https://salsa.debian.org/python-team/modules/dnsdiag +Rules-Requires-Root: no Package: dnsdiag Architecture: all diff -Nru dnsdiag-1.7.0/debian/patches/0001-Adapt-new-dnspython-v2.0.0.patch dnsdiag-2.0.2/debian/patches/0001-Adapt-new-dnspython-v2.0.0.patch --- dnsdiag-1.7.0/debian/patches/0001-Adapt-new-dnspython-v2.0.0.patch 2021-11-23 15:48:29.000000000 +0000 +++ dnsdiag-2.0.2/debian/patches/0001-Adapt-new-dnspython-v2.0.0.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -From 3d9eb79e0037837586be5e3d01ffecbe3c442065 Mon Sep 17 00:00:00 2001 -From: Babak Farrokhi -Date: Thu, 20 Aug 2020 23:47:11 +0200 -Subject: Adapt new dnspython v2.0.0 - -- Adapt new resolve() function (Fixes #67) -- Update dependency to newer dnspython ---- - dnseval.py | 4 ++-- - dnsping.py | 4 ++-- - dnstraceroute.py | 2 +- - requirements.txt | 2 +- - setup.py | 2 +- - 5 files changed, 7 insertions(+), 7 deletions(-) - -diff --git a/dnseval.py b/dnseval.py -index d3d0afb..63820c6 100755 ---- a/dnseval.py -+++ b/dnseval.py -@@ -172,8 +172,8 @@ def dnsping(host, server, dnsrecord, timeout, count, use_tcp=False, use_edns=Fal - fqdn = host - - stime = time.perf_counter() -- answers = resolver.query(fqdn, dnsrecord, tcp=use_tcp, -- raise_on_no_answer=False) # todo: response validation in future -+ answers = resolver.resolve(fqdn, dnsrecord, tcp=use_tcp, -+ raise_on_no_answer=False) # todo: response validation in future - - except (dns.resolver.NoNameservers, dns.resolver.NoAnswer): - break -diff --git a/dnsping.py b/dnsping.py -index 6845cdb..a243ab5 100755 ---- a/dnsping.py -+++ b/dnsping.py -@@ -186,8 +186,8 @@ def main(): - - 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) -+ answers = resolver.resolve(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: -diff --git a/dnstraceroute.py b/dnstraceroute.py -index 8a58e40..44a4770 100755 ---- a/dnstraceroute.py -+++ b/dnstraceroute.py -@@ -194,7 +194,7 @@ def ping(resolver, hostname, dnsrecord, ttl, src_ip, use_edns=False): - 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.resolve(hostname, dnsrecord, source=src_ip, raise_on_no_answer=False) - - except dns.resolver.NoNameservers as e: - if not quiet: -diff --git a/requirements.txt b/requirements.txt -index 7fe56ca..f1238bd 100644 ---- a/requirements.txt -+++ b/requirements.txt -@@ -1,2 +1,2 @@ --dnspython>=1.16.0 -+dnspython>=2.0.0 - cymruwhois>=1.6 -diff --git a/setup.py b/setup.py -index 39233d2..2db02ca 100644 ---- a/setup.py -+++ b/setup.py -@@ -5,7 +5,7 @@ setup( - version="1.7.0", - packages=find_packages(), - scripts=["dnseval.py", "dnsping.py", "dnstraceroute.py"], -- install_requires=['dnspython>=1.16.0', 'cymruwhois>=1.6'], -+ install_requires=['dnspython>=2.0.0', 'cymruwhois>=1.6'], - - classifiers=[ - "Topic :: System :: Networking", --- -2.20.1 - diff -Nru dnsdiag-1.7.0/debian/patches/series dnsdiag-2.0.2/debian/patches/series --- dnsdiag-1.7.0/debian/patches/series 2021-11-23 15:49:52.000000000 +0000 +++ dnsdiag-2.0.2/debian/patches/series 2022-01-19 12:33:40.000000000 +0000 @@ -1,2 +1 @@ consolescripts.diff -0001-Adapt-new-dnspython-v2.0.0.patch diff -Nru dnsdiag-1.7.0/debian/source/options dnsdiag-2.0.2/debian/source/options --- dnsdiag-1.7.0/debian/source/options 2020-02-08 17:52:07.000000000 +0000 +++ dnsdiag-2.0.2/debian/source/options 2022-01-19 12:52:50.000000000 +0000 @@ -1 +1 @@ -extend-diff-ignore = "^[^/]*[.]egg-info/" +extend-diff-ignore = "^[^/]*[.](egg-info/|cache)" diff -Nru dnsdiag-1.7.0/dnseval.py dnsdiag-2.0.2/dnseval.py --- dnsdiag-1.7.0/dnseval.py 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/dnseval.py 2021-05-12 10:02:49.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2020, Babak Farrokhi +# Copyright (c) 2016-2021, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,30 +25,26 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os - +import datetime import getopt import ipaddress -import datetime import json -import signal +import os import socket import sys -import time -import random -import string -from statistics import stdev +import dns.rcode import dns.rdatatype import dns.resolver +import util.dns + __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' -__version__ = "1.7.0" +__version__ = '2.0.2' __progname__ = os.path.basename(sys.argv[0]) -shutdown = False -resolvers = dns.resolver.get_default_resolver().nameservers +from util.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, setup_signal_handler, flags_to_text class Colors(object): @@ -63,14 +59,13 @@ 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 +usage: %s [-ehmvCTXH] [-f server-list] [-j output.json] [-c count] [-t type] [-p port] [-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) @@ -78,177 +73,57 @@ -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) + -X --tls Use TLS as transport protocol + -j --json Save results as a JSON formatted file + -H --doh Use HTTPS as transport protols (DoH) + -p --port DNS server port number (default: 53 for TCP/UDP and 853 for TLS) + -S --srcip Query source IP address + -e --edns Disable EDNS0 (default: Enabled) + -D --dnssec Enable 'DNSSEC desired' flag in requests. -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 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.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: - 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 - - 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 - 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 + setup_signal_handler() if len(sys.argv) == 1: usage() # defaults - dnsrecord = 'A' + rdatatype = 'A' + proto = PROTO_UDP + src_ip = None + dst_port = 53 # default for UDP and TCP count = 10 waittime = 2 inputfilename = None fromfile = False - save_json = False - use_tcp = False + json_output = False use_edns = True + want_dnssec = False force_miss = False verbose = False color_mode = False - hostname = 'wikipedia.org' + qname = 'wikipedia.org' try: - opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:TevCm", - ["help", "file=", "count=", "type=", "wait=", "json", "tcp", "edns", "verbose", "color", - "force-miss"]) + opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:S:TevCmXHDj:", + ["help", "file=", "count=", "type=", "wait=", "json=", "tcp", "edns", "verbose", + "color", "force-miss", "srcip=", "tls", "doh", "dnssec"]) except getopt.GetoptError as err: print(err) usage() if args and len(args) == 1: - hostname = args[0] + qname = args[0] else: usage() @@ -265,17 +140,31 @@ elif o in ("-m", "--cache-miss"): force_miss = True elif o in ("-t", "--type"): - dnsrecord = a + rdatatype = a elif o in ("-T", "--tcp"): - use_tcp = True + proto = PROTO_TCP + elif o in ("-S", "--srcip"): + src_ip = a elif o in ("-j", "--json"): - save_json = True + json_output = True + json_filename = a elif o in ("-e", "--edns"): use_edns = False + elif o in ("-D", "--dnssec"): + want_dnssec = True elif o in ("-C", "--color"): color_mode = True elif o in ("-v", "--verbose"): verbose = True + elif o in ("-X", "--tls"): + proto = PROTO_TLS + dst_port = 853 # default for DoT, unless overriden using -p + elif o in ("-H", "--doh"): + proto = PROTO_HTTPS + dst_port = 443 # default for DoH, unless overriden using -p + elif o in ("-p", "--port"): + dst_port = int(a) + else: print("Invalid option: %s" % o) usage() @@ -296,7 +185,8 @@ print(e) sys.exit(1) else: - f = resolvers + f = dns.resolver.get_default_resolver().nameservers + if len(f) == 0: print("No nameserver specified") @@ -305,8 +195,12 @@ width = maxlen(f) blanks = (width - 5) * ' ' - print('server ', blanks, ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags') - print((93 + width) * '-') + + if not json_output: + print('server ', blanks, + ' avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags response') + print((104 + width) * '-') + for server in f: # check if we have a valid dns server address if server.lstrip() == '': # deal with empty lines @@ -320,7 +214,7 @@ except OSError: print('Error: cannot resolve hostname:', server) resolver = None - except: + except Exception: pass else: resolver = server @@ -329,56 +223,61 @@ continue 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 + retval = util.dns.ping(qname, resolver, dst_port, rdatatype, waittime, count, proto, src_ip, + use_edns=use_edns, force_miss=force_miss, want_dnssec=want_dnssec) + + except SystemExit: + break except Exception as e: print('%s: %s' % (server, e)) continue resolver = server.ljust(width + 1) - text_flags = flags_to_text(flags) + text_flags = flags_to_text(retval.flags) - s_ttl = str(ttl) + s_ttl = str(retval.ttl) if s_ttl == "None": s_ttl = "N/A" - if r_lost_percent > 0: + if retval.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 save_json: - dns_data = {} - dns_data['hostname'] = hostname - dns_data['timestamp'] = str(datetime.datetime.now()) - dns_data['r_min'] = r_min - dns_data['r_avg'] = r_avg - dns_data['resolver'] = resolver - dns_data['r_max'] = r_max - dns_data['r_lost_percent'] = r_lost_percent - dns_data['s_ttl'] = s_ttl - dns_data['text_flags'] = text_flags - outer_data = {} - outer_data['hostname'] = hostname - outer_data['data'] = dns_data - with open('results.json', 'a+') as outfile: - json.dump(outer_data, outfile) - if verbose and hasattr(answers, 'response'): + + if json_output: + dns_data = { + 'hostname': qname, + 'timestamp': str(datetime.datetime.now()), + 'r_min': retval.r_min, + 'r_avg': retval.r_avg, + 'resolver': resolver.rstrip(), + 'r_max': retval.r_max, + 'r_lost_percent': retval.r_lost_percent, + 's_ttl': s_ttl, + 'text_flags': text_flags, + 'flags': retval.flags, + 'rcode': retval.rcode, + 'rcode_text': retval.rcode_text, + } + outer_data = { + 'hostname': qname, + 'data': dns_data + } + + if json_filename == '-': # stdout + print(json.dumps(outer_data, indent=2)) + else: + with open(json_filename, 'a+') as outfile: + json.dump(outer_data, outfile, indent=2) + + else: + print("%s %-8.3f %-8.3f %-8.3f %-8.3f %s%%%-3d%s %-8s %21s %-20s" % ( + resolver, retval.r_avg, retval.r_min, retval.r_max, retval.r_stddev, l_color, retval.r_lost_percent, + color.N, s_ttl, text_flags, retval.rcode_text), flush=True) + + if verbose and retval.answer and not json_output: ans_index = 1 - for answer in answers.response.answer: + for answer in retval.answer: print("Answer %d [ %s%s%s ]" % (ans_index, color.G, answer, color.N)) ans_index += 1 print("") diff -Nru dnsdiag-1.7.0/dnsping.py dnsdiag-2.0.2/dnsping.py --- dnsdiag-1.7.0/dnsping.py 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/dnsping.py 2021-05-12 10:02:49.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2020, Babak Farrokhi +# Copyright (c) 2016-2021, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,9 +25,11 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import datetime import getopt import ipaddress import os +import requests import signal import socket import sys @@ -35,25 +37,29 @@ from statistics import stdev import dns.flags -import dns.rdatatype import dns.resolver +from util.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, proto_to_text, unsupported_feature + __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' -__version__ = "1.7.0" +__version__ = '2.0.2' __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 +usage: %s [-46DeFhqTvX] [-i interval] [-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 + -p --port DNS server port number (default: 53 for TCP/UDP and 853 for TLS) + -T --tcp Use TCP as transport protocol + -X --tls Use TLS as transport protocol + -H --doh Use HTTPS as transport protols (DoH) -4 --ipv4 Use IPv4 as default network protocol -6 --ipv6 Use IPv6 as default network protocol -P --srcport Query source port number (default: 0) @@ -63,6 +69,8 @@ -i --interval Time between each request (default: 1 seconds) -t --type DNS request record type (default: A) -e --edns Disable EDNS0 (default: Enabled) + -D --dnssec Enable 'DNSSEC desired' flag in requests. Implies EDNS. + -F --flags Display response flags """ % (__progname__, __version__, __progname__)) sys.exit(0) @@ -85,32 +93,35 @@ usage() # defaults - dnsrecord = 'A' + rdatatype = 'A' count = 10 timeout = 2 interval = 1 quiet = False verbose = False - dnsserver = dns.resolver.get_default_resolver().nameservers[0] - dst_port = 53 + show_flags = False + dnsserver = None # do not try to use system resolver by default + dst_port = 53 # default for UDP and TCP src_port = 0 src_ip = None - use_tcp = False + proto = PROTO_UDP use_edns = True + want_dnssec = False af = socket.AF_INET - hostname = 'wikipedia.org' + qname = 'wikipedia.org' try: - opts, args = getopt.getopt(sys.argv[1:], "qhc:s:t:w:i:vp:P:S:T46e", + opts, args = getopt.getopt(sys.argv[1:], "qhc:s:t:w:i:vp:P:S:T46eDFXH", ["help", "count=", "server=", "quiet", "type=", "wait=", "interval=", "verbose", - "port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport=", "edns"]) + "port=", "srcip=", "tcp", "ipv4", "ipv6", "srcport=", "edns", "dnssec", "flags", + "tls", "doh"]) except getopt.GetoptError as err: # print help information and exit: print(err, file=sys.stderr) # will print something like "option -a not recognized" usage() if args and len(args) == 1: - hostname = args[0] + qname = args[0] else: usage() @@ -123,8 +134,6 @@ 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 @@ -133,15 +142,27 @@ elif o in ("-i", "--interval"): interval = int(a) elif o in ("-t", "--type"): - dnsrecord = a + rdatatype = a elif o in ("-T", "--tcp"): - use_tcp = True + proto = PROTO_TCP + elif o in ("-X", "--tls"): + proto = PROTO_TLS + dst_port = 853 # default for DoT, unless overriden using -p + elif o in ("-H", "--doh"): + proto = PROTO_HTTPS + dst_port = 443 # default for DoH, unless overriden using -p 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 ("-D", "--dnssec"): + want_dnssec = True + elif o in ("-F", "--flags"): + show_flags = True + elif o in ("-p", "--port"): + dst_port = int(a) elif o in ("-P", "--srcport"): src_port = int(a) if src_port < 1024: @@ -151,6 +172,11 @@ else: usage() + # Use system DNS server if parameter is not specified + # remember not all systems have /etc/resolv.conf (i.e. Android) + if dnsserver is None: + dnsserver = dns.resolver.get_default_resolver().nameservers[0] + # check if we have a valid dns server address try: ipaddress.ip_address(dnsserver) @@ -161,21 +187,20 @@ print('Error: cannot resolve hostname:', dnsserver, file=sys.stderr, flush=True) 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')) + query = dns.message.make_query(qname, rdatatype, dns.rdataclass.IN, + use_edns=True, want_dnssec=want_dnssec, + ednsflags=dns.flags.edns_from_text('DO'), payload=8192) + else: + query = dns.message.make_query(qname, rdatatype, dns.rdataclass.IN, + use_edns=False, want_dnssec=want_dnssec) 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, proto: %s, rdatatype: %s, flags: %s" % + (__progname__, dnsserver, dst_port, qname, proto_to_text(proto), rdatatype, + dns.flags.to_text(query.flags)), flush=True) while not shutdown: @@ -186,39 +211,60 @@ 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) + if proto is PROTO_UDP: + answers = dns.query.udp(query, dnsserver, timeout=timeout, port=dst_port, + source=src_ip, source_port=src_port, ignore_unexpected=True) + elif proto is PROTO_TCP: + answers = dns.query.tcp(query, dnsserver, timeout=timeout, port=dst_port, + source=src_ip, source_port=src_port) + elif proto is PROTO_TLS: + if hasattr(dns.query, 'tls'): + answers = dns.query.tls(query, dnsserver, timeout, dst_port, + src_ip, src_port) + else: + unsupported_feature() + + elif proto is PROTO_HTTPS: + if hasattr(dns.query, 'https'): + answers = dns.query.https(query, dnsserver, timeout, dst_port, + src_ip, src_port) + else: + unsupported_feature() + etime = time.perf_counter() 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", file=sys.stderr, flush=True) if verbose: print("error:", e, file=sys.stderr, flush=True) sys.exit(1) - except dns.resolver.NXDOMAIN as e: - if not quiet: - print("Hostname does not exist", file=sys.stderr, flush=True) - if verbose: - print("Error:", e, file=sys.stderr, flush=True) - sys.exit(1) - except dns.resolver.Timeout: + except (requests.exceptions.ConnectTimeout, dns.exception.Timeout): if not quiet: print("Request timeout", flush=True) - pass - except dns.resolver.NoAnswer: + except requests.exceptions.ReadTimeout: + if not quiet: + print("Read timeout", flush=True) + except ValueError: if not quiet: - print("No answer", flush=True) - pass + print("Invalid Response", flush=True) + continue else: - elapsed = answers.response.time * 1000 # convert to milliseconds + # convert time to milliseconds, considering that + # time property is retruned differently by query.https + if type(answers.time) is datetime.timedelta: + elapsed = answers.time.total_seconds() * 1000 + else: + elapsed = answers.time * 1000 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) + if show_flags: + flags = " [%s] %s" % (dns.flags.to_text(answers.flags), dns.rcode.to_text(answers.rcode())) + else: + flags = "" + print("%d bytes from %s: seq=%-3d time=%.3f ms%s" % ( + len(answers.to_wire()), dnsserver, i, elapsed, flags), flush=True) if verbose: - print(answers.rrset, flush=True) - print("flags:", dns.flags.to_text(answers.response.flags), flush=True) + print(answers.to_text(), flush=True) time_to_next = (stime + interval) - etime if time_to_next > 0: diff -Nru dnsdiag-1.7.0/dnstraceroute.py dnsdiag-2.0.2/dnstraceroute.py --- dnsdiag-1.7.0/dnstraceroute.py 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/dnstraceroute.py 2021-05-12 10:02:49.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (c) 2020, Babak Farrokhi +# Copyright (c) 2016-2021, Babak Farrokhi # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -29,8 +29,6 @@ import getopt import ipaddress import os -import pickle -import signal import socket import sys import time @@ -39,31 +37,18 @@ import dns.rdatatype import dns.resolver -import cymruwhois +import util.whois +from util.dns import PROTO_UDP, PROTO_TCP, setup_signal_handler # Global Variables -__author__ = 'Babak Farrokhi (babak@farrokhi.net)' -__license__ = 'BSD' -__version__ = "1.7.0" -_ttl = None quiet = False whois_cache = {} -shutdown = False # Constants +__author__ = 'Babak Farrokhi (babak@farrokhi.net)' +__license__ = 'BSD' +__version__ = '2.0.2' __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(): @@ -75,7 +60,6 @@ N = '\033[m' # native R = '\033[31m' # red G = '\033[32m' # green - O = '\033[33m' # orange B = '\033[34m' # blue def __init__(self, mode): @@ -83,72 +67,28 @@ self.N = '' self.R = '' self.G = '' - self.O = '' self.B = '' -def whois_lookup(ip): - asn = None - 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) - except Exception as e: - pass - return asn - - -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(' -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(' -t --type DNS request record type (default: A)') - print(' -C --color Print colorful output') - print(' -e --edns Disable EDNS0 (Default: Enabled)') - print(' ') - sys.exit() - + print("""%s version %s +usage: %s [-aeqhCx] [-s server] [-p port] [-c count] [-t type] [-w wait] hostname -def signal_handler(sig, frame): - global shutdown - if shutdown: # pressed twice, so exit immediately - sys.exit(0) - shutdown = True # pressed once, exit gracefully + -h --help Show this help + -q --quiet Quiet + -T --tcp Use TCP as transport protocol + -x --expert Print expert hints if available + -a --asn Turn on AS# lookups for each hop encountered + -s --server DNS server to use (default: first system resolver) + -p --port DNS server port number (default: 53) + -S --srcip Query source IP address (default: default interface address) + -c --count Maximum number of hops (default: 30) + -w --wait Maximum wait time for a reply (default: 2) + -t --type DNS request record type (default: A) + -C --color Print colorful output + -e --edns Disable EDNS0 (Default: Enabled) +""" % (__progname__, __version__, __progname__)) + sys.exit() def expert_report(trace_path, color_mode): @@ -184,63 +124,44 @@ 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): - global _ttl - +def ping(qname, server, rdtype, proto, port, ttl, timeout, src_ip, use_edns=False): 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')) + resp_time = None try: - resolver.query(hostname, dnsrecord, source=src_ip, raise_on_no_answer=False) + resp = util.dns.ping(qname, server, port, rdtype, timeout, 1, proto, src_ip, use_edns, force_miss=False, + want_dnssec=False, socket_ttl=ttl) - 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 + if resp.answer: + reached = True + resp_time = resp.r_max - return reached + return reached, resp_time def main(): global quiet + shutdown = False - 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 + setup_signal_handler() if len(sys.argv) == 1: usage() - dnsrecord = 'A' + rdatatype = 'A' count = 30 timeout = 2 - dnsserver = dns.resolver.get_default_resolver().nameservers[0] + dnsserver = None dest_port = 53 src_ip = None hops = 0 + proto = PROTO_UDP as_lookup = False expert_mode = False should_resolve = True @@ -249,16 +170,16 @@ args = None try: - opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:S:t:w:p:nexC", + opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:S:t:w:p:nexCT", ["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert", - "color", "srcip="]) + "color", "srcip=", "tcp"]) 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] + qname = args[0] else: usage() @@ -278,13 +199,15 @@ elif o in ("-w", "--wait"): timeout = int(a) elif o in ("-t", "--type"): - dnsrecord = a + rdatatype = 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 ("-T", "--tcp"): + proto = PROTO_TCP elif o in ("-a", "--asn"): as_lookup = True elif o in ("-e", "--edns"): @@ -294,6 +217,11 @@ color = Colors(color_mode) + # Use system DNS server if parameter is not specified + # remember not all systems have /etc/resolv.conf (i.e. Android) + if dnsserver is None: + dnsserver = dns.resolver.get_default_resolver().nameservers[0] + # check if we have a valid dns server address try: ipaddress.ip_address(dnsserver) @@ -304,13 +232,6 @@ 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 - icmp = socket.getprotobyname('icmp') ttl = 1 @@ -318,12 +239,10 @@ trace_path = [] if not quiet: - print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, hostname, dnsrecord), + print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, qname, rdatatype), 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 @@ -343,32 +262,34 @@ 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) + thr = pool.submit(ping, qname, dnsserver, rdatatype, proto, dest_port, ttl, timeout, src_ip=src_ip, + use_edns=use_edns) try: # expect ICMP response packet, curr_addr = icmp_socket.recvfrom(512) if len(packet) > 51: icmp_type = packet[20] - udp_port = packet[50] << 8 | packet[51] - if icmp_type == 11 and udp_port == dest_port: + l4_dst_port = packet[50] << 8 | packet[51] + if icmp_type == 11 and l4_dst_port == dest_port: curr_addr = curr_addr[0] else: curr_addr = None except socket.error: pass + except SystemExit: + shutdown = True + break finally: etime = time.perf_counter() icmp_socket.close() - reached = thr.result() + reached, resp_time = 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() - - elapsed = abs(etime - stime) * 1000 # convert to milliseconds + elapsed = resp_time + else: + elapsed = abs(etime - stime) * 1000 # convert to milliseconds if should_resolve: try: @@ -378,15 +299,16 @@ curr_name = curr_addr except SystemExit: pass - except: + except Exception: print("unxpected error: ", sys.exc_info()[0]) else: curr_name = curr_addr + global whois_cache if curr_addr: as_name = "" if as_lookup: - asn = whois_lookup(curr_addr) + asn, whois_cache = util.whois.asn_lookup(curr_addr, whois_cache) as_name = '' try: if asn and asn.asn != "NA": @@ -394,7 +316,6 @@ except AttributeError: if shutdown: sys.exit(0) - pass c = color.N # default if curr_addr != '*': @@ -406,7 +327,7 @@ c = color.B if curr_addr == dnsserver: c = color.G - except: + except Exception: pass print("%d\t%s (%s%s%s) %s%.3f ms" % (ttl, curr_name, c, curr_addr, color.N, as_name, elapsed), flush=True) @@ -426,7 +347,7 @@ if __name__ == '__main__': try: - whois_cache = load_whois_cache(WHOIS_CACHE) + whois_cache = util.whois.restore() main() finally: - save_whois_cache(WHOIS_CACHE, whois_cache) + util.whois.save(whois_cache) diff -Nru dnsdiag-1.7.0/Dockerfile dnsdiag-2.0.2/Dockerfile --- dnsdiag-1.7.0/Dockerfile 1970-01-01 00:00:00.000000000 +0000 +++ dnsdiag-2.0.2/Dockerfile 2021-05-12 10:02:49.000000000 +0000 @@ -0,0 +1,8 @@ +FROM python:3.8.2 + +WORKDIR /dnsdiag + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + diff -Nru dnsdiag-1.7.0/.gitignore dnsdiag-2.0.2/.gitignore --- dnsdiag-1.7.0/.gitignore 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/.gitignore 2021-05-12 10:02:49.000000000 +0000 @@ -5,9 +5,11 @@ __pycache__/ *.py[cod] *$py.class +*.json .idea/ .vscode/ whois.cache +pkg/ # C extensions *.so diff -Nru dnsdiag-1.7.0/public-servers.txt dnsdiag-2.0.2/public-servers.txt --- dnsdiag-1.7.0/public-servers.txt 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/public-servers.txt 2021-05-12 10:02:49.000000000 +0000 @@ -30,6 +30,7 @@ #freenom world 80.80.80.80 80.80.81.81 + #Google 8.8.4.4 8.8.8.8 @@ -40,3 +41,11 @@ 9.9.9.9 2620:fe::fe 149.112.112.112 + +#Verisign +64.6.64.6 +64.6.65.6 + +#Comodo +8.26.56.26 +8.20.247.20 diff -Nru dnsdiag-1.7.0/public-v4.txt dnsdiag-2.0.2/public-v4.txt --- dnsdiag-1.7.0/public-v4.txt 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/public-v4.txt 2021-05-12 10:02:49.000000000 +0000 @@ -12,7 +12,6 @@ #DYN DNS 216.146.35.35 -216.146.36.36 #Level3 209.244.0.3 diff -Nru dnsdiag-1.7.0/README.md dnsdiag-2.0.2/README.md --- dnsdiag-1.7.0/README.md 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/README.md 2021-05-12 10:02:49.000000000 +0000 @@ -1,9 +1,9 @@ -[![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)]() [![Docker Pulls](https://img.shields.io/docker/pulls/farrokhi/dnsdiag)](https://hub.docker.com/r/farrokhi/dnsdiag) [![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 Measurement, Troubleshooting and Security Auditing Toolset +=============================================================== -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 +Ever been wondering if your ISP is [hijacking your DNS traffic](https://medium.com/decentralize-today/is-your-isp-hijacking-your-dns-traffic-f3eb7ccb0ee7)? 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 @@ -19,24 +19,19 @@ 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 +server for your network. While it is highly recommended using 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 -# installation +There are several ways that you can use this toolset. However, using the source code is always recommended. -There are several ways that you can use this toolset. However using the sourcecode is always recommended. +## Source Code -## From Source Code - -1. You can checkout this git repo and its submodules +1. Check out the git repository and install dependencies: ``` git clone https://github.com/farrokhi/dnsdiag.git @@ -50,26 +45,50 @@ pip3 install dnsdiag ``` -## From Binary +## Binary Package + +From time to time, binary packages will be released for Windows, Mac OS X and Linux. You can grab the latest release from [releases page](https://github.com/farrokhi/dnsdiag/releases). + +## Docker -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). +If you don't want to install dnsdiags on your local machine, you may use the docker image and run programs in a container. For example: + +``` +docker run -it --rm farrokhi/dnsdiag ./dnsping.py +``` # dnsping -dnsping pings a DNS resolver by sending an arbitrary DNS query for given number -of times: +dnsping pings a DNS resolver by sending an arbitrary DNS query for given number of times. +A complete explanation of supported command line flags is shown by using `--help`. Here are a few useful flags: + +- Using `--tcp`, `--tls` and `--doh` to select transport protocol. Default is UDP. +- Using `--flags` to display response flags for each response +- Using `--dnssec` to request DNSSEC if available + +In addition to UDP, you can ping using TCP, DoT (DNS over TLS) and DoH (DNS over HTTPS) using `--tcp`, `--tls` and `--doh` respectively. + ``` -% ./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 +% ./dnsping.py -c 5 --dnssec --flags --tls -t AAAA -s 9.9.9.9 ripe.net +dnsping.py DNS: 9.9.9.9:853, hostname: ripe.net, proto: TLS, rdatatype: AAAA, flags: RD +233 bytes from 9.9.9.9: seq=1 time=186.202 ms [QR RD RA AD] +233 bytes from 9.9.9.9: seq=2 time=191.233 ms [QR RD RA AD] +233 bytes from 9.9.9.9: seq=3 time=105.455 ms [QR RD RA AD] +233 bytes from 9.9.9.9: seq=4 time=111.053 ms [QR RD RA AD] +233 bytes from 9.9.9.9: seq=5 time=110.329 ms [QR RD RA AD] + +--- 9.9.9.9 dnsping statistics --- +5 requests transmitted, 5 responses received, 0% lost +min=105.455 ms, avg=140.854 ms, max=191.233 ms, stddev=43.782 ms ``` -This script calculates minimum, maximum and average response time as well as -jitter (stddev) + +It also displays statistics such as minimum, maximum and average response time as well as +jitter (stddev) and lost packets. + +There are several interesting use cases for dnsping, including: + +- Comparing response times using different transport protocols (e.g. UDP vs DoH) +- Measuring how reliable your DNS server is, by measuring Jitter and packet loss +- Measuring responses times when DNSSEC is enabled using `--dnssec` # dnstraceroute dnstraceroute is a traceroute utility to figure out the path that your DNS @@ -77,41 +96,64 @@ it to your actual network traceroute and make sure your DNS traffic is not routed to any unwanted path. +In addition to UDP, it also supports TCP as transport protocol, using `--tcp` flag. + ``` -% ./dnstraceroute.py --expert -C -t A -s 8.8.4.4 facebook.com +% ./dnstraceroute.py --expert --asn -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 +5 dns.google (8.8.4.4) [AS15169 GOOGLE, US] 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). +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-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 -- -- +DNS servers at once. + +You can use `dnseval` to compare response times using different transport +protocols such as UDP (default), TCP, DoT and DoH using `--tcp`, `--tls` and +`--doh` respectively. + +``` +% ./dnseval.py --dnssec -t AAAA -f public-servers.txt -c10 ripe.net +server avg(ms) min(ms) max(ms) stddev(ms) lost(%) ttl flags response +---------------------------------------------------------------------------------------------------------------------------- +1.0.0.1 36.906 7.612 152.866 50.672 %0 300 QR -- -- RD RA AD -- NOERROR +1.1.1.1 7.752 7.512 8.132 0.183 %0 298 QR -- -- RD RA AD -- NOERROR +2606:4700:4700::1001 7.661 7.169 8.102 0.240 %0 297 QR -- -- RD RA AD -- NOERROR +2606:4700:4700::1111 7.802 7.000 8.128 0.312 %0 296 QR -- -- RD RA AD -- NOERROR +195.46.39.39 14.723 7.024 78.239 22.362 %0 300 QR -- -- RD RA -- -- NOERROR +195.46.39.40 7.524 6.972 10.897 1.191 %0 300 QR -- -- RD RA -- -- NOERROR +208.67.220.220 70.519 6.694 180.229 66.516 %0 300 QR -- -- RD RA AD -- NOERROR +208.67.222.222 37.868 6.663 107.601 41.178 %0 300 QR -- -- RD RA AD -- NOERROR +2620:0:ccc::2 31.471 6.768 178.647 56.546 %0 299 QR -- -- RD RA AD -- NOERROR +2620:0:ccd::2 20.651 6.699 145.029 43.702 %0 300 QR -- -- RD RA AD -- NOERROR +216.146.35.35 19.338 6.713 131.198 39.306 %0 300 QR -- -- RD RA AD -- NOERROR +216.146.36.36 107.741 73.421 266.969 58.003 %0 299 QR -- -- RD RA AD -- NOERROR +209.244.0.3 14.717 7.015 80.329 23.058 %0 300 QR -- -- RD RA -- -- NOERROR +209.244.0.4 7.184 7.003 8.197 0.361 %0 300 QR -- -- RD RA -- -- NOERROR +4.2.2.1 7.040 6.994 7.171 0.052 %0 299 QR -- -- RD RA -- -- NOERROR +4.2.2.2 14.358 6.968 79.964 23.052 %0 300 QR -- -- RD RA -- -- NOERROR +4.2.2.3 7.083 6.945 7.265 0.091 %0 299 QR -- -- RD RA -- -- NOERROR +4.2.2.4 7.103 6.990 7.238 0.086 %0 299 QR -- -- RD RA -- -- NOERROR +4.2.2.5 7.100 7.025 7.267 0.074 %0 299 QR -- -- RD RA -- -- NOERROR +80.80.80.80 149.924 53.310 247.395 97.311 %0 299 QR -- -- RD RA AD -- NOERROR +80.80.81.81 144.262 53.360 252.564 97.759 %0 298 QR -- -- RD RA AD -- NOERROR +8.8.4.4 9.196 7.160 10.974 1.484 %0 299 QR -- -- RD RA AD -- NOERROR +8.8.8.8 7.847 7.056 9.866 0.836 %0 299 QR -- -- RD RA AD -- NOERROR +2001:4860:4860::8844 31.819 7.194 155.761 50.671 %0 299 QR -- -- RD RA AD -- NOERROR +2001:4860:4860::8888 7.773 7.200 9.814 0.777 %0 298 QR -- -- RD RA AD -- NOERROR +9.9.9.9 21.894 6.670 81.434 30.299 %0 300 QR -- -- RD RA AD -- NOERROR +2620:fe::fe 21.177 6.723 80.046 30.062 %0 300 QR -- -- RD RA AD -- NOERROR ``` ### Author @@ -126,5 +168,3 @@ ### 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) diff -Nru dnsdiag-1.7.0/requirements.txt dnsdiag-2.0.2/requirements.txt --- dnsdiag-1.7.0/requirements.txt 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/requirements.txt 2021-05-12 10:02:49.000000000 +0000 @@ -1,2 +1,4 @@ dnspython>=1.16.0 cymruwhois>=1.6 +requests>=2.21.0 +requests-toolbelt>=0.9.1 diff -Nru dnsdiag-1.7.0/setup.py dnsdiag-2.0.2/setup.py --- dnsdiag-1.7.0/setup.py 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/setup.py 2021-05-12 10:02:49.000000000 +0000 @@ -2,21 +2,22 @@ setup( name="dnsdiag", - version="1.7.0", + version="2.0.2", packages=find_packages(), scripts=["dnseval.py", "dnsping.py", "dnstraceroute.py"], - install_requires=['dnspython>=1.16.0', 'cymruwhois>=1.6'], + install_requires=['dnspython>=1.16.0', 'cymruwhois>=1.6', 'requests>=2.21.0', 'requests-toolbelt>=0.9.1'], 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 :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: Name Service (DNS)", "Development Status :: 5 - Production/Stable", @@ -25,7 +26,7 @@ author="Babak Farrokhi", author_email="babak@farrokhi.net", - description="DNS Diagnostics and measurement tools (ping, traceroute)", + description="DNS Measurement, Troubleshooting and Security Auditing Toolset (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 diff -Nru dnsdiag-1.7.0/TODO.md dnsdiag-2.0.2/TODO.md --- dnsdiag-1.7.0/TODO.md 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/TODO.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -# TODO - -- dnsfingerprint.py tool to fingerprint DNS servers diff -Nru dnsdiag-1.7.0/tox.ini dnsdiag-2.0.2/tox.ini --- dnsdiag-1.7.0/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ dnsdiag-2.0.2/tox.ini 2021-05-12 10:02:49.000000000 +0000 @@ -0,0 +1,5 @@ +[pycodestyle] +ignore = E501 + +[flake8] +ignore = E501 diff -Nru dnsdiag-1.7.0/.travis.yml dnsdiag-2.0.2/.travis.yml --- dnsdiag-1.7.0/.travis.yml 2020-01-18 10:02:08.000000000 +0000 +++ dnsdiag-2.0.2/.travis.yml 2021-05-12 10:02:49.000000000 +0000 @@ -1,7 +1,21 @@ language: python sudo: false -install: "pip install -r requirements.txt" -script: nosetests dnstraceroute.py +branches: + only: + - master +install: + - pip install -r requirements.txt + - pip install flake8 +scripts: + - nosetests dnstraceroute.py + - flake8 *.py util/*.py --count --show-source --statistics + - ./dnsping.py -c 1 --flags -t AAAA -s 9.9.9.9 ripe.net + - ./dnsping.py -c 1 --flags --tcp -t AAAA -s 9.9.9.9 ripe.net + - ./dnsping.py -c 1 --flags --doh -t AAAA -s 9.9.9.9 ripe.net || true + - ./dnstraceroute.py + - echo 1.1.1.1 > ci-test.txt + - ./dnseval.py -c 2 --dnssec ci-test.txt + - ./dnseval.py -c 2 --dnssec -H ci-test.txt matrix: fast_finish: true include: @@ -9,9 +23,6 @@ - python: "3.5" - python: "3.6" - python: "3.7" - dist: xenial - sudo: true - - python: "3.8-dev" - dist: xenial - sudo: true + - python: "3.8" + - python: "3.9" - python: "pypy3" diff -Nru dnsdiag-1.7.0/util/dns.py dnsdiag-2.0.2/util/dns.py --- dnsdiag-1.7.0/util/dns.py 1970-01-01 00:00:00.000000000 +0000 +++ dnsdiag-2.0.2/util/dns.py 2021-05-12 10:02:49.000000000 +0000 @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2016-2021, 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 datetime +import random +import signal +import socket +import sys +from statistics import stdev + +import dns.flags +import dns.message +import dns.query +import dns.rcode +import dns.rdataclass +import requests.exceptions +import string + +shutdown = False + +# Transport protocols +PROTO_UDP = 0 +PROTO_TCP = 1 +PROTO_TLS = 2 +PROTO_HTTPS = 3 + +_TTL = None + + +class PingResponse: + def __init__(self): + self.r_avg = 0 + self.r_min = 0 + self.r_max = 0 + self.r_stddev = 0 + self.r_lost_percent = 0 + self.flags = 0 + self.ttl = None + self.answer = None + self.rcode = 0 + self.rcode_text = '' + + +def proto_to_text(proto): + _proto_name = { + PROTO_UDP: 'UDP', + PROTO_TCP: 'TCP', + PROTO_TLS: 'TLS', + PROTO_HTTPS: 'HTTPS', + } + return _proto_name[proto] + + +class CustomSocket(socket.socket): + def __init__(self, *args, **kwargs): + super(CustomSocket, self).__init__(*args, **kwargs) + if _TTL: + self.setsockopt(socket.SOL_IP, socket.IP_TTL, _TTL) + + +def ping(qname, server, dst_port, rdtype, timeout, count, proto, src_ip, use_edns=False, force_miss=False, + want_dnssec=False, socket_ttl=None): + retval = PingResponse() + retval.rcode_text = "No Response" + + response_times = [] + i = 0 + + if socket_ttl: + global _TTL + _TTL = socket_ttl + dns.query.socket_factory = CustomSocket + + for i in range(count): + + if shutdown: # user pressed CTRL+C + raise SystemExit + + if force_miss: + fqdn = "_dnsdiag_%s_.%s" % (random_string(), qname) + else: + fqdn = qname + + if use_edns: + query = dns.message.make_query(fqdn, rdtype, dns.rdataclass.IN, use_edns, want_dnssec, + ednsflags=dns.flags.edns_from_text('DO'), payload=8192) + else: + query = dns.message.make_query(fqdn, rdtype, dns.rdataclass.IN, use_edns, want_dnssec) + + try: + if proto is PROTO_UDP: + response = dns.query.udp(query, server, timeout=timeout, port=dst_port, source=src_ip, + ignore_unexpected=True) + elif proto is PROTO_TCP: + response = dns.query.tcp(query, server, timeout=timeout, port=dst_port, source=src_ip) + elif proto is PROTO_TLS: + if hasattr(dns.query, 'tls'): + response = dns.query.tls(query, server, timeout, dst_port, src_ip) + else: + unsupported_feature() + elif proto is PROTO_HTTPS: + if hasattr(dns.query, 'https'): + response = dns.query.https(query, server, timeout, dst_port, src_ip) + else: + unsupported_feature() + + except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError): + raise ConnectionError('Connection failed') + except ValueError: + retval.rcode_text = "Invalid Response" + break + except dns.exception.Timeout: + break + except OSError: + if socket_ttl: # this is an acceptable error while doing traceroute + break + except Exception as e: + print(e) + break + else: + # convert time to milliseconds, considering that + # time property is retruned differently by query.https + if type(response.time) is datetime.timedelta: + elapsed = response.time.total_seconds() * 1000 + else: + elapsed = response.time * 1000 + response_times.append(elapsed) + if response: + retval.flags = response.flags + retval.answer = response.answer + retval.rcode = response.rcode() + retval.rcode_text = dns.rcode.to_text(response.rcode()) + if len(response.answer) > 0: + retval.ttl = response.answer[0].ttl + + r_sent = i + 1 + r_received = len(response_times) + retval.r_lost_count = r_sent - r_received + retval.r_lost_percent = (100 * retval.r_lost_count) / r_sent + if response_times: + retval.r_min = min(response_times) + retval.r_max = max(response_times) + retval.r_avg = sum(response_times) / r_received + if len(response_times) > 1: + retval.r_stddev = stdev(response_times) + else: + retval.r_stddev = 0 + else: + retval.r_min = 0 + retval.r_max = 0 + retval.r_avg = 0 + retval.r_stddev = 0 + + return retval + + +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 signal_handler(sig, frame): + global shutdown + if shutdown: # pressed twice, so exit immediately + sys.exit(0) + shutdown = True # pressed once, exit gracefully + + +def unsupported_feature(): + print("Error: You have an older version of Python interpreter.") + print(" Some features such as DoT and DoH are not available. You should upgrade") + print(" the Python interpreter to at least 3.6 and reinstall dependencies.") + sys.exit(127) + + +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 = sorted(_by_value.items(), reverse=True) + + _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 setup_signal_handler(): + 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 diff -Nru dnsdiag-1.7.0/util/whois.py dnsdiag-2.0.2/util/whois.py --- dnsdiag-1.7.0/util/whois.py 1970-01-01 00:00:00.000000000 +0000 +++ dnsdiag-2.0.2/util/whois.py 2021-05-12 10:02:49.000000000 +0000 @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2016-2021, 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 pickle +import time + +import cymruwhois + +WHOIS_CACHE_FILE = 'whois.cache' + + +def asn_lookup(ip, whois_cache) -> (str, dict): + """ + Look up an ASN given teh IP address from cache. If not in cache, lookup from a whois server and update the cache + :param ip: IP Address (str) + :param whois_cache: whois data cache (dict) + :return: AS Number (str), Updated whois cache (dict) + """ + asn = None + try: + currenttime = time.time() + 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) + except Exception: + pass + return asn, whois_cache + + +def restore() -> dict: + """ + Loads whois cache data from a file + :return: whois data dict + """ + try: + pkl_file = open(WHOIS_CACHE_FILE, 'rb') + try: + whois = pickle.load(pkl_file) + pkl_file.close() + except Exception: + whois = {} + except IOError: + whois = {} + return whois + + +def save(whois_data: dict): + """ + Saves whois cache data to a file + :param whois_data: whois data (dict) + :return: None + """ + pkl_file = open(WHOIS_CACHE_FILE, 'wb') + pickle.dump(whois_data, pkl_file) + pkl_file.close()