pax_global_header00006660000000000000000000000064146343070030014512gustar00rootroot0000000000000052 comment=e57eeb7a68f52de1247d64e0a492477f81e48738 dnsdiag-2.5.0/000077500000000000000000000000001463430700300131275ustar00rootroot00000000000000dnsdiag-2.5.0/.github/000077500000000000000000000000001463430700300144675ustar00rootroot00000000000000dnsdiag-2.5.0/.github/workflows/000077500000000000000000000000001463430700300165245ustar00rootroot00000000000000dnsdiag-2.5.0/.github/workflows/packages.yml000066400000000000000000000020321463430700300210220ustar00rootroot00000000000000name: Package Build on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pyinstaller if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=60 --max-line-length=127 --statistics - name: Build package run: | sh build-pkgs.sh dnsdiag-2.5.0/.gitignore000066400000000000000000000015061463430700300151210ustar00rootroot00000000000000# virtualenv .venv/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class *.json .idea/ .vscode/ whois.cache pkg/ # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints results.json dnsdiag-2.5.0/.gitmodules000066400000000000000000000000001463430700300152720ustar00rootroot00000000000000dnsdiag-2.5.0/Dockerfile000066400000000000000000000002041463430700300151150ustar00rootroot00000000000000FROM python:3.12-alpine WORKDIR /dnsdiag ENV PATH "$PATH:/dnsdiag" COPY . . RUN pip install --no-cache-dir -r requirements.txt dnsdiag-2.5.0/LICENSE000066400000000000000000000024211463430700300141330ustar00rootroot00000000000000Copyright (c) 2024, 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. dnsdiag-2.5.0/MANIFEST.in000066400000000000000000000001231463430700300146610ustar00rootroot00000000000000include LICENSE README.md TODO.md public-servers.txt public-v4.txt rootservers.txt dnsdiag-2.5.0/README.md000066400000000000000000000242671463430700300144210ustar00rootroot00000000000000[![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)]() [![Downloads](https://static.pepy.tech/personalized-badge/dnsdiag?period=total&units=international_system&left_color=grey&right_color=blue&left_text=PyPi%20Downloads)](https://pepy.tech/project/dnsdiag) [![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 Measurement, Troubleshooting and Security Auditing Toolset =============================================================== 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 working as you expect. You can measure the response time of any given DNS server for arbitrary requests using `dnsping`. Just like traditional ping utility, it gives you similar functionality for DNS requests. You can also trace the path your DNS request takes to destination to make sure it is not being redirected or hijacked. This can be done by comparing different DNS queries being sent to the same DNS server using `dnstraceroute` and observe 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 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. # Installation There are several ways that you can use this toolset. However, using the source code is always recommended. ## Source Code 1. Check out the git repository and install dependencies: ``` git clone https://github.com/farrokhi/dnsdiag.git cd dnsdiag pip3 install -r requirements.txt ``` 2. You can alternatively install the package using pip: ``` pip3 install dnsdiag ``` ## 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 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 --network host -it --rm farrokhi/dnsdiag dnsping.py ``` # dnsping 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 (including EDNS flags) for each response - Using `--dnssec` to request DNSSEC if available - Using `--ede` to display Extended DNS Error messages ([RFC 8914](https://www.rfc-editor.org/rfc/rfc8914)) - Using `--nsid` to display Name Server Identifier (NSID) if available ([RFC 5001](https://www.rfc-editor.org/rfc/rfc5001)) In addition to UDP, you can ping using TCP, DoT (DNS over TLS) and DoH (DNS over HTTPS) using `--tcp`, `--tls` and `--doh` respectively. ```shell ./dnsping.py -c 5 --dnssec --flags --tls --ede -t AAAA -s 8.8.8.8 brokendnssec.net ``` ``` dnsping.py DNS: 8.8.8.8:853, hostname: brokendnssec.net, proto: TLS, class: IN, type: AAAA, flags: [RD] 75 bytes from 8.8.8.8: seq=1 time=113.631 ms [QR RD RA DO] SERVFAIL [EDE 10: For brokendnssec.net/soa] 75 bytes from 8.8.8.8: seq=2 time=115.479 ms [QR RD RA DO] SERVFAIL [EDE 10: For brokendnssec.net/soa] 75 bytes from 8.8.8.8: seq=3 time=90.882 ms [QR RD RA DO] SERVFAIL [EDE 10: For brokendnssec.net/soa] 75 bytes from 8.8.8.8: seq=4 time=91.256 ms [QR RD RA DO] SERVFAIL [EDE 10: For brokendnssec.net/soa] 75 bytes from 8.8.8.8: seq=5 time=94.072 ms [QR RD RA DO] SERVFAIL [EDE 10: For brokendnssec.net/soa] --- 8.8.8.8 dnsping statistics --- 5 requests transmitted, 5 responses received, 0% lost min=90.882 ms, avg=101.064 ms, max=115.479 ms, stddev=12.394 ms ``` 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 request is passing through to get to its destination. You may want to compare 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. ```shell ./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 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). # 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. 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. ```shell ./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 Babak Farrokhi - github: [github.com/farrokhi](https://github.com/farrokhi/) - website: [farrokhi.net](https://farrokhi.net/) - mastodon: [@farrokhi@unix.family](https://unix.family/@farrokhi) - twitter: [@farrokhi](https://twitter.com/farrokhi) ### License dnsdiag is released under a 2 clause BSD license. dnsdiag-2.5.0/build-pkgs.sh000077500000000000000000000040621463430700300155310ustar00rootroot00000000000000#!/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 "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 util/shared.py | awk -F\' '{print $2}') PKG_NAME="dnsdiag-${DDVER}.${PLATFORM}-${ARCH}-bin" PKG_PATH="pkg/${PKG_NAME}" msg "Starting to build dnsdiag package version ${DDVER} for ${PLATFORM}-${ARCH}" ## main if [ $# -gt 0 ]; then if [ $1 == "--venv" ]; then msg "Initializing virtualenv" checkbin "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 fi fi msg "Installing dependencies" pip3 install --upgrade pip 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=httpx 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}" dnsdiag-2.5.0/dnseval.py000077500000000000000000000231151463430700300151420ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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 getopt import ipaddress import json import os import socket import sys import dns.rcode import dns.rdatatype import dns.resolver import util.dns __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __progname__ = os.path.basename(sys.argv[0]) from util.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, setup_signal_handler, flags_to_text from util.shared import __version__, Colors def usage(): print("""%s version %s 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) -m --cache-miss Force cache miss measurement by prepending a random hostname -w --wait Maximum wait time for a reply (default: 2) -t --type DNS request record type (default: A) -T --tcp Use TCP instead of UDP -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 Enable EDNS0 -D --dnssec Enable 'DNSSEC desired' (DO flag) in requests -C --color Print colorful output -v --verbose Print actual dns response """ % (__progname__, __version__, __progname__)) sys.exit() def maxlen(names): sn = sorted(names, key=len) return len(sn[-1]) def main(): setup_signal_handler() if len(sys.argv) == 1: usage() # defaults rdatatype = 'A' proto = PROTO_UDP src_ip = None dst_port = 53 # default for UDP and TCP count = 10 waittime = 2 inputfilename = None fromfile = False json_output = False use_edns = False want_dnssec = False force_miss = False verbose = False color_mode = False qname = 'wikipedia.org' try: opts, args = getopt.getopt(sys.argv[1:], "hf:c:t:w:S:TevCmXHDj:p:", ["help", "file=", "count=", "type=", "wait=", "json=", "tcp", "edns", "verbose", "color", "cache-miss", "srcip=", "tls", "doh", "dnssec", "port="]) except getopt.GetoptError as err: print(err) usage() if args and len(args) == 1: qname = 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"): inputfilename = a fromfile = True elif o in ("-w", "--wait"): waittime = int(a) elif o in ("-m", "--cache-miss"): force_miss = True elif o in ("-t", "--type"): rdatatype = a elif o in ("-T", "--tcp"): proto = PROTO_TCP elif o in ("-S", "--srcip"): src_ip = a elif o in ("-j", "--json"): json_output = True json_filename = a elif o in ("-e", "--edns"): use_edns = True elif o in ("-D", "--dnssec"): want_dnssec = True use_edns = True # implied 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 overridden using -p elif o in ("-H", "--doh"): proto = PROTO_HTTPS dst_port = 443 # default for DoH, unless overridden using -p elif o in ("-p", "--port"): dst_port = int(a) else: print("Invalid option: %s" % o) usage() # validate RR type if not util.dns.valid_rdatatype(rdatatype): print('Error: Invalid record type "%s" ' % rdatatype) sys.exit(1) color = Colors(color_mode) try: if fromfile: if inputfilename == '-': # read from stdin with sys.stdin as flist: f = flist.read().splitlines() else: try: with open(inputfilename, 'rt') as flist: f = flist.read().splitlines() except Exception as e: print(e) sys.exit(1) else: f = dns.resolver.get_default_resolver().nameservers if len(f) == 0: print("Error: No nameserver specified") f = [name.strip() for name in f] # remove annoying blanks f = [x for x in f if not x.startswith('#') and len(x)] # remove comments and empty entries width = maxlen(f) blanks = (width - 5) * ' ' 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 continue server = server.replace(' ', '') try: ipaddress.ip_address(server) except ValueError: # so it is not a valid IPv4 or IPv6 address, so try to resolve host name try: resolver = socket.getaddrinfo(server, port=None)[1][4][0] except OSError: print('Error: cannot resolve hostname:', server) resolver = None except Exception: pass else: resolver = server if not resolver: continue try: 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(retval.flags) s_ttl = str(retval.ttl) if s_ttl == "None": s_ttl = "N/A" if retval.r_lost_percent > 0: l_color = color.O else: l_color = color.N 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 retval.answer: print("Answer %d [ %s%s%s ]" % (ans_index, color.G, answer, color.N)) ans_index += 1 print("") except Exception as e: print('%s: %s' % (server, e)) sys.exit(1) if __name__ == '__main__': main() dnsdiag-2.5.0/dnsping.py000077500000000000000000000366441463430700300151630ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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 getopt import ipaddress import os import signal import socket import sys import time import httpx from statistics import stdev import dns.flags import dns.resolver import util.dns from util.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, proto_to_text, unsupported_feature, random_string from util.shared import __version__ __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __progname__ = os.path.basename(sys.argv[0]) shutdown = False def usage(): print("""%s version %s usage: %s [-46aDeEFhLmqnrvTxXH] [-i interval] [-w wait] [-p dst_port] [-P src_port] [-S src_ip] %s [-c count] [-t qtype] [-C class] [-s server] 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 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) -S --srcip Query source IP address (default: default interface address) -c --count Number of requests to send (default: 10, 0 for infinity) -r --norecurse Enforce non-recursive query by clearing the RD (recursion desired) bit in the query -m --cache-miss Force cache miss measurement by prepending a random hostname -w --wait Maximum wait time for a reply (default: 2 seconds) -i --interval Time between each request (default: 1 seconds) -t --type DNS request record type (default: A) -L --ttl Display response TTL (if present) -C --class DNS request record class (default: IN) -a --answer Display first matching answer in rdata, if applicable -e --edns Enable EDNS0 and set -E --ede Display EDE messages when available -n --nsid Enable NSID bit to find out identification of the resolver. Implies EDNS. -D --dnssec Enable 'DNSSEC desired' flag in requests. Implies EDNS. -F --flags Display response flags -x --expert Display extra information. Implies --ttl --flags --ede. """ % (__progname__, __version__, __progname__, ' ' * len(__progname__))) sys.exit(0) 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 def signal_handler(sig, frame): global shutdown if shutdown: # pressed twice, so exit immediately sys.exit(0) shutdown = True # pressed once, exit gracefully def print_stderr(s, should_die): print(s, file=sys.stderr, flush=True) if should_die: sys.exit(1) def validate_server_address(dnsserver, address_family): """checks if we have a valid dns server address and resolve if it is a hostname""" 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=address_family)[1][4][0] except OSError: print_stderr('Error: cannot resolve hostname: %s' % dnsserver, True) return dnsserver def main(): setup_signal_handler() if len(sys.argv) == 1: usage() # defaults rdatatype = 'A' rdata_class = dns.rdataclass.from_text('IN') count = 10 timeout = 2 interval = 1 quiet = False verbose = False show_flags = False show_ede = 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 proto = PROTO_UDP use_edns = False want_nsid = False want_dnssec = False show_ttl = False force_miss = False show_answer = False request_flags = dns.flags.from_text('RD') af = socket.AF_INET qname = 'wikipedia.org' try: opts, args = getopt.getopt(sys.argv[1:], "qhc:s:t:w:i:vp:P:S:T46meDFXHrnEC:Lxa", ["help", "count=", "server=", "quiet", "type=", "wait=", "interval=", "verbose", "port=", "srcip=", "tcp", "ipv4", "ipv6", "cache-miss", "srcport=", "edns", "dnssec", "flags", "norecurse", "tls", "doh", "nsid", "ede", "class=", "ttl", "expert", "answer"]) except getopt.GetoptError as err: # print help information and exit: print_stderr(err, False) # will print something like "option -a not recognized" usage() if args and len(args) == 1: qname = args[0] else: usage() for o, a in opts: if o in ("-h", "--help"): usage() elif o in ("-c", "--count"): if a.isdigit(): count = abs(int(a)) else: print_stderr("Invalid count of requests: %s" % a, True) elif o in ("-v", "--verbose"): verbose = True elif o in ("-s", "--server"): dnsserver = a elif o in ("-q", "--quiet"): quiet = True verbose = False elif o in ("-w", "--wait"): timeout = int(a) elif o in ("-a", "--answer"): show_answer = True elif o in ("-x", "--expert"): show_flags = True show_ede = True show_ttl = True elif o in ("-m", "--cache-miss"): force_miss = True elif o in ("-i", "--interval"): interval = float(a) elif o in ("-L", "--ttl"): show_ttl = True elif o in ("-t", "--type"): rdatatype = a elif o in ("-C", "--class"): try: rdata_class = dns.rdataclass.from_text(a) except dns.rdataclass.UnknownRdataclass: print_stderr("Invalid RR class: %s" % a, True) elif o in ("-T", "--tcp"): proto = PROTO_TCP elif o in ("-X", "--tls"): proto = PROTO_TLS dst_port = 853 # default for DoT, unless overridden using -p elif o in ("-H", "--doh"): proto = PROTO_HTTPS dst_port = 443 # default for DoH, unless overridden 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 = True elif o in ("-n", "--nsid"): use_edns = True # required want_nsid = True elif o in ("-r", "--norecurse"): request_flags = dns.flags.from_text('') elif o in ("-D", "--dnssec"): use_edns = True # required want_dnssec = True elif o in ("-F", "--flags"): show_flags = True elif o in ("-E", "--ede"): show_ede = True elif o in ("-p", "--port"): dst_port = int(a) elif o in ("-P", "--srcport"): src_port = int(a) if src_port < 1024 and not quiet: print_stderr("WARNING: Source ports below 1024 are only available to superuser", False) elif o in ("-S", "--srcip"): src_ip = a 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] dnsserver = validate_server_address(dnsserver, af) response_time = [] i = 0 # validate RR type if not util.dns.valid_rdatatype(rdatatype): print_stderr('Error: Invalid record type: %s ' % rdatatype, True) print("%s DNS: %s:%d, hostname: %s, proto: %s, class: %s, type: %s, flags: [%s]" % (__progname__, dnsserver, dst_port, qname, proto_to_text(proto), dns.rdataclass.to_text(rdata_class), rdatatype, dns.flags.to_text(request_flags)), flush=True) while not shutdown: if 0 < count <= i: break else: i += 1 if force_miss: fqdn = "_dnsdiag_%s_.%s" % (random_string(8, 8), qname) else: fqdn = qname if use_edns: edns_options = [] if want_nsid: edns_options.append(dns.edns.GenericOption(dns.edns.NSID, '')) query = dns.message.make_query(fqdn, rdatatype, rdata_class, flags=request_flags, use_edns=True, want_dnssec=want_dnssec, payload=1232, options=edns_options) else: query = dns.message.make_query(fqdn, rdatatype, rdata_class, flags=request_flags, use_edns=False, want_dnssec=False) try: stime = time.perf_counter() 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_stderr("No response to DNS request", False) if verbose: print_stderr("error: %s" % e, False) sys.exit(1) except (httpx.ConnectTimeout, dns.exception.Timeout): if not quiet: print("Request timeout", flush=True) except httpx.ReadTimeout: if not quiet: print("Read timeout", flush=True) except PermissionError: if not quiet: print_stderr("Permission denied", True) sys.exit(1) except OSError as e: if not quiet: print_stderr("%s" % e, True) sys.exit(1) except ValueError: if not quiet: print_stderr("Invalid Response", False) continue else: # convert time to milliseconds, considering that # time property is returned 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: extras = "" extras += " %s" % dns.rcode.to_text(answers.rcode()) # add response code if show_ttl: if answers.answer: ans_ttl = str(answers.answer[0].ttl) extras += " [TTL=%-4s]" % ans_ttl if show_flags: ans_flags = dns.flags.to_text(answers.flags) edns_flags = dns.flags.edns_to_text(answers.ednsflags) extras += " [%s]" % " ".join([ans_flags, edns_flags]).rstrip(' ') # show both regular + edns flags if want_nsid: for ans_opt in answers.options: if ans_opt.otype == dns.edns.OptionType.NSID: nsid_val = ans_opt.nsid extras += " [ID: %s]" % nsid_val.decode("utf-8") if show_ede: for ans_opt in answers.options: # EDE response is optional, but print if there is one if ans_opt.otype == dns.edns.EDE: extras += " [EDE %d: %s]" % (ans_opt.code, ans_opt.text) if show_answer: # The answer should be displayed at the rightmost for ans in answers.answer: if ans.rdtype == dns.rdatatype.from_text(rdatatype): # is this the answer to our question? # extras += " [%s]" % ans[0] extras += " [RDATA: %s]" % ans[0] break print("%-3d bytes from %s: seq=%-3d time=%-7.3f ms %s" % ( len(answers.to_wire()), dnsserver, i, elapsed, extras), flush=True) if verbose: print(answers.to_text(), flush=True) time_to_next = (stime + interval) - etime if time_to_next > 0: time.sleep(time_to_next) r_sent = i r_received = len(response_time) r_lost = r_sent - r_received r_lost_percent = (100 * r_lost) / r_sent if response_time: r_min = min(response_time) r_max = max(response_time) r_avg = sum(response_time) / r_received if len(response_time) > 1: r_stddev = stdev(response_time) else: r_stddev = 0 else: r_min = 0 r_max = 0 r_avg = 0 r_stddev = 0 print('\n--- %s dnsping statistics ---' % dnsserver, flush=True) print('%d requests transmitted, %d responses received, %.0f%% lost' % (r_sent, r_received, r_lost_percent), flush=True) print('min=%.3f ms, avg=%.3f ms, max=%.3f ms, stddev=%.3f ms' % (r_min, r_avg, r_max, r_stddev), flush=True) if __name__ == '__main__': main() dnsdiag-2.5.0/dnstraceroute.py000077500000000000000000000264221463430700300163740ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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 import ipaddress import os import socket import sys import time import dns.query import dns.rdatatype import dns.resolver import util.whois from util.dns import PROTO_UDP, PROTO_TCP, setup_signal_handler from util.shared import __version__, Colors # Global Variables quiet = False whois_cache = {} # Constants __author__ = 'Babak Farrokhi (babak@farrokhi.net)' __license__ = 'BSD' __progname__ = os.path.basename(sys.argv[0]) def test_import(): # passing this test means imports were successful pass def usage(): print("""%s version %s usage: %s [-aeqhCx] [-s server] [-p port] [-c count] [-t type] [-w wait] hostname -h --help Show this help -q --quiet Quiet mode: No extra information, only traceroute output. -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 Enable EDNS0 (Default: Disabled) """ % (__progname__, __version__, __progname__)) sys.exit() def expert_report(trace_path, color_mode): color = Colors(color_mode) print("\n%s=== Expert Hints ===%s" % (color.B, color.N)) 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 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: print(" %s[*]%s public DNS server is next to an invisible hop (probably a firewall)" % (color.R, color.N)) return if prev_hop and len(trace_path) > private_network_radius and ipaddress.ip_address(prev_hop).is_private: 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: print(" %s[*]%s public DNS server is next to a reserved IP address (possible hijacking)" % (color.R, color.N)) return # no expert info available print(" %s[*]%s No expert hint available for this trace" % (color.G, color.N)) def ping(qname, server, rdtype, proto, port, ttl, timeout, src_ip, use_edns): reached = False resp_time = None try: 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 SystemExit: pass except Exception as e: print("unxpected error: ", e) sys.exit(1) else: if resp.answer: reached = True resp_time = resp.r_max return reached, resp_time def main(): global quiet shutdown = False setup_signal_handler() if len(sys.argv) == 1: usage() rdatatype = 'A' count = 30 timeout = 2 dnsserver = None dest_port = 53 src_ip = None hops = 0 proto = PROTO_UDP as_lookup = False expert_mode = False should_resolve = True use_edns = False color_mode = False args = None try: 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=", "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: qname = 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"): expert_mode = True elif o in ("-s", "--server"): dnsserver = a elif o in ("-q", "--quiet"): quiet = True elif o in ("-S", "--srcip"): src_ip = a elif o in ("-w", "--wait"): timeout = int(a) elif o in ("-t", "--type"): 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"): use_edns = True else: usage() color = Colors(color_mode) # validate RR type if not util.dns.valid_rdatatype(rdatatype): print('Error: Invalid record type "%s" ' % rdatatype) sys.exit(1) # 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) 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) icmp = socket.getprotobyname('icmp') ttl = 1 reached = False trace_path = [] if not quiet: print("%s DNS: %s:%d, hostname: %s, rdatatype: %s" % (__progname__, dnsserver, dest_port, qname, rdatatype), flush=True) while True: # 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.") sys.exit(1) icmp_socket.bind(("", dest_port)) icmp_socket.settimeout(timeout) curr_addr = None with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: # dispatch dns lookup to another thread stime = time.perf_counter() 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] 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, resp_time = thr.result() if reached: curr_addr = dnsserver elapsed = resp_time else: elapsed = abs(etime - stime) * 1000 # convert to milliseconds curr_name = curr_addr 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 Exception: print("unxpected error: ", sys.exc_info()[0]) global whois_cache if curr_addr: as_name = "" if as_lookup: asn, whois_cache = util.whois.asn_lookup(curr_addr, whois_cache) as_name = '' try: if asn and asn.asn != "NA": as_name = "[AS%s %s] " % (asn.asn, asn.owner) except AttributeError: if shutdown: sys.exit(0) c = color.N # default if curr_addr != '*': try: IP = ipaddress.ip_address(curr_addr) if IP.is_private: c = color.R if IP.is_reserved: c = color.B if curr_addr == dnsserver: c = color.G except 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) trace_path.append(curr_addr) else: print("%d\t *" % ttl, flush=True) trace_path.append("*") ttl += 1 hops += 1 if (hops >= count) or (curr_addr == dnsserver) or reached: break if expert_mode and not shutdown: expert_report(trace_path, color_mode) if __name__ == '__main__': try: whois_cache = util.whois.restore() main() finally: util.whois.save(whois_cache) dnsdiag-2.5.0/public-servers.txt000066400000000000000000000016311463430700300166360ustar00rootroot00000000000000## Cloudflare 1.0.0.1 1.1.1.1 2606:4700:4700::1001 2606:4700:4700::1111 ## SafeDNS 195.46.39.39 195.46.39.40 ## OpenDNS 208.67.220.220 208.67.222.222 2620:0:ccc::2 2620:0:ccd::2 ## DYN DNS 216.146.35.35 216.146.36.36 ## Level3 209.244.0.3 209.244.0.4 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 2001:4860:4860::8844 2001:4860:4860::8888 ## Quad9 9.9.9.9 149.112.112.112 2620:fe::fe ## Verisign 64.6.64.6 64.6.65.6 2620:74:1b::1:1 2620:74:1c::2:2 ## Comodo 8.26.56.26 8.20.247.20 ## DNS0.eu 193.110.81.0 185.253.5.0 2a0f:fc80:: 2a0f:fc81:: ## Hurricane Electric 74.82.42.42 2001:470:20::2 ## Yandex.DNS 77.88.8.88 77.88.8.2 2a02:6b8::feed:bad 2a02:6b8:0:1::feed:bad ## DNS4all 194.0.5.3 2001:678:8::3 ## AdGuard DNS 94.140.14.14 94.140.15.15 2a10:50c0::ad1:ff 2a10:50c0::ad2:ff ## Control D 76.76.2.11 76.76.10.11 2606:1a40::11 2606:1a40:1::11 dnsdiag-2.5.0/public-v4.txt000066400000000000000000000012221463430700300154720ustar00rootroot00000000000000## 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 ## Quad9 9.9.9.9 149.112.112.112 ## Verisign 64.6.64.6 64.6.65.6 ## Comodo 8.26.56.26 8.20.247.20 ## DNS0.eu 193.110.81.0 185.253.5.0 ## Hurricane Electric 74.82.42.42 ## Yandex.DNS 77.88.8.88 77.88.8.2 ## DNS4all 194.0.5.3 ## AdGuard DNS 94.140.14.14 94.140.15.15 ## Control D 76.76.2.11 76.76.10.11 ## Wikimedia DNS (Formerly Wikidough) 185.71.138.138 dnsdiag-2.5.0/requirements.txt000066400000000000000000000001161463430700300164110ustar00rootroot00000000000000dnspython>=2.6.1 cymruwhois>=1.6 httpx>=0.27.0 cryptography>=42.0.7 h2>=4.1.0 dnsdiag-2.5.0/rootservers.txt000066400000000000000000000003671463430700300162730ustar00rootroot00000000000000a.root-servers.net b.root-servers.net c.root-servers.net d.root-servers.net e.root-servers.net f.root-servers.net g.root-servers.net h.root-servers.net i.root-servers.net j.root-servers.net k.root-servers.net l.root-servers.net m.root-servers.net dnsdiag-2.5.0/setup.py000066400000000000000000000033661463430700300146510ustar00rootroot00000000000000from setuptools import setup, find_packages from util.shared import __version__ setup( name="dnsdiag", version=__version__, packages=find_packages(), scripts=["dnseval.py", "dnsping.py", "dnstraceroute.py"], install_requires=['dnspython>=2.6.1', 'cymruwhois>=1.6', 'httpx>=0.27.0', 'cryptography>=42.0.7', 'h2>=4.1.0'], classifiers=[ "Topic :: System :: Networking", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet :: Name Service (DNS)", "Development Status :: 5 - Production/Stable", "Operating System :: OS Independent", ], author="Babak Farrokhi", author_email="babak@farrokhi.net", 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 you can measure your DNS response quality from delay and loss perspective as well as tracing the path your DNS query takes to get to DNS server. """, license="BSD", keywords="dns traceroute ping performance", url="https://dnsdiag.org/", entry_points={ 'console_scripts': [ 'dnsping = dnsping:main', 'dnstraceroute = dnstraceroute:main', 'dnseval = dnseval:main', ] } ) dnsdiag-2.5.0/tox.ini000066400000000000000000000001001463430700300144310ustar00rootroot00000000000000[pycodestyle] ignore = E501, E741 [flake8] ignore = E501, E741 dnsdiag-2.5.0/util/000077500000000000000000000000001463430700300141045ustar00rootroot00000000000000dnsdiag-2.5.0/util/__init__.py000066400000000000000000000000001463430700300162030ustar00rootroot00000000000000dnsdiag-2.5.0/util/dns.py000066400000000000000000000175331463430700300152530ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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 httpx import dns.flags import dns.message import dns.query import dns.rcode import dns.rdataclass 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, payload=1232) else: query = dns.message.make_query(fqdn, rdtype, dns.rdataclass.IN, use_edns=False, want_dnssec=False) 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 (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.ConnectError): raise ConnectionError('Connection failed') except ValueError: retval.rcode_text = "Invalid Response" break except dns.exception.Timeout: break except OSError as e: if socket_ttl: # this is an acceptable error while doing traceroute break print("error: %s"% e.strerror, file=sys.stderr, flush=True) raise OSError(e) except Exception as e: print("error: %s"% e, file=sys.stderr, flush=True) 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 unsupported version of Python interpreter dnspython library.") print(" Some features such as DoT and DoH are not available. You should upgrade") print(" the Python interpreter to at least 3.7 and reinstall dependencies.") sys.exit(127) def valid_rdatatype(rtype): # validate RR type try: _ = dns.rdatatype.from_text(rtype) except dns.rdatatype.UnknownRdatatype: return False return 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 = 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 dnsdiag-2.5.0/util/shared.py000066400000000000000000000032761463430700300157340ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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. __version__ = '2.5.0' 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 = '' dnsdiag-2.5.0/util/whois.py000066400000000000000000000052721463430700300156150ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright (c) 2016-2024, 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()