dnsdiag/dnstraceroute.py

411 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
#
# Copyright (c) 2016, Babak Farrokhi
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import concurrent.futures
import getopt
2016-05-02 12:31:07 +04:30
import ipaddress
import os
2016-05-04 12:09:13 +04:30
import pickle
import signal
import socket
2016-04-08 12:32:50 +04:30
import sys
import time
import dns.query
import dns.rdatatype
import dns.resolver
2016-05-04 12:09:13 +04:30
from cymruwhois import cymruwhois
__author__ = 'Babak Farrokhi (babak@farrokhi.net)'
__license__ = 'BSD'
2017-04-30 21:12:08 +04:30
__version__ = "1.6.2"
_ttl = None
2016-06-14 16:48:35 +04:30
quiet = False
2016-06-14 16:48:35 +04:30
class CustomSocket(socket.socket):
def __init__(self, *args, **kwargs):
2016-06-14 16:48:35 +04:30
super(CustomSocket, self).__init__(*args, **kwargs)
def sendto(self, *args, **kwargs):
global _ttl
if _ttl:
self.setsockopt(socket.SOL_IP, socket.IP_TTL, _ttl)
2016-06-14 16:48:35 +04:30
super(CustomSocket, self).sendto(*args, **kwargs)
2016-05-04 12:09:13 +04:30
2016-05-03 19:53:37 +04:30
def test_import():
2016-05-04 12:09:13 +04:30
# passing this test means imports were successful
2016-05-03 19:53:37 +04:30
pass
# Constants
__progname__ = os.path.basename(sys.argv[0])
WHOIS_CACHE = 'whois.cache'
2016-05-02 12:31:07 +04:30
class Colors(object):
N = '\033[m' # native
R = '\033[31m' # red
G = '\033[32m' # green
O = '\033[33m' # orange
B = '\033[34m' # blue
def __init__(self, mode):
if not mode:
self.N = ''
self.R = ''
self.G = ''
self.O = ''
self.B = ''
# Globarl Variables
2016-05-07 14:47:15 +04:30
shutdown = False
2016-05-04 12:09:13 +04:30
def whoisrecord(ip):
try:
currenttime = time.perf_counter()
2016-05-04 12:09:13 +04:30
ts = currenttime
if ip in whois:
2016-06-14 16:48:35 +04:30
asn, ts = whois[ip]
2016-05-04 12:09:13 +04:30
else:
ts = 0
2016-06-14 16:48:35 +04:30
if (currenttime - ts) > 36000:
2016-05-04 12:09:13 +04:30
c = cymruwhois.Client()
2016-06-14 16:48:35 +04:30
asn = c.lookup(ip)
whois[ip] = (asn, currenttime)
return asn
2016-05-04 12:09:13 +04:30
except Exception as e:
return e
2016-05-04 12:09:13 +04:30
try:
pkl_file = open(WHOIS_CACHE, 'rb')
try:
2016-05-04 12:09:13 +04:30
whois = pickle.load(pkl_file)
except EOFError:
whois = {}
2016-05-04 12:09:13 +04:30
except IOError:
whois = {}
def usage():
print('%s version %s\n' % (__progname__, __version__))
print('usage: %s [-aeqhCx] [-s server] [-p port] [-c count] [-t type] [-w wait] hostname' % __progname__)
print(' -h --help Show this help')
print(' -q --quiet Quiet')
print(' -x --expert Print expert hints if available')
print(' -a --asn Turn on AS# lookups for each hop encountered')
print(' -s --server DNS server to use (default: first system resolver)')
print(' -p --port DNS server port number (default: 53)')
print(' -c --count Maximum number of hops (default: 30)')
print(' -w --wait Maximum wait time for a reply (default: 2)')
print(' -t --type DNS request record type (default: A)')
2016-05-02 12:31:07 +04:30
print(' -C --color Print colorful output')
print(' -e --edns Disable EDNS0 (Default: Enabled)')
print(' ')
2016-05-09 13:33:32 +04:30
sys.exit()
def signal_handler(sig, frame):
2016-05-07 14:47:15 +04:30
global shutdown
if shutdown: # pressed twice, so exit immediately
2016-05-09 13:33:32 +04:30
sys.exit(0)
shutdown = True # pressed once, exit gracefully
2016-05-02 12:31:07 +04:30
def expert_report(trace_path, color_mode):
color = Colors(color_mode)
2016-05-02 12:46:51 +04:30
print("\n%s=== Expert Hints ===%s" % (color.B, color.N))
2016-05-02 12:31:07 +04:30
if len(trace_path) == 0:
print(" [*] empty trace - should not happen")
return
private_network_radius = 4 # number of hops we assume we are still inside our local network
2016-05-02 12:31:07 +04:30
prev_hop = None
if len(trace_path) > 1:
prev_hop = trace_path[-2]
if len(trace_path) < 2:
print(
" %s[*]%s path too short (possible DNS hijacking, unless it is a local DNS resolver)" % (color.R, color.N))
return
if prev_hop == '*' and len(trace_path) > private_network_radius:
2016-05-02 12:39:20 +04:30
print(" %s[*]%s public DNS server is next to an invisible hop (probably a firewall)" % (color.R, color.N))
2016-05-02 12:31:07 +04:30
return
if prev_hop and len(trace_path) > private_network_radius and ipaddress.ip_address(prev_hop).is_private:
2016-05-02 12:31:07 +04:30
print(" %s[*]%s public DNS server is next to a private IP address (possible hijacking)" % (color.R, color.N))
return
if prev_hop and len(trace_path) > private_network_radius and ipaddress.ip_address(prev_hop).is_reserved:
2016-05-02 12:31:07 +04:30
print(" %s[*]%s public DNS server is next to a reserved IP address (possible hijacking)" % (color.R, color.N))
return
2016-06-14 16:48:35 +04:30
# no expert info available
2016-05-02 12:46:51 +04:30
print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N))
2016-05-02 12:31:07 +04:30
def ping(resolver, hostname, dnsrecord, ttl, use_edns=False):
global _ttl
reached = False
2016-06-14 16:48:35 +04:30
dns.query.socket_factory = CustomSocket
_ttl = ttl
2016-06-26 17:15:54 +04:30
if use_edns:
resolver.use_edns(edns=0, payload=8192, ednsflags=dns.flags.edns_from_text('DO'))
try:
resolver.query(hostname, dnsrecord, raise_on_no_answer=False)
except dns.resolver.NoNameservers as e:
if not quiet:
print("no or bad response:", e)
2016-05-09 13:33:32 +04:30
sys.exit(1)
except dns.resolver.NXDOMAIN as e:
if not quiet:
print("Invalid hostname:", e)
2016-05-09 13:33:32 +04:30
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)
2016-05-09 13:33:32 +04:30
sys.exit(1)
else:
reached = True
return reached
def main():
global quiet
2016-04-14 13:33:45 +04:30
try:
signal.signal(signal.SIGTSTP, signal.SIG_IGN) # ignore CTRL+Z
signal.signal(signal.SIGINT, signal_handler) # custom CTRL+C handler
2016-05-02 12:31:07 +04:30
except AttributeError: # not all signals are supported on all platforms
2016-04-14 13:33:45 +04:30
pass
if len(sys.argv) == 1:
usage()
dnsrecord = 'A'
count = 30
timeout = 2
dnsserver = dns.resolver.get_default_resolver().nameservers[0]
dest_port = 53
hops = 0
as_lookup = False
2016-05-02 12:31:07 +04:30
expert_mode = False
should_resolve = True
use_edns = True
2016-05-02 12:31:07 +04:30
color_mode = False
try:
opts, args = getopt.getopt(sys.argv[1:], "aqhc:s:t:w:p:nexC",
2016-05-02 12:31:07 +04:30
["help", "count=", "server=", "quiet", "type=", "wait=", "asn", "port", "expert",
"color"])
except getopt.GetoptError as err:
# print help information and exit:
print(err) # will print something like "option -a not recognized"
usage()
if args and len(args) == 1:
hostname = args[0]
else:
usage()
for o, a in opts:
if o in ("-h", "--help"):
usage()
elif o in ("-c", "--count"):
count = int(a)
elif o in ("-x", "--expert"):
2016-05-02 12:31:07 +04:30
expert_mode = True
elif o in ("-s", "--server"):
dnsserver = a
elif o in ("-q", "--quiet"):
quiet = True
elif o in ("-w", "--wait"):
timeout = int(a)
elif o in ("-t", "--type"):
dnsrecord = a
elif o in ("-p", "--port"):
dest_port = int(a)
2016-05-02 12:31:07 +04:30
elif o in ("-C", "--color"):
color_mode = True
elif o in ("-n"):
should_resolve = False
elif o in ("-a", "--asn"):
2016-05-04 12:09:13 +04:30
as_lookup = True
2016-06-26 17:15:54 +04:30
elif o in ("-e", "--edns"):
use_edns = False
else:
usage()
2016-05-02 12:31:07 +04:30
color = Colors(color_mode)
# check if we have a valid dns server address
try:
ipaddress.ip_address(dnsserver)
except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name
try:
dnsserver = socket.getaddrinfo(dnsserver, port=None, family=socket.AF_INET)[1][4][0]
except OSError:
print('Error: cannot resolve hostname:', dnsserver)
sys.exit(1)
resolver = dns.resolver.Resolver()
resolver.nameservers = [dnsserver]
resolver.timeout = timeout
resolver.lifetime = timeout
resolver.retry_servfail = 0
icmp = socket.getprotobyname('icmp')
ttl = 1
reached = False
2016-05-02 12:31:07 +04:30
trace_path = []
if not quiet:
print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, hostname, dnsrecord),
flush=True)
while True:
2016-05-07 14:47:15 +04:30
if shutdown:
break
# some platforms permit opening a DGRAM socket for ICMP without root permission
# if not availble, we will fall back to RAW which explicitly requires root permission
try:
icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
except OSError:
try:
icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, icmp)
except OSError:
print("Error: Unable to create ICMP socket with unprivileged user. Please run as root.")
2016-05-09 13:33:32 +04:30
sys.exit(1)
icmp_socket.bind(("", dest_port))
icmp_socket.settimeout(timeout)
curr_addr = None
curr_host = None
2016-04-07 15:23:38 +04:30
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: # dispatch dns lookup to another thread
stime = time.perf_counter()
2016-06-26 17:15:54 +04:30
thr = pool.submit(ping, resolver, hostname, dnsrecord, ttl, use_edns=use_edns)
try: # expect ICMP response
_, curr_addr = icmp_socket.recvfrom(512)
curr_addr = curr_addr[0]
except socket.error:
etime = time.perf_counter()
pass
finally:
etime = time.perf_counter()
icmp_socket.close()
reached = thr.result()
if reached:
curr_addr = dnsserver
stime = time.perf_counter() # need to recalculate elapsed time for last hop without waiting for an icmp error reply
2016-06-26 17:15:54 +04:30
ping(resolver, hostname, dnsrecord, ttl, use_edns=use_edns)
etime = time.perf_counter()
elapsed = abs(etime - stime) * 1000 # convert to milliseconds
if should_resolve:
try:
if curr_addr:
curr_name = socket.gethostbyaddr(curr_addr)[0]
except socket.error:
curr_name = curr_addr
except SystemExit:
pass
except:
print("unxpected error: ", sys.exc_info()[0])
else:
curr_name = curr_addr
if curr_addr:
as_name = ""
2016-05-04 12:09:13 +04:30
if as_lookup:
2016-06-14 16:48:35 +04:30
asn = whoisrecord(curr_addr)
as_name = ''
try:
2016-06-14 16:48:35 +04:30
if asn and asn.asn != "NA":
as_name = "[%s %s] " % (asn.asn, asn.owner)
except AttributeError:
2016-05-07 14:47:15 +04:30
if shutdown:
2016-05-09 13:33:32 +04:30
sys.exit(0)
pass
2016-05-02 12:31:07 +04:30
c = color.N # default
if curr_addr != '*':
IP = ipaddress.ip_address(curr_addr)
if IP.is_private:
c = color.R
if IP.is_reserved:
c = color.B
if curr_addr == dnsserver:
c = color.G
print("%d\t%s (%s%s%s) %s%.3f ms" % (ttl, curr_name, c, curr_addr, color.N, as_name, elapsed), flush=True)
2016-05-02 12:31:07 +04:30
trace_path.append(curr_addr)
else:
2016-05-02 12:31:07 +04:30
print("%d\t *" % ttl, flush=True)
trace_path.append("*")
ttl += 1
hops += 1
if (hops >= count) or (curr_addr == dnsserver) or reached:
break
2016-05-07 14:47:15 +04:30
if expert_mode and not shutdown:
2016-05-02 12:31:07 +04:30
expert_report(trace_path, color_mode)
2016-04-08 12:32:50 +04:30
if __name__ == '__main__':
try:
main()
finally:
2016-05-04 12:09:13 +04:30
pkl_file = open(WHOIS_CACHE, 'wb')
pickle.dump(whois, pkl_file)