pax_global_header00006660000000000000000000000064147070116340014515gustar00rootroot0000000000000052 comment=ab347757078f4bd7ac6f2a8cc8a5aca8253f04cb dnsdiag-2.6.0/000077500000000000000000000000001470701163400131335ustar00rootroot00000000000000dnsdiag-2.6.0/.github/000077500000000000000000000000001470701163400144735ustar00rootroot00000000000000dnsdiag-2.6.0/.github/workflows/000077500000000000000000000000001470701163400165305ustar00rootroot00000000000000dnsdiag-2.6.0/.github/workflows/packages.yml000066400000000000000000000020471470701163400210340ustar00rootroot00000000000000name: Package Build on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.10"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 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.6.0/.gitignore000066400000000000000000000015061470701163400151250ustar00rootroot00000000000000# 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.6.0/.gitmodules000066400000000000000000000000001470701163400152760ustar00rootroot00000000000000dnsdiag-2.6.0/Dockerfile000066400000000000000000000002041470701163400151210ustar00rootroot00000000000000FROM python:3.12-alpine WORKDIR /dnsdiag ENV PATH "$PATH:/dnsdiag" COPY . . RUN pip install --no-cache-dir -r requirements.txt dnsdiag-2.6.0/LICENSE000066400000000000000000000024211470701163400141370ustar00rootroot00000000000000Copyright (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.6.0/MANIFEST.in000066400000000000000000000001231470701163400146650ustar00rootroot00000000000000include LICENSE README.md TODO.md public-servers.txt public-v4.txt rootservers.txt dnsdiag-2.6.0/README.md000066400000000000000000000245641470701163400144250ustar00rootroot00000000000000[![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) [![Downloads](https://static.pepy.tech/badge/dnsdiag/month)](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 =============================================================== Have you ever wondered if your ISP is [intercepting your DNS traffic](https://medium.com/decentralize-today/is-your-isp-hijacking-your-dns-traffic-f3eb7ccb0ee7))? Have you noticed any unusual behavior in your DNS responses, or been redirected to the wrong address and suspected something might be off with your DNS? We offer a suite of tools to perform basic audits on your DNS requests and responses, helping you ensure your DNS is functioning as expected. With `dnsping`, you can measure the response time of any DNS server for arbitrary queries. Similar to the regular ping utility, dnsping offers comparable functionality for DNS requests, helping you monitor server responsiveness. You can also trace the route of your DNS request to its destination using `dnstraceroute`, verifying that it isn't being redirected or intercepted. By comparing DNS queries sent to the same server, `dnstraceroute` allows you to observe any differences in the paths taken, alerting you to possible issues. `dnseval` assesses multiple DNS resolvers to help you choose the best DNS resolver for your network. While using your own DNS resolver is recommended to avoid reliance on third-party DNS resolvers, `dnseval` can assist in selecting the optimal DNS resolver when needed. It lets you compare DNS servers based on performance (latency) and reliability (packet loss), giving you a comprehensive view for informed decision-making. # Installation There are several ways to use this toolset, though we recommend running it directly from the source code for optimal flexibility and control. ## 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 prefer not to install `dnsdiag` on your local machine, you can use the Docker image to run the tools in a containerized environment. For example: ``` docker run --network host -it --rm farrokhi/dnsdiag dnsping.py ``` # dnsping `dnsping` allows you to "ping" a DNS resolver by sending an arbitrary DNS query multiple times. For a full list of supported command-line options, use `--help`. Here are a few key flags: - Use `--tcp`, `--tls`, or `--doh` to select the transport protocol (default is UDP). - Use `--flags` to display response flags, including EDNS flags, for each response. - Use `--dnssec` to request DNSSEC validation if available. - Use `--ede` to display Extended DNS Error messages ([RFC 8914](https://www.rfc-editor.org/rfc/rfc8914)). - Use `--nsid` to display the Name Server Identifier (NSID) if available ([RFC 5001](https://www.rfc-editor.org/rfc/rfc5001)). ```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 ``` `dnsping` also provides statistics such as minimum, maximum, and average response times, along with jitter (standard deviation) and packet loss. Here are a few interesting use cases for `dnsping`: - Comparing response times across different transport protocols (e.g., UDP vs. DoH). - Evaluating the reliability of your DNS server by measuring jitter and packet loss. - Measuring response times with DNSSEC enabled using the `--dnssec` flag. # dnstraceroute `dnstraceroute` is a utility that traces the path of your DNS requests to their destination. You may want to compare this with your actual network traceroute to ensure that your DNS traffic is not being routed through any unwanted paths. In addition to UDP, `dnstraceroute` also supports TCP as a transport protocol when you use the `--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 the `--expert` flag with `dnstraceroute` will enable the display of expert hints, including warnings about potential DNS traffic hijacking. # dnseval `dnseval` is a bulk ping utility that sends arbitrary DNS queries to a specified list of DNS servers, allowing you to compare their response times simultaneously. You can use `dnseval` to evaluate response times across different transport protocols, including UDP (default), TCP, DoT (DNS over TLS), and DoH (DNS over HTTPS) by using the `--tcp`, `--tls`, and `--doh` flags, 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.6.0/build-pkgs.sh000077500000000000000000000040641470701163400155370ustar00rootroot00000000000000#!/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.6.0/dnseval.py000077500000000000000000000234321470701163400151500ustar00rootroot00000000000000#!/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 Display this help message -f, --file Specify a DNS server list file to use (default: system resolvers) -c, --count Number of requests to send (default: 10) -m, --cache-miss Force a cache miss measurement by prepending a random hostname -w, --wait Set the maximum wait time for a reply in seconds (default: 2) -t, --type Set the DNS request record type (default: A) -T, --tcp Use TCP as the transport protocol instead of UDP -X, --tls Use TLS as the transport protocol -j, --json Save the results to a specified file in JSON format -H, --doh Use HTTPS as the transport protocol (DoH) -p, --port Specify the DNS server port number (default: 53 for TCP/UDP, 853 for TLS) -S, --srcip Set the query source IP address -e, --edns Enable EDNS0 in requests -D, --dnssec Enable the 'DNSSEC desired' (DO flag) in requests -C, --color Enable colorful output -v, --verbose Print the full DNS response details """ % (__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: result = "%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) print(result.rstrip(), 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.6.0/dnsping.py000077500000000000000000000414741470701163400151640ustar00rootroot00000000000000#!/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 from util.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, PROTO_QUIC, proto_to_text, unsupported_feature, \ random_string, getDefaultPort, valid_rdatatype 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 [-46aDeEFhLmqnrvTQxXH] [-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 message -q, --quiet Suppress output -v, --verbose Print the full DNS response -s, --server Specify the DNS server to use (default: first entry from /etc/resolv.conf) -p, --port Specify the DNS server port number (default: 53 for TCP/UDP, 853 for TLS) -T, --tcp Use TCP as the transport protocol -X, --tls Use TLS as the transport protocol -H, --doh Use HTTPS as the transport protocol (DoH) -Q, --doq Use QUIC as the transport protocol (DoQ) -4, --ipv4 Use IPv4 as the network protocol -6, --ipv6 Use IPv6 as the network protocol -P, --srcport Specify the source port number for the query (default: 0) -S, --srcip Specify the source IP address for the query (default: default interface address) -c, --count Number of requests to send (default: 10, 0 for unlimited) -r, --norecurse Enforce a non-recursive query by clearing the RD (recursion desired) bit -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 interval between requests (default: 1 second) -t, --type DNS request record type (default: A) -L, --ttl Display the response TTL (if present) -C, --class DNS request record class (default: IN) -a, --answer Display the first matching answer in rdata, if applicable -e, --edns Enable EDNS0 and set its options -E, --ede Display EDE (Extended DNS Error) messages, when available -n, --nsid Enable the NSID bit to retrieve resolver identification (implies EDNS) -D, --dnssec Enable the DNSSEC desired flag (implies EDNS) -F, --flags Display response flags -x, --expert Display additional 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() dns.rdata.load_all_types() # 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 proto = PROTO_UDP dst_port = getDefaultPort(proto) use_default_dst_port = True src_port = 0 src_ip = None 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:TQ46meDFXHrnEC: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", "quic"]) 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 if use_default_dst_port: dst_port = getDefaultPort(proto) elif o in ("-X", "--tls"): proto = PROTO_TLS if use_default_dst_port: dst_port = getDefaultPort(proto) elif o in ("-H", "--doh"): proto = PROTO_HTTPS if use_default_dst_port: dst_port = getDefaultPort(proto) elif o in ("-Q", "--quic"): proto = PROTO_QUIC if use_default_dst_port: dst_port = getDefaultPort(proto) 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) use_default_dst_port = False 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 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=timeout, port=dst_port, source=src_ip, source_port=src_port) else: unsupported_feature("DNS-over-TLS") elif proto is PROTO_HTTPS: if hasattr(dns.query, 'https'): try: answers = dns.query.https(query, dnsserver, timeout=timeout, port=dst_port, source=src_ip, source_port=src_port) except httpx.ConnectError: print_stderr(f"The server did not respond to DoH on port {dst_port}", should_die=True) else: unsupported_feature("DNS-over-HTTPS (DoH)") elif proto is PROTO_QUIC: if hasattr(dns.query, 'quic'): try: answers = dns.query.quic(query, dnsserver, timeout=timeout, port=dst_port, source=src_ip, source_port=src_port) except dns.exception.Timeout: print_stderr(f"The server did not respond to DoQ on port {dst_port}", should_die=True) else: unsupported_feature("DNS-over-QUIC (DoQ)") 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.6.0/dnstraceroute.py000077500000000000000000000266571470701163400164120ustar00rootroot00000000000000#!/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 Options: -h, --help Show this help message -q, --quiet Enable quiet mode: suppress additional information, showing only traceroute output -T, --tcp Use TCP as the transport protocol -x, --expert Display expert hints, if available -a, --asn Enable AS# lookups for each encountered hop -s, --server Specify the DNS server to use (default: first system resolver) -p, --port Set the DNS server port number (default: 53) -S, --srcip Set the source IP address for the query (default: address of the default network interface) -c, --count Specify the maximum number of hops (default: 30) -w, --wait Set the maximum wait time for a reply, in seconds (default: 2) -t, --type DNS request record type (default: A) -C, --color Enable 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.6.0/public-servers.txt000066400000000000000000000016311470701163400166420ustar00rootroot00000000000000## 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.6.0/public-v4.txt000066400000000000000000000012221470701163400154760ustar00rootroot00000000000000## 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.6.0/requirements.txt000066400000000000000000000001351470701163400164160ustar00rootroot00000000000000aioquic>=1.2.0 cryptography>=42.0.5 cymruwhois>=1.6 dnspython>=2.7.0 h2>=4.1.0 httpx>=0.27.0 dnsdiag-2.6.0/rootservers.txt000066400000000000000000000003671470701163400162770ustar00rootroot00000000000000a.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.6.0/setup.py000066400000000000000000000034111470701163400146440ustar00rootroot00000000000000from 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=['aioquic>=1.2.0', 'cryptography>=42.0.5', 'cymruwhois>=1.6', 'dnspython>=2.7.0', 'h2>=4.1.0', 'httpx>=0.27.0'], classifiers=[ "Topic :: System :: Networking", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "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.6.0/tox.ini000066400000000000000000000001201470701163400144370ustar00rootroot00000000000000[pycodestyle] ignore = E501, E741 [flake8] ignore = E501, E741 exclude = .venv dnsdiag-2.6.0/util/000077500000000000000000000000001470701163400141105ustar00rootroot00000000000000dnsdiag-2.6.0/util/__init__.py000066400000000000000000000000001470701163400162070ustar00rootroot00000000000000dnsdiag-2.6.0/util/dns.py000066400000000000000000000203221470701163400152450ustar00rootroot00000000000000#!/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 PROTO_QUIC = 4 _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', PROTO_QUIC: 'QUIC', } return _proto_name[proto] def getDefaultPort(proto): _proto_port = { PROTO_UDP: 53, PROTO_TCP: 53, PROTO_TLS: 853, # RFC 7858, Secion 3.1 PROTO_HTTPS: 443, PROTO_QUIC: 853, # RFC 9250, Section 4.1.1 } return _proto_port[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(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.") if feature: print("Missing Feature: %s" % feature) 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.6.0/util/shared.py000066400000000000000000000032761470701163400157400ustar00rootroot00000000000000#!/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.6.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.6.0/util/whois.py000066400000000000000000000052721470701163400156210ustar00rootroot00000000000000#!/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()