Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package dnsdiag for openSUSE:Factory checked 
in at 2025-09-22 16:41:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/dnsdiag (Old)
 and      /work/SRC/openSUSE:Factory/.dnsdiag.new.27445 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "dnsdiag"

Mon Sep 22 16:41:04 2025 rev:9 rq:1306502 version:2.7.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/dnsdiag/dnsdiag.changes  2024-10-28 
15:24:20.233504000 +0100
+++ /work/SRC/openSUSE:Factory/.dnsdiag.new.27445/dnsdiag.changes       
2025-09-22 16:41:56.283104468 +0200
@@ -1,0 +2,24 @@
+Sun Sep 21 10:26:34 UTC 2025 - Martin Hauke <[email protected]>
+
+- Update to version 2.8.0
+  New Features
+  * DNS over HTTP/3 (DoH3) Support: Added support for RFC 9114 DNS
+    over HTTP3 protocol using -3 or --doh3 option in dnsping.
+  * Improved Error Handling: Enhanced error handling for DoH3
+    connection failures.
+  Breaking Changes
+  * Python 3.9 Support Dropped: Minimum Python version is now 3.10
+    due to dnspython 2.8.0 requirements.
+  Improvements
+  * DoQ and DoH3 Enhancements:
+    + Upgraded to dnspython 2.8.0 which provides improved DoQ (DNS
+      over QUIC) and DoH3 error handling.
+  * Display Enhancements:
+    + Fixed RTT display that was broken in previous release.
+    + Improved response time display.
+    + Better display of DNS response flags.
+    + Fixed EDE (Extended DNS Error) payload display to show empty
+      string instead of "None".
+    + Added quotes around EDE payload strings for better visibility
+
+-------------------------------------------------------------------

Old:
----
  dnsdiag-2.6.0.tar.gz

New:
----
  dnsdiag-2.7.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ dnsdiag.spec ++++++
--- /var/tmp/diff_new_pack.V1QPVP/_old  2025-09-22 16:41:56.955132704 +0200
+++ /var/tmp/diff_new_pack.V1QPVP/_new  2025-09-22 16:41:56.955132704 +0200
@@ -1,8 +1,8 @@
 #
 # spec file for package dnsdiag
 #
-# Copyright (c) 2024 SUSE LLC
-# Copyright (c) 2017-2024, Martin Hauke <[email protected]>
+# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2017-2025, Martin Hauke <[email protected]>
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,23 +19,24 @@
 
 %bcond_without test
 Name:           dnsdiag
-Version:        2.6.0
+Version:        2.7.0
 Release:        0
 Summary:        DNS request auditing toolset
 License:        BSD-3-Clause
 Group:          Development/Languages/Python
 #Git-Clone:     https://github.com/farrokhi/dnsdiag.git
 URL:            https://dnsdiag.org/
-Source:         
https://files.pythonhosted.org/packages/source/d/dnsdiag/dnsdiag-%{version}.tar.gz
+Source:         
https://github.com/farrokhi/dnsdiag/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz
 Source1:        dnseval.1
 Source2:        dnsping.1
 Source3:        dnstraceroute.1
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 BuildRequires:  python3-setuptools
+Requires:       python3-aioquic >= 1.2.0
 Requires:       python3-cryptography >= 42.0.5
 Requires:       python3-cymruwhois >= 1.6
-Requires:       python3-dnspython >= 2.6.1
+Requires:       python3-dnspython >= 2.8.0
 Requires:       python3-h2 >= 4.1.0
 Requires:       python3-httpx >= 0.27.0
 BuildArch:      noarch
@@ -65,8 +66,8 @@
 of a resolver.
 
 %prep
-%setup -q -n dnsdiag-%{version}
-sed -e '/^#!\//, 1d' -i util/*.py
+%autosetup -n dnsdiag-%{version}
+sed -e '/^#!\//, 1d' -i dnsdiag/*.py
 
 %build
 %python3_build
@@ -91,5 +92,5 @@
 %{_mandir}/man1/dnseval.1%{?ext_man}
 %{_mandir}/man1/dnstraceroute.1%{?ext_man}
 %{_mandir}/man1/dnsping.1%{?ext_man}
-%{python3_sitelib}/*
+%{python3_sitelib}/dnsdiag*
 

++++++ dnsdiag-2.6.0.tar.gz -> dnsdiag-2.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/.github/workflows/packages.yml 
new/dnsdiag-2.7.0/.github/workflows/packages.yml
--- old/dnsdiag-2.6.0/.github/workflows/packages.yml    1970-01-01 
01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/.github/workflows/packages.yml    2025-09-21 
11:55:15.000000000 +0200
@@ -0,0 +1,33 @@
+name: Package Build
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.10", "3.11", "3.12", "3.13"]
+
+    steps:
+      - uses: actions/checkout@v5
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v6
+        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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/.gitignore new/dnsdiag-2.7.0/.gitignore
--- old/dnsdiag-2.6.0/.gitignore        1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/.gitignore        2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,71 @@
+# 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/Dockerfile new/dnsdiag-2.7.0/Dockerfile
--- old/dnsdiag-2.6.0/Dockerfile        1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/Dockerfile        2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,10 @@
+FROM python:3.12-alpine
+
+WORKDIR /dnsdiag
+
+ENV PATH "$PATH:/dnsdiag"
+
+COPY . .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/LICENSE new/dnsdiag-2.7.0/LICENSE
--- old/dnsdiag-2.6.0/LICENSE   2024-10-25 16:53:59.000000000 +0200
+++ new/dnsdiag-2.7.0/LICENSE   2025-09-21 11:55:15.000000000 +0200
@@ -1,4 +1,4 @@
-Copyright (c) 2024, Babak Farrokhi
+Copyright (c) 2025, Babak Farrokhi
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/MANIFEST.in 
new/dnsdiag-2.7.0/MANIFEST.in
--- old/dnsdiag-2.6.0/MANIFEST.in       2019-11-02 13:38:19.000000000 +0100
+++ new/dnsdiag-2.7.0/MANIFEST.in       2025-09-21 11:55:15.000000000 +0200
@@ -1 +1 @@
-include LICENSE README.md TODO.md public-servers.txt public-v4.txt 
rootservers.txt
+include LICENSE README.md public-servers.txt public-v4.txt rootservers.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/PKG-INFO new/dnsdiag-2.7.0/PKG-INFO
--- old/dnsdiag-2.6.0/PKG-INFO  2024-10-25 23:57:52.778604300 +0200
+++ new/dnsdiag-2.7.0/PKG-INFO  1970-01-01 01:00:00.000000000 +0100
@@ -1,35 +0,0 @@
-Metadata-Version: 2.1
-Name: dnsdiag
-Version: 2.6.0
-Summary: DNS Measurement, Troubleshooting and Security Auditing Toolset (ping, 
traceroute)
-Home-page: https://dnsdiag.org/
-Author: Babak Farrokhi
-Author-email: [email protected]
-License: BSD
-Keywords: dns traceroute ping performance
-Classifier: Topic :: System :: Networking
-Classifier: Environment :: Console
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: Name Service (DNS)
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Operating System :: OS Independent
-License-File: LICENSE
-Requires-Dist: aioquic>=1.2.0
-Requires-Dist: cryptography>=42.0.5
-Requires-Dist: cymruwhois>=1.6
-Requires-Dist: dnspython>=2.7.0
-Requires-Dist: h2>=4.1.0
-Requires-Dist: httpx>=0.27.0
-
-
-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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/README.md new/dnsdiag-2.7.0/README.md
--- old/dnsdiag-2.6.0/README.md 2024-10-25 16:53:59.000000000 +0200
+++ new/dnsdiag-2.7.0/README.md 2025-09-21 11:55:15.000000000 +0200
@@ -66,9 +66,9 @@
 
 `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 `--tcp`, `--tls`, `--doh`, `doq` or `--http3` 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 `--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)).
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/build-pkgs.sh 
new/dnsdiag-2.7.0/build-pkgs.sh
--- old/dnsdiag-2.6.0/build-pkgs.sh     1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/build-pkgs.sh     2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,81 @@
+#!/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}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag/dns.py 
new/dnsdiag-2.7.0/dnsdiag/dns.py
--- old/dnsdiag-2.6.0/dnsdiag/dns.py    1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/dnsdiag/dns.py    2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,274 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2016-2025, 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
+PROTO_HTTP3 = 5
+
+_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',
+        PROTO_HTTP3: 'HTTP3',
+    }
+    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
+        PROTO_HTTP3: 443,
+    }
+    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.10 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag/shared.py 
new/dnsdiag-2.7.0/dnsdiag/shared.py
--- old/dnsdiag-2.6.0/dnsdiag/shared.py 1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/dnsdiag/shared.py 2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2016-2025, 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.7.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 = ''
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag/whois.py 
new/dnsdiag-2.7.0/dnsdiag/whois.py
--- old/dnsdiag-2.6.0/dnsdiag/whois.py  1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/dnsdiag/whois.py  2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2016-2025, 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()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/PKG-INFO 
new/dnsdiag-2.7.0/dnsdiag.egg-info/PKG-INFO
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/PKG-INFO 2024-10-25 23:57:52.000000000 
+0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/PKG-INFO 1970-01-01 01:00:00.000000000 
+0100
@@ -1,35 +0,0 @@
-Metadata-Version: 2.1
-Name: dnsdiag
-Version: 2.6.0
-Summary: DNS Measurement, Troubleshooting and Security Auditing Toolset (ping, 
traceroute)
-Home-page: https://dnsdiag.org/
-Author: Babak Farrokhi
-Author-email: [email protected]
-License: BSD
-Keywords: dns traceroute ping performance
-Classifier: Topic :: System :: Networking
-Classifier: Environment :: Console
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Topic :: Internet :: Name Service (DNS)
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Operating System :: OS Independent
-License-File: LICENSE
-Requires-Dist: aioquic>=1.2.0
-Requires-Dist: cryptography>=42.0.5
-Requires-Dist: cymruwhois>=1.6
-Requires-Dist: dnspython>=2.7.0
-Requires-Dist: h2>=4.1.0
-Requires-Dist: httpx>=0.27.0
-
-
-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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/SOURCES.txt 
new/dnsdiag-2.7.0/dnsdiag.egg-info/SOURCES.txt
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/SOURCES.txt      2024-10-25 
23:57:52.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/SOURCES.txt      1970-01-01 
01:00:00.000000000 +0100
@@ -1,20 +0,0 @@
-LICENSE
-MANIFEST.in
-README.md
-dnseval.py
-dnsping.py
-dnstraceroute.py
-public-servers.txt
-public-v4.txt
-rootservers.txt
-setup.py
-dnsdiag.egg-info/PKG-INFO
-dnsdiag.egg-info/SOURCES.txt
-dnsdiag.egg-info/dependency_links.txt
-dnsdiag.egg-info/entry_points.txt
-dnsdiag.egg-info/requires.txt
-dnsdiag.egg-info/top_level.txt
-util/__init__.py
-util/dns.py
-util/shared.py
-util/whois.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/dependency_links.txt 
new/dnsdiag-2.7.0/dnsdiag.egg-info/dependency_links.txt
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/dependency_links.txt     2024-10-25 
23:57:52.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/dependency_links.txt     1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/entry_points.txt 
new/dnsdiag-2.7.0/dnsdiag.egg-info/entry_points.txt
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/entry_points.txt 2024-10-25 
23:57:52.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/entry_points.txt 1970-01-01 
01:00:00.000000000 +0100
@@ -1,4 +0,0 @@
-[console_scripts]
-dnseval = dnseval:main
-dnsping = dnsping:main
-dnstraceroute = dnstraceroute:main
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/requires.txt 
new/dnsdiag-2.7.0/dnsdiag.egg-info/requires.txt
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/requires.txt     2024-10-25 
23:57:52.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/requires.txt     1970-01-01 
01:00:00.000000000 +0100
@@ -1,6 +0,0 @@
-aioquic>=1.2.0
-cryptography>=42.0.5
-cymruwhois>=1.6
-dnspython>=2.7.0
-h2>=4.1.0
-httpx>=0.27.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsdiag.egg-info/top_level.txt 
new/dnsdiag-2.7.0/dnsdiag.egg-info/top_level.txt
--- old/dnsdiag-2.6.0/dnsdiag.egg-info/top_level.txt    2024-10-25 
23:57:52.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsdiag.egg-info/top_level.txt    1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-util
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnseval.py new/dnsdiag-2.7.0/dnseval.py
--- old/dnsdiag-2.6.0/dnseval.py        2024-10-25 16:53:59.000000000 +0200
+++ new/dnsdiag-2.7.0/dnseval.py        2025-09-21 11:55:15.000000000 +0200
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (c) 2016-2024, Babak Farrokhi
+# Copyright (c) 2016-2025, Babak Farrokhi
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,14 +37,14 @@
 import dns.rdatatype
 import dns.resolver
 
-import util.dns
+import dnsdiag.dns
 
 __author__ = 'Babak Farrokhi ([email protected])'
 __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
+from dnsdiag.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, 
setup_signal_handler, flags_to_text
+from dnsdiag.shared import __version__, Colors
 
 
 def usage():
@@ -156,7 +156,7 @@
             usage()
 
     # validate RR type
-    if not util.dns.valid_rdatatype(rdatatype):
+    if not dnsdiag.dns.valid_rdatatype(rdatatype):
         print('Error: Invalid record type "%s" ' % rdatatype)
         sys.exit(1)
 
@@ -214,7 +214,7 @@
                 continue
 
             try:
-                retval = util.dns.ping(qname, resolver, dst_port, rdatatype, 
waittime, count, proto, src_ip,
+                retval = dnsdiag.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:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnsping.py new/dnsdiag-2.7.0/dnsping.py
--- old/dnsdiag-2.6.0/dnsping.py        2024-10-25 23:39:58.000000000 +0200
+++ new/dnsdiag-2.7.0/dnsping.py        2025-09-21 11:55:15.000000000 +0200
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (c) 2016-2024, Babak Farrokhi
+# Copyright (c) 2016-2025, Babak Farrokhi
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -33,15 +33,15 @@
 import socket
 import sys
 import time
-import httpx
 from statistics import stdev
 
 import dns.flags
 import dns.resolver
+import httpx
 
-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__
+from dnsdiag.dns import PROTO_UDP, PROTO_TCP, PROTO_TLS, PROTO_HTTPS, 
PROTO_QUIC, PROTO_HTTP3, proto_to_text, \
+    unsupported_feature, random_string, getDefaultPort, valid_rdatatype
+from dnsdiag.shared import __version__
 
 __author__ = 'Babak Farrokhi ([email protected])'
 __license__ = 'BSD'
@@ -62,6 +62,7 @@
   -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)
+  -3, --http3       Use HTTP/3 as the transport protocol (DoH3)
   -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
@@ -154,11 +155,11 @@
     qname = 'wikipedia.org'
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], 
"qhc:s:t:w:i:vp:P:S:TQ46meDFXHrnEC:Lxa",
+        opts, args = getopt.getopt(sys.argv[1:], 
"qhc:s:t:w:i:vp:P:S:TQ346meDFXHrnEC: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"])
+                                    "expert", "answer", "quic", "http3"])
     except getopt.GetoptError as err:
         # print help information and exit:
         print_stderr(err, False)  # will print something like "option -a not 
recognized"
@@ -238,6 +239,11 @@
             if use_default_dst_port:
                 dst_port = getDefaultPort(proto)
 
+        elif o in ("-3", "--http3"):
+            proto = PROTO_HTTP3
+            if use_default_dst_port:
+                dst_port = getDefaultPort(proto)
+
         elif o in ("-4", "--ipv4"):
             af = socket.AF_INET
 
@@ -346,6 +352,18 @@
                 else:
                     unsupported_feature("DNS-over-HTTPS (DoH)")
 
+            elif proto is PROTO_HTTP3:
+                if hasattr(dns.query, 'quic'):
+                    try:
+                        answers = dns.query.https(query, dnsserver, 
timeout=timeout, port=dst_port,
+                                                  source=src_ip, 
source_port=src_port,
+                                                  
http_version=dns.query.HTTPVersion.H3)
+                    except ConnectionRefusedError:
+                        print_stderr(f"The server did not respond to 
DNS-over-HTTPS/3 on port {dst_port}",
+                                     should_die=True)
+                else:
+                    unsupported_feature("DNS-over-HTTPS/3 (DoH3)")
+
             elif proto is PROTO_QUIC:
                 if hasattr(dns.query, 'quic'):
                     try:
@@ -353,6 +371,9 @@
                                                  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)
+                    except ConnectionRefusedError:
+                        print_stderr(f"The server did not respond to 
DNS-over-HTTPS/3 on port {dst_port}",
+                                     should_die=True)
                 else:
                     unsupported_feature("DNS-over-QUIC (DoQ)")
 
@@ -401,6 +422,8 @@
                 if show_flags:
                     ans_flags = dns.flags.to_text(answers.flags)
                     edns_flags = dns.flags.edns_to_text(answers.ednsflags)
+                    if want_dnssec and not (answers.flags & dns.flags.AD):
+                        ans_flags += " --"  # add padding to printer output 
when dnssec is requested, but AD flag is not set
                     extras += " [%s]" % " ".join([ans_flags, 
edns_flags]).rstrip(' ')  # show both regular + edns flags
 
                 if want_nsid:
@@ -412,12 +435,11 @@
                 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)
+                            extras += " [EDE %d: \"%s\"]" % (ans_opt.code, 
ans_opt.text or "")
 
                 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
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/dnstraceroute.py 
new/dnsdiag-2.7.0/dnstraceroute.py
--- old/dnsdiag-2.6.0/dnstraceroute.py  2024-10-25 16:53:59.000000000 +0200
+++ new/dnsdiag-2.7.0/dnstraceroute.py  2025-09-21 11:55:15.000000000 +0200
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #
-# Copyright (c) 2016-2024, Babak Farrokhi
+# Copyright (c) 2016-2025, Babak Farrokhi
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,9 +37,9 @@
 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
+import dnsdiag.whois
+from dnsdiag.dns import PROTO_UDP, PROTO_TCP, setup_signal_handler
+from dnsdiag.shared import __version__, Colors
 
 # Global Variables
 quiet = False
@@ -116,7 +116,7 @@
     resp_time = None
 
     try:
-        resp = util.dns.ping(qname, server, port, rdtype, timeout, 1, proto, 
src_ip, use_edns, force_miss=False,
+        resp = dnsdiag.dns.ping(qname, server, port, rdtype, timeout, 1, 
proto, src_ip, use_edns, force_miss=False,
                              want_dnssec=False, socket_ttl=ttl)
 
     except SystemExit:
@@ -205,7 +205,7 @@
     color = Colors(color_mode)
 
     # validate RR type
-    if not util.dns.valid_rdatatype(rdatatype):
+    if not dnsdiag.dns.valid_rdatatype(rdatatype):
         print('Error: Invalid record type "%s" ' % rdatatype)
         sys.exit(1)
 
@@ -299,7 +299,7 @@
         if curr_addr:
             as_name = ""
             if as_lookup:
-                asn, whois_cache = util.whois.asn_lookup(curr_addr, 
whois_cache)
+                asn, whois_cache = dnsdiag.whois.asn_lookup(curr_addr, 
whois_cache)
                 as_name = ''
                 try:
                     if asn and asn.asn != "NA":
@@ -338,7 +338,7 @@
 
 if __name__ == '__main__':
     try:
-        whois_cache = util.whois.restore()
+        whois_cache = dnsdiag.whois.restore()
         main()
     finally:
-        util.whois.save(whois_cache)
+        dnsdiag.whois.save(whois_cache)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/requirements.txt 
new/dnsdiag-2.7.0/requirements.txt
--- old/dnsdiag-2.6.0/requirements.txt  1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/requirements.txt  2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,6 @@
+aioquic>=1.2.0
+cryptography>=42.0.5
+cymruwhois>=1.6
+dnspython>=2.8.0
+h2>=4.1.0
+httpx>=0.27.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/setup.cfg new/dnsdiag-2.7.0/setup.cfg
--- old/dnsdiag-2.6.0/setup.cfg 2024-10-25 23:57:52.778800200 +0200
+++ new/dnsdiag-2.7.0/setup.cfg 1970-01-01 01:00:00.000000000 +0100
@@ -1,4 +0,0 @@
-[egg_info]
-tag_build = 
-tag_date = 0
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/setup.py new/dnsdiag-2.7.0/setup.py
--- old/dnsdiag-2.6.0/setup.py  2024-10-25 23:44:57.000000000 +0200
+++ new/dnsdiag-2.7.0/setup.py  2025-09-21 11:55:15.000000000 +0200
@@ -1,24 +1,22 @@
 from setuptools import setup, find_packages
-from util.shared import __version__
+from dnsdiag.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'],
+    install_requires=['aioquic>=1.2.0', 'cryptography>=42.0.5', 
'cymruwhois>=1.6', 'dnspython>=2.8.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",
@@ -27,6 +25,7 @@
     author="Babak Farrokhi",
     author_email="[email protected]",
     description="DNS Measurement, Troubleshooting and Security Auditing 
Toolset (ping, traceroute)",
+    long_description_content_type="text/plain",
     long_description="""
 DNSDiag provides a handful of tools to measure and diagnose your DNS
 performance and integrity. Using dnsping, dnstraceroute and dnseval tools
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/tox.ini new/dnsdiag-2.7.0/tox.ini
--- old/dnsdiag-2.6.0/tox.ini   1970-01-01 01:00:00.000000000 +0100
+++ new/dnsdiag-2.7.0/tox.ini   2025-09-21 11:55:15.000000000 +0200
@@ -0,0 +1,6 @@
+[pycodestyle]
+ignore = E501, E741
+
+[flake8]
+ignore = E501, E741
+exclude = .venv
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/util/dns.py 
new/dnsdiag-2.7.0/util/dns.py
--- old/dnsdiag-2.6.0/util/dns.py       2024-10-25 23:39:58.000000000 +0200
+++ new/dnsdiag-2.7.0/util/dns.py       1970-01-01 01:00:00.000000000 +0100
@@ -1,271 +0,0 @@
-#!/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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/util/shared.py 
new/dnsdiag-2.7.0/util/shared.py
--- old/dnsdiag-2.6.0/util/shared.py    2024-10-25 23:40:33.000000000 +0200
+++ new/dnsdiag-2.7.0/util/shared.py    1970-01-01 01:00:00.000000000 +0100
@@ -1,44 +0,0 @@
-#!/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 = ''
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dnsdiag-2.6.0/util/whois.py 
new/dnsdiag-2.7.0/util/whois.py
--- old/dnsdiag-2.6.0/util/whois.py     2024-10-25 16:53:59.000000000 +0200
+++ new/dnsdiag-2.7.0/util/whois.py     1970-01-01 01:00:00.000000000 +0100
@@ -1,83 +0,0 @@
-#!/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()

Reply via email to