=== added file '.flake8'
--- old/.flake8	1970-01-01 00:00:00 +0000
+++ new/.flake8	2021-04-07 01:51:59 +0000
@@ -0,0 +1,21 @@
+[flake8]
+ignore =
+       # Prefer emacs indentation of continued lines
+       E126,
+       E127,
+       E129,
+       # Whitespace round parameter '=' can be excessive
+       E252,
+       # Multiple # in a comment is OK
+       E266,
+       # Not excited by the "two blank lines" rule
+       E302,
+       E305,
+       # or the one blank line rule
+       E306,
+       # Ambigious variables are ok.
+       E741,
+       # Lines ending with binary operators are OK
+       W504,
+
+max-line-length = 80

=== added directory '.github'
=== added file '.github/dependabot.yml'
--- old/.github/dependabot.yml	1970-01-01 00:00:00 +0000
+++ new/.github/dependabot.yml	2020-08-06 23:26:37 +0000
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "pip" # See documentation for possible values
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "daily"

=== added file '.gitignore'
--- old/.gitignore	1970-01-01 00:00:00 +0000
+++ new/.gitignore	2020-08-06 23:26:37 +0000
@@ -0,0 +1,18 @@
+build
+dist
+MANIFEST
+html
+html.zip
+html.tar.gz
+tests/*.out
+*.pyc
+.coverage
+.tox
+dnspython.egg-info/
+.eggs/
+.mypy_cache/
+.python-version
+poetry.lock
+htmlcov
+coverage.xml
+.dir-locals.el

=== added file '.readthedocs.yml'
--- old/.readthedocs.yml	1970-01-01 00:00:00 +0000
+++ new/.readthedocs.yml	2020-08-06 23:26:37 +0000
@@ -0,0 +1,16 @@
+version: 2
+
+sphinx:
+  configuration: doc/conf.py
+
+formats: []
+
+python:
+  version: 3.6
+  install:
+    - method: pip
+      path: .
+      extra_requirements:
+        - dnssec
+        - idna
+        - doh

=== added file '.travis.yml'
--- old/.travis.yml	1970-01-01 00:00:00 +0000
+++ new/.travis.yml	2021-04-07 01:51:59 +0000
@@ -0,0 +1,16 @@
+language: python
+python:
+  - "3.6"
+  - "3.7"
+  - "3.8"
+  - "3.9"
+branches:
+  except:
+    - python3
+install:
+ - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
+ - ~/.poetry/bin/poetry install -E dnssec -E doh -E idna -E trio -E curio
+script:
+ - ~/.poetry/bin/poetry run pytest --cov=. --cov-report=xml:coverage.xml
+after_success:
+ - bash <(curl -s https://codecov.io/bash)

=== added file 'LICENSE'
--- old/LICENSE	1970-01-01 00:00:00 +0000
+++ new/LICENSE	2018-12-23 00:54:24 +0000
@@ -0,0 +1,35 @@
+ISC License
+
+Copyright (C) Dnspython Contributors
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted, provided that the
+above copyright notice and this permission notice appear in all
+copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+
+
+Copyright (C) 2001-2017 Nominum, Inc.
+Copyright (C) Google Inc.
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose with or without fee is hereby granted,
+provided that the above copyright notice and this permission notice
+appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

=== added file 'MANIFEST.in'
--- old/MANIFEST.in	1970-01-01 00:00:00 +0000
+++ new/MANIFEST.in	2021-04-07 01:51:59 +0000
@@ -0,0 +1,3 @@
+include LICENSE ChangeLog README.md
+recursive-include examples *.txt *.py
+recursive-include tests *.txt *.py Makefile *.good example query *.pickle

=== added file 'Makefile'
--- old/Makefile	1970-01-01 00:00:00 +0000
+++ new/Makefile	2021-04-07 01:51:59 +0000
@@ -0,0 +1,74 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# $Id: Makefile,v 1.16 2004/03/19 00:17:27 halley Exp $
+
+PYTHON=python
+
+all:
+	${PYTHON} ./setup.py build
+
+install:
+	${PYTHON} ./setup.py install
+
+clean:
+	${PYTHON} ./setup.py clean --all
+	find . -name '*.pyc' -exec rm {} \;
+	find . -name '*.pyo' -exec rm {} \;
+	rm -f TAGS
+	rm -rf htmlcov .coverage
+	rm -rf .pytest_cache
+
+distclean: clean docclean
+	rm -rf build dist
+	rm -f MANIFEST
+	rm -rf dnspython.egg-info
+
+doc:
+	cd doc; make html
+
+docclean:
+	rm -rf doc/_build
+
+check: test
+
+test:
+	cd tests; make test
+
+potest:
+	poetry run pytest
+
+potestlf:
+	poetry run pytest --lf
+
+potype:
+	poetry run python -m mypy examples tests dns/*.py
+
+polint:
+	poetry run pylint dns
+
+poflake:
+	poetry run flake8 dns
+
+pocov:
+	poetry run coverage run --branch -m pytest
+	poetry run coverage html --include 'dns*'
+	poetry run coverage report --include 'dns*'
+
+pokit:
+	po run python setup.py sdist --formats=zip bdist_wheel
+

=== added file 'PKG-INFO'
--- old/PKG-INFO	1970-01-01 00:00:00 +0000
+++ new/PKG-INFO	2021-04-07 01:51:59 +0000
@@ -0,0 +1,39 @@
+Metadata-Version: 2.1
+Name: dnspython
+Version: 1.15.1.dev1197+g6a53ddf
+Summary: DNS toolkit
+Home-page: http://www.dnspython.org
+Author: Bob Halley
+Author-email: halley@dnspython.org
+License: ISC
+Description: dnspython is a DNS toolkit for Python. It supports almost all
+        record types. It can be used for queries, zone transfers, and dynamic
+        updates.  It supports TSIG authenticated messages and EDNS0.
+        
+        dnspython provides both high and low level access to DNS. The high
+        level classes perform queries for data of a given name, type, and
+        class, and return an answer set.  The low level classes allow
+        direct manipulation of DNS zones, messages, names, and records.
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: ISC License (ISCL)
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet :: Name Service (DNS)
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Provides: dns
+Requires-Python: >=3.6
+Description-Content-Type: text/plain
+Provides-Extra: curio
+Provides-Extra: dnssec
+Provides-Extra: doh
+Provides-Extra: idna
+Provides-Extra: trio

=== added file 'README.md'
--- old/README.md	1970-01-01 00:00:00 +0000
+++ new/README.md	2021-04-07 01:51:59 +0000
@@ -0,0 +1,74 @@
+# dnspython
+
+[![Build Status](https://travis-ci.org/rthalley/dnspython.svg?branch=master)](https://travis-ci.org/rthalley/dnspython)
+[![Documentation Status](https://readthedocs.org/projects/dnspython/badge/?version=latest)](https://dnspython.readthedocs.io/en/latest/?badge=latest)
+[![PyPI version](https://badge.fury.io/py/dnspython.svg)](https://badge.fury.io/py/dnspython)
+[![Coverage](https://codecov.io/github/rthalley/dnspython/coverage.svg?branch=master)](https://codecov.io/gh/rthalley/dnspython)
+[![License: ISC](https://img.shields.io/badge/License-ISC-brightgreen.svg)](https://opensource.org/licenses/ISC)
+
+## INTRODUCTION
+
+dnspython is a DNS toolkit for Python. It supports almost all record types. It
+can be used for queries, zone transfers, and dynamic updates. It supports TSIG
+authenticated messages and EDNS0.
+
+dnspython provides both high and low level access to DNS. The high level classes
+perform queries for data of a given name, type, and class, and return an answer
+set. The low level classes allow direct manipulation of DNS zones, messages,
+names, and records.
+
+To see a few of the ways dnspython can be used, look in the `examples/`
+directory.
+
+dnspython is a utility to work with DNS, `/etc/hosts` is thus not used. For
+simple forward DNS lookups, it's better to use `socket.getaddrinfo()` or
+`socket.gethostbyname()`.
+
+dnspython originated at Nominum where it was developed
+to facilitate the testing of DNS software.
+
+## ABOUT THIS RELEASE
+
+This is the development version of dnspython 2.2.0.
+Please read
+[What's New](https://dnspython.readthedocs.io/en/stable/whatsnew.html) for
+information about the changes in this release.
+
+## INSTALLATION
+
+* Many distributions have dnspython packaged for you, so you should
+  check there first.
+* If you have pip installed, you can do `pip install dnspython`
+* If not just download the source file and unzip it, then run
+  `sudo python setup.py install`
+* To install the latest from the master branch, run `pip install git+https://github.com/rthalley/dnspython.git`
+
+If you want to use DNS-over-HTTPS, you must run
+`pip install dnspython[doh]`.
+
+If you want to use DNSSEC functionality, you must run
+`pip install dnspython[dnssec]`.
+
+If you want to use internationalized domain names (IDNA)
+functionality, you must run
+`pip install dnspython[idna]`
+
+If you want to use the Trio asynchronous I/O package, you must run
+`pip install dnspython[trio]`.
+
+If you want to use the Curio asynchronous I/O package, you must run
+`pip install dnspython[curio]`.
+
+Note that you can install any combination of the above, e.g.:
+`pip install dnspython[doh,dnssec,idna]`
+
+### Notices
+
+Python 2.x support ended with the release of 1.16.0.  dnspython 2.0.0 and
+later only support Python 3.6 and later.
+
+Documentation has moved to
+[dnspython.readthedocs.io](https://dnspython.readthedocs.io).
+
+The ChangeLog has been discontinued.  Please see the git history for detailed
+change information.

=== added file 'SECURITY.md'
--- old/SECURITY.md	1970-01-01 00:00:00 +0000
+++ new/SECURITY.md	2020-08-06 23:26:37 +0000
@@ -0,0 +1,70 @@
+# Security Policy
+
+## Supported Versions
+
+The following versions would get a security update release if necessary.
+
+| Version  | Supported          |
+| -------- | ------------------ |
+| 2.0.x    | :white_check_mark: |
+| 1.16.x   | :white_check_mark: |
+| < 1.16.0 | :x:                |
+
+## Reporting a Vulnerability
+
+Send email to security@dnspython.org.  For confidentiality, use the following public key:
+
+```
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBF7mj/kBEAC6UB++f8N8qCBQuRLzIHoZd5oEYpylm+C9CrbfA0BYQpql9L6s
+Ty8C/aph3ZHqtKutsbotQjmnd2b5D/W1Ku5F1Z+B5/gr2ija9NV1u66wJjKOdHon
+LLYMaquzIcQvwxkrjAcGd1BZsem0maHKR/iT2bpGOmLB0U89lnEmwmNgSh+vg+68
+SzXl6WFAJd89J7+UIFUZm/qoyHp7sGzO3+q44ecupUeH+xDpuQVAxnMF7nWT8G+P
+UZ6rT1AFz7f9PJ9Ul+szpALem9/YWZO7UpBqIfHzbOBEgcGB6RKm0Yk/RUSSYdk8
+kFFvRogZwSPbIC5cYYbMRX8BGmaPGVVLAPnzvA7U0/3XRIOc0TCcjfW3n7mxZ1R+
+YxcQfF63zys72dey77zoZCtLaWKifcNKJQcRIVUHoDRP1YAlDh45wm2W4QKKmdUn
+eo8Ghl7IOhYeQi37OxtrA2cYEp0Y1oUaxYFo5NdLaW40asm9lAf1ZS9hrgK0ZHtK
+F9aN9VFSmsbuvzDN+miQwNfoH1/hCDJtoZPXZ0Hqjy/hbC2WCE8Tdw2zBb4gZzfO
+3nOcWSOOwrJqz0/Hl/7VE6eQDKq3rcc+BlhqW3WDxnK41RFJPkGyn+NauV+RDqlR
+CoOj3JiAp6mH8HFdnd92OcKkf7ZDFU/jHvuk6WgZKN/fgNS+AwG+ErHGdQARAQAB
+tCtEbnNweXRob24gU2VjdXJpdHkgPHNlY3VyaXR5QGRuc3B5dGhvbi5vcmc+iQJO
+BBMBCAA4FiEErlGglzMd8tn0T8v2tEx7LtBQJQ8FAl7mj/kCGwMFCwkIBwIGFQoJ
+CAsCBBYCAwECHgECF4AACgkQtEx7LtBQJQ+BOw//b413gZ4+/NkTH4L8qbtq4VmR
+Y8m7qBG5uEqqZ4Nu0U+8m6uBvIVtSNkvmNgVvYRbjRIhSZr+bAqhPUa65m4/oN9s
+/TliovKeZv0087o5wfe4eBs+4Yvt6kq5mazNKxHyIE3uvjmHNmVL3E7MPByJW0Yl
+g7B1FEi90qCKc/8UAVyRZJeZr87wj4YV2R66EBwhylT9TKXU2Xxr7FXmjePeX9ZN
+DsO2JxHQf7ZIGcFPeLsWDo7Vra+TPeoCr0Klq0Z4TvwXr24zTqin0MNp7xzXioW7
+OpslEUJ5e8ce8g1Lyv5y9UUv7wQd6OClf+FXd9a/SQtjiflU+hFr8lG55YvOq3DO
+JZn1peD8/QphdJ46DUuYf5fp8AFw4yNnMca6IiFgJ19Rd+zTFahgKdwA4q41ex9d
+DEvjAs+JNuiKPbzckALYAPrklZfTk6OFWolg/XqSEFGdtxtw6787efe8xKlYBpvn
+Q+zkxpRJfMfJwO7Z54KWgoy/YiwbGPeDtS8TX5OJLt04NBQ5WcK81qtIMstTZ7cp
+SPuoDHS9UIhiedvG9yay/ob4Pe9lcBECv6+YpcWqu6vHS+qdzUflJQ+nZHIhfQXE
+ymV4hRXOUbH9Rc8m3LViB+YYlBToLzSJar/W7DDORWAVfMqO5rmqZEYYG1CQvoiV
+w4CapV4Jg9URwiqs+HG5Ag0EXuaP+QEQAOlGCchcxIy0jaQ6qTaG7eHF/CEMhk0u
+wkZ7lfFsZvm/6wUItKfdfC0Y+HQx9fUVjzs1dtwvEhTKY1tfxF6Kyu/+oejCY+WU
+ovjTq6PN+HxgDkMhd32HpznS06KByYEVW3XMtzgW9KKb37nDSs1OSv7kpdvp70tv
+8uaNds95S1aReGVaio9lHikSJXUsjYH1pQhRbwr5bzR+LQVPsFhlKSF6PY31lRjU
+UxN5koxogefehAgHRKktXYS89IUk1uM4yIoC13JJfsjUBXjpgZu1C/cFx8bEVNiW
+42r7jugvuQAyBba7vgCE/BRP3V+ctE5asxLlLJWKfDlmiFFubFEc3oqmYOH4XEAt
+ICA7lVC1bOAAhHaNy3K3Ae7PRNOwDb+cinpImmYQlky+clK3wSgfIrM10sDBag10
+y5CyqziMN6h+n50zxNuxSX6//qRH4TL2AD4TmBugqC8gP0EPgMzot4H9c/SZcoTA
+OC04ddlJtwTwDMx42C4vGjT0yXl3VfSqwfgA5lKusnM2jw82edzk3UdW8HSaSN3x
+QWQ4cyEjbgnEL4AuY8RWTCHDNiovwN4jcDPfPyQp/3DbnxhDD9kJyuR6rcp8/WDA
+vp666HQUmySz5vVUs1K1+hNeXA909aW/hT9hhXIkeAmp2wm3K95zIvc1foHKPR6O
+EdqpaQFQA6LhABEBAAGJAjYEGAEIACAWIQSuUaCXMx3y2fRPy/a0THsu0FAlDwUC
+XuaP+QIbDAAKCRC0THsu0FAlD1nBEACcyyZEwMK3RkD5BJVZTTXBjGRxUohvsx9G
+tv/5YQFTHfjB/h5tvEI6fqmIUm+DoAzQNydzr/vpu5AA7WfUr59TmIVanvQHC2ir
+vh08m37OdECORejbPzCj+BHUkAk3NblfKRXYyNduS0qBVB8eLRAAX6PwMeK3TFwy
+z3lW9H5uLrj7wPc1d5932DeYWlYUF5mFyQzbgbhBW5wHN7B0iD6iNnBzMXoM6WRv
+tWU9QGzCmbJaTVLh1W7yArr4qnibz/XaQ0+1XLl6dZIe3XFztlFaHJ58aQKBf7k9
+IC70stP7A3xeb9hCttPJI4qPl6YJwiPp0OeMy5H+KPcfkk7gyTNlcjSBiC1vy6Nh
+vpjLKJrTwEftfc8B+p0JwQGgOLWCqziodCbR5Tiw/6S2cSl94Cu8OTMxPgN7JXKA
+0W0WjutHlAO1tlKITUl4qTYQ+7r7JaOU6IcnyQksUzpm47bqEYwoGmscWObpEhs3
+e862BGb8qdY821UcReukRaKf4nW11mCul6VSvApaRVYxcK8EZklxpwJOtNKIKUzq
+l9fXngLMi6Qas6xx/ti3eHtEom2atKMnuRlSM7idOZZGowLtf65GhxogVsuFJhCX
+sp5LOiYu+1IxUgzHC1snMvG7JJ8JT9XkvlVdgSDrBtfseekBkkl6IQeL6J4nbywe
+clqgUVNxjw==
+=qmj5
+-----END PGP PUBLIC KEY BLOCK-----
+```

=== added file 'azure-pipelines.yml'
--- old/azure-pipelines.yml	1970-01-01 00:00:00 +0000
+++ new/azure-pipelines.yml	2021-04-07 01:51:59 +0000
@@ -0,0 +1,62 @@
+# Python package
+# Create and test a Python package on multiple Python versions.
+# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
+# https://docs.microsoft.com/azure/devops/pipelines/languages/python
+
+trigger:
+- master
+
+jobs:
+- job: Windows
+  pool:
+    vmImage: 'vs2017-win2016'
+  strategy:
+    matrix:
+      Python38:
+        python.version: '3.8'
+  steps:
+  - task: UsePythonVersion@0
+    inputs:
+      versionSpec: '$(python.version)'
+    displayName: 'Use Python $(python.version)'
+
+#  - script: |
+#      python -m pip install --upgrade pip wheel setuptools
+#    displayName: 'Install pip and wheel'
+
+  - powershell:
+      (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -
+    displayName: 'Install Poetry'
+
+  - script: |
+      %USERPROFILE%\.poetry\bin\poetry install -E dnssec -E doh -E idna -E trio -E curio
+    displayName: 'Install python dependencies'
+
+#  - script: |
+#      python -m pip install requests requests-toolbelt idna cryptography
+#      python -m pip install trio sniffio curio
+#    displayName: 'Install python dependencies'
+
+  - script: |
+      dotnet tool install --global Codecov.Tool
+    displayName: 'Install Codecov.Tool'
+
+  - script: |
+      %USERPROFILE%\.poetry\bin\poetry run python -m pip install pytest-azurepipelines
+      %USERPROFILE%\.poetry\bin\poetry run pytest --junitxml=junit/test-results.xml --cov=. --cov-report=xml --cov-report=html
+    displayName: 'pytest'
+
+  - task: PublishTestResults@2
+    condition: succeededOrFailed()
+    inputs:
+      testResultsFiles: '**/test-*.xml'
+      testRunTitle: 'Publish test results for Python $(python.version)'
+
+#  - task: PublishCodeCoverageResults@1
+#    inputs:
+#      codeCoverageTool: Cobertura
+#      summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
+
+  - script: |
+      %USERPROFILE%\.dotnet\tools\codecov -f coverage.xml
+    displayName: 'Upload to codecov'

=== added file 'codecov.yml'
--- old/codecov.yml	1970-01-01 00:00:00 +0000
+++ new/codecov.yml	2020-08-06 23:26:37 +0000
@@ -0,0 +1,15 @@
+coverage:
+  status:
+    project:
+      default:
+        target: auto
+        threshold: 2%
+    patch:
+      default:
+        target: 85%
+        threshold: 2%
+ignore:
+  - 'tests/*'
+  - 'examples/*'
+  - 'doc/*'
+  - 'setup.py'

=== added directory 'dns'
=== added file 'dns/__init__.py'
--- old/dns/__init__.py	1970-01-01 00:00:00 +0000
+++ new/dns/__init__.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,66 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""dnspython DNS toolkit"""
+
+__all__ = [
+    'asyncbackend',
+    'asyncquery',
+    'asyncresolver',
+    'dnssec',
+    'e164',
+    'edns',
+    'entropy',
+    'exception',
+    'flags',
+    'immutable',
+    'inet',
+    'ipv4',
+    'ipv6',
+    'message',
+    'name',
+    'namedict',
+    'node',
+    'opcode',
+    'query',
+    'rcode',
+    'rdata',
+    'rdataclass',
+    'rdataset',
+    'rdatatype',
+    'renderer',
+    'resolver',
+    'reversename',
+    'rrset',
+    'serial',
+    'set',
+    'tokenizer',
+    'transaction',
+    'tsig',
+    'tsigkeyring',
+    'ttl',
+    'rdtypes',
+    'update',
+    'version',
+    'versioned',
+    'wire',
+    'xfr',
+    'zone',
+    'zonefile',
+]
+
+from dns.version import version as __version__  # noqa

=== added file 'dns/_asyncbackend.py'
--- old/dns/_asyncbackend.py	1970-01-01 00:00:00 +0000
+++ new/dns/_asyncbackend.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,69 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# This is a nullcontext for both sync and async.  3.7 has a nullcontext,
+# but it is only for sync use.
+
+class NullContext:
+    def __init__(self, enter_result=None):
+        self.enter_result = enter_result
+
+    def __enter__(self):
+        return self.enter_result
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        pass
+
+    async def __aenter__(self):
+        return self.enter_result
+
+    async def __aexit__(self, exc_type, exc_value, traceback):
+        pass
+
+
+# These are declared here so backends can import them without creating
+# circular dependencies with dns.asyncbackend.
+
+class Socket:  # pragma: no cover
+    async def close(self):
+        pass
+
+    async def getpeername(self):
+        raise NotImplementedError
+
+    async def getsockname(self):
+        raise NotImplementedError
+
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_value, traceback):
+        await self.close()
+
+
+class DatagramSocket(Socket):  # pragma: no cover
+    async def sendto(self, what, destination, timeout):
+        raise NotImplementedError
+
+    async def recvfrom(self, size, timeout):
+        raise NotImplementedError
+
+
+class StreamSocket(Socket):  # pragma: no cover
+    async def sendall(self, what, destination, timeout):
+        raise NotImplementedError
+
+    async def recv(self, size, timeout):
+        raise NotImplementedError
+
+
+class Backend:    # pragma: no cover
+    def name(self):
+        return 'unknown'
+
+    async def make_socket(self, af, socktype, proto=0,
+                          source=None, destination=None, timeout=None,
+                          ssl_context=None, server_hostname=None):
+        raise NotImplementedError
+
+    def datagram_connection_required(self):
+        return False

=== added file 'dns/_asyncio_backend.py'
--- old/dns/_asyncio_backend.py	1970-01-01 00:00:00 +0000
+++ new/dns/_asyncio_backend.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,150 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""asyncio library query support"""
+
+import socket
+import asyncio
+import sys
+
+import dns._asyncbackend
+import dns.exception
+
+
+_is_win32 = sys.platform == 'win32'
+
+def _get_running_loop():
+    try:
+        return asyncio.get_running_loop()
+    except AttributeError:  # pragma: no cover
+        return asyncio.get_event_loop()
+
+
+class _DatagramProtocol:
+    def __init__(self):
+        self.transport = None
+        self.recvfrom = None
+
+    def connection_made(self, transport):
+        self.transport = transport
+
+    def datagram_received(self, data, addr):
+        if self.recvfrom:
+            self.recvfrom.set_result((data, addr))
+            self.recvfrom = None
+
+    def error_received(self, exc):  # pragma: no cover
+        if self.recvfrom and not self.recvfrom.done():
+            self.recvfrom.set_exception(exc)
+
+    def connection_lost(self, exc):
+        if self.recvfrom and not self.recvfrom.done():
+            self.recvfrom.set_exception(exc)
+
+    def close(self):
+        self.transport.close()
+
+
+async def _maybe_wait_for(awaitable, timeout):
+    if timeout:
+        try:
+            return await asyncio.wait_for(awaitable, timeout)
+        except asyncio.TimeoutError:
+            raise dns.exception.Timeout(timeout=timeout)
+    else:
+        return await awaitable
+
+
+class DatagramSocket(dns._asyncbackend.DatagramSocket):
+    def __init__(self, family, transport, protocol):
+        self.family = family
+        self.transport = transport
+        self.protocol = protocol
+
+    async def sendto(self, what, destination, timeout):  # pragma: no cover
+        # no timeout for asyncio sendto
+        self.transport.sendto(what, destination)
+
+    async def recvfrom(self, size, timeout):
+        # ignore size as there's no way I know to tell protocol about it
+        done = _get_running_loop().create_future()
+        assert self.protocol.recvfrom is None
+        self.protocol.recvfrom = done
+        await _maybe_wait_for(done, timeout)
+        return done.result()
+
+    async def close(self):
+        self.protocol.close()
+
+    async def getpeername(self):
+        return self.transport.get_extra_info('peername')
+
+    async def getsockname(self):
+        return self.transport.get_extra_info('sockname')
+
+
+class StreamSocket(dns._asyncbackend.StreamSocket):
+    def __init__(self, af, reader, writer):
+        self.family = af
+        self.reader = reader
+        self.writer = writer
+
+    async def sendall(self, what, timeout):
+        self.writer.write(what)
+        return await _maybe_wait_for(self.writer.drain(), timeout)
+
+    async def recv(self, count, timeout):
+        return await _maybe_wait_for(self.reader.read(count),
+                                     timeout)
+
+    async def close(self):
+        self.writer.close()
+        try:
+            await self.writer.wait_closed()
+        except AttributeError:  # pragma: no cover
+            pass
+
+    async def getpeername(self):
+        return self.writer.get_extra_info('peername')
+
+    async def getsockname(self):
+        return self.writer.get_extra_info('sockname')
+
+
+class Backend(dns._asyncbackend.Backend):
+    def name(self):
+        return 'asyncio'
+
+    async def make_socket(self, af, socktype, proto=0,
+                          source=None, destination=None, timeout=None,
+                          ssl_context=None, server_hostname=None):
+        if destination is None and socktype == socket.SOCK_DGRAM and \
+           _is_win32:
+            raise NotImplementedError('destinationless datagram sockets '
+                                      'are not supported by asyncio '
+                                      'on Windows')
+        loop = _get_running_loop()
+        if socktype == socket.SOCK_DGRAM:
+            transport, protocol = await loop.create_datagram_endpoint(
+                _DatagramProtocol, source, family=af,
+                proto=proto, remote_addr=destination)
+            return DatagramSocket(af, transport, protocol)
+        elif socktype == socket.SOCK_STREAM:
+            (r, w) = await _maybe_wait_for(
+                asyncio.open_connection(destination[0],
+                                        destination[1],
+                                        ssl=ssl_context,
+                                        family=af,
+                                        proto=proto,
+                                        local_addr=source,
+                                        server_hostname=server_hostname),
+                timeout)
+            return StreamSocket(af, r, w)
+        raise NotImplementedError('unsupported socket ' +
+                                  f'type {socktype}')  # pragma: no cover
+
+    async def sleep(self, interval):
+        await asyncio.sleep(interval)
+
+    def datagram_connection_required(self):
+        return _is_win32
+        

=== added file 'dns/_curio_backend.py'
--- old/dns/_curio_backend.py	1970-01-01 00:00:00 +0000
+++ new/dns/_curio_backend.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,108 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""curio async I/O library query support"""
+
+import socket
+import curio
+import curio.socket  # type: ignore
+
+import dns._asyncbackend
+import dns.exception
+import dns.inet
+
+
+def _maybe_timeout(timeout):
+    if timeout:
+        return curio.ignore_after(timeout)
+    else:
+        return dns._asyncbackend.NullContext()
+
+
+# for brevity
+_lltuple = dns.inet.low_level_address_tuple
+
+# pylint: disable=redefined-outer-name
+
+
+class DatagramSocket(dns._asyncbackend.DatagramSocket):
+    def __init__(self, socket):
+        self.socket = socket
+        self.family = socket.family
+
+    async def sendto(self, what, destination, timeout):
+        async with _maybe_timeout(timeout):
+            return await self.socket.sendto(what, destination)
+        raise dns.exception.Timeout(timeout=timeout)  # pragma: no cover
+
+    async def recvfrom(self, size, timeout):
+        async with _maybe_timeout(timeout):
+            return await self.socket.recvfrom(size)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def close(self):
+        await self.socket.close()
+
+    async def getpeername(self):
+        return self.socket.getpeername()
+
+    async def getsockname(self):
+        return self.socket.getsockname()
+
+
+class StreamSocket(dns._asyncbackend.StreamSocket):
+    def __init__(self, socket):
+        self.socket = socket
+        self.family = socket.family
+
+    async def sendall(self, what, timeout):
+        async with _maybe_timeout(timeout):
+            return await self.socket.sendall(what)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def recv(self, size, timeout):
+        async with _maybe_timeout(timeout):
+            return await self.socket.recv(size)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def close(self):
+        await self.socket.close()
+
+    async def getpeername(self):
+        return self.socket.getpeername()
+
+    async def getsockname(self):
+        return self.socket.getsockname()
+
+
+class Backend(dns._asyncbackend.Backend):
+    def name(self):
+        return 'curio'
+
+    async def make_socket(self, af, socktype, proto=0,
+                          source=None, destination=None, timeout=None,
+                          ssl_context=None, server_hostname=None):
+        if socktype == socket.SOCK_DGRAM:
+            s = curio.socket.socket(af, socktype, proto)
+            try:
+                if source:
+                    s.bind(_lltuple(source, af))
+            except Exception:  # pragma: no cover
+                await s.close()
+                raise
+            return DatagramSocket(s)
+        elif socktype == socket.SOCK_STREAM:
+            if source:
+                source_addr = _lltuple(source, af)
+            else:
+                source_addr = None
+            async with _maybe_timeout(timeout):
+                s = await curio.open_connection(destination[0], destination[1],
+                                                ssl=ssl_context,
+                                                source_addr=source_addr,
+                                                server_hostname=server_hostname)
+            return StreamSocket(s)
+        raise NotImplementedError('unsupported socket ' +
+                                  f'type {socktype}')  # pragma: no cover
+
+    async def sleep(self, interval):
+        await curio.sleep(interval)

=== added file 'dns/_immutable_attr.py'
--- old/dns/_immutable_attr.py	1970-01-01 00:00:00 +0000
+++ new/dns/_immutable_attr.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,84 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# This implementation of the immutable decorator is for python 3.6,
+# which doesn't have Context Variables.  This implementation is somewhat
+# costly for classes with slots, as it adds a __dict__ to them.
+
+
+import inspect
+
+
+class _Immutable:
+    """Immutable mixin class"""
+
+    # Note we MUST NOT have __slots__ as that causes
+    #
+    #    TypeError: multiple bases have instance lay-out conflict
+    #
+    # when we get mixed in with another class with slots.  When we
+    # get mixed into something with slots, it effectively adds __dict__ to
+    # the slots of the other class, which allows attribute setting to work,
+    # albeit at the cost of the dictionary.
+
+    def __setattr__(self, name, value):
+        if not hasattr(self, '_immutable_init') or \
+           self._immutable_init is not self:
+            raise TypeError("object doesn't support attribute assignment")
+        else:
+            super().__setattr__(name, value)
+
+    def __delattr__(self, name):
+        if not hasattr(self, '_immutable_init') or \
+           self._immutable_init is not self:
+            raise TypeError("object doesn't support attribute assignment")
+        else:
+            super().__delattr__(name)
+
+
+def _immutable_init(f):
+    def nf(*args, **kwargs):
+        try:
+            # Are we already initializing an immutable class?
+            previous = args[0]._immutable_init
+        except AttributeError:
+            # We are the first!
+            previous = None
+            object.__setattr__(args[0], '_immutable_init', args[0])
+        try:
+            # call the actual __init__
+            f(*args, **kwargs)
+        finally:
+            if not previous:
+                # If we started the initialzation, establish immutability
+                # by removing the attribute that allows mutation
+                object.__delattr__(args[0], '_immutable_init')
+    nf.__signature__ = inspect.signature(f)
+    return nf
+
+
+def immutable(cls):
+    if _Immutable in cls.__mro__:
+        # Some ancestor already has the mixin, so just make sure we keep
+        # following the __init__ protocol.
+        cls.__init__ = _immutable_init(cls.__init__)
+        if hasattr(cls, '__setstate__'):
+            cls.__setstate__ = _immutable_init(cls.__setstate__)
+        ncls = cls
+    else:
+        # Mixin the Immutable class and follow the __init__ protocol.
+        class ncls(_Immutable, cls):
+
+            @_immutable_init
+            def __init__(self, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+
+            if hasattr(cls, '__setstate__'):
+                @_immutable_init
+                def __setstate__(self, *args, **kwargs):
+                    super().__setstate__(*args, **kwargs)
+
+        # make ncls have the same name and module as cls
+        ncls.__name__ = cls.__name__
+        ncls.__qualname__ = cls.__qualname__
+        ncls.__module__ = cls.__module__
+    return ncls

=== added file 'dns/_immutable_ctx.py'
--- old/dns/_immutable_ctx.py	1970-01-01 00:00:00 +0000
+++ new/dns/_immutable_ctx.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,75 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# This implementation of the immutable decorator requires python >=
+# 3.7, and is significantly more storage efficient when making classes
+# with slots immutable.  It's also faster.
+
+import contextvars
+import inspect
+
+
+_in__init__ = contextvars.ContextVar('_immutable_in__init__', default=False)
+
+
+class _Immutable:
+    """Immutable mixin class"""
+
+    # We set slots to the empty list to say "we don't have any attributes".
+    # We do this so that if we're mixed in with a class with __slots__, we
+    # don't cause a __dict__ to be added which would waste space.
+
+    __slots__ = ()
+
+    def __setattr__(self, name, value):
+        if _in__init__.get() is not self:
+            raise TypeError("object doesn't support attribute assignment")
+        else:
+            super().__setattr__(name, value)
+
+    def __delattr__(self, name):
+        if _in__init__.get() is not self:
+            raise TypeError("object doesn't support attribute assignment")
+        else:
+            super().__delattr__(name)
+
+
+def _immutable_init(f):
+    def nf(*args, **kwargs):
+        previous = _in__init__.set(args[0])
+        try:
+            # call the actual __init__
+            f(*args, **kwargs)
+        finally:
+            _in__init__.reset(previous)
+    nf.__signature__ = inspect.signature(f)
+    return nf
+
+
+def immutable(cls):
+    if _Immutable in cls.__mro__:
+        # Some ancestor already has the mixin, so just make sure we keep
+        # following the __init__ protocol.
+        cls.__init__ = _immutable_init(cls.__init__)
+        if hasattr(cls, '__setstate__'):
+            cls.__setstate__ = _immutable_init(cls.__setstate__)
+        ncls = cls
+    else:
+        # Mixin the Immutable class and follow the __init__ protocol.
+        class ncls(_Immutable, cls):
+            # We have to do the __slots__ declaration here too!
+            __slots__ = ()
+
+            @_immutable_init
+            def __init__(self, *args, **kwargs):
+                super().__init__(*args, **kwargs)
+
+            if hasattr(cls, '__setstate__'):
+                @_immutable_init
+                def __setstate__(self, *args, **kwargs):
+                    super().__setstate__(*args, **kwargs)
+
+        # make ncls have the same name and module as cls
+        ncls.__name__ = cls.__name__
+        ncls.__qualname__ = cls.__qualname__
+        ncls.__module__ = cls.__module__
+    return ncls

=== added file 'dns/_trio_backend.py'
--- old/dns/_trio_backend.py	1970-01-01 00:00:00 +0000
+++ new/dns/_trio_backend.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,121 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+"""trio async I/O library query support"""
+
+import socket
+import trio
+import trio.socket  # type: ignore
+
+import dns._asyncbackend
+import dns.exception
+import dns.inet
+
+
+def _maybe_timeout(timeout):
+    if timeout:
+        return trio.move_on_after(timeout)
+    else:
+        return dns._asyncbackend.NullContext()
+
+
+# for brevity
+_lltuple = dns.inet.low_level_address_tuple
+
+# pylint: disable=redefined-outer-name
+
+
+class DatagramSocket(dns._asyncbackend.DatagramSocket):
+    def __init__(self, socket):
+        self.socket = socket
+        self.family = socket.family
+
+    async def sendto(self, what, destination, timeout):
+        with _maybe_timeout(timeout):
+            return await self.socket.sendto(what, destination)
+        raise dns.exception.Timeout(timeout=timeout)  # pragma: no cover
+
+    async def recvfrom(self, size, timeout):
+        with _maybe_timeout(timeout):
+            return await self.socket.recvfrom(size)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def close(self):
+        self.socket.close()
+
+    async def getpeername(self):
+        return self.socket.getpeername()
+
+    async def getsockname(self):
+        return self.socket.getsockname()
+
+
+class StreamSocket(dns._asyncbackend.StreamSocket):
+    def __init__(self, family, stream, tls=False):
+        self.family = family
+        self.stream = stream
+        self.tls = tls
+
+    async def sendall(self, what, timeout):
+        with _maybe_timeout(timeout):
+            return await self.stream.send_all(what)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def recv(self, size, timeout):
+        with _maybe_timeout(timeout):
+            return await self.stream.receive_some(size)
+        raise dns.exception.Timeout(timeout=timeout)
+
+    async def close(self):
+        await self.stream.aclose()
+
+    async def getpeername(self):
+        if self.tls:
+            return self.stream.transport_stream.socket.getpeername()
+        else:
+            return self.stream.socket.getpeername()
+
+    async def getsockname(self):
+        if self.tls:
+            return self.stream.transport_stream.socket.getsockname()
+        else:
+            return self.stream.socket.getsockname()
+
+
+class Backend(dns._asyncbackend.Backend):
+    def name(self):
+        return 'trio'
+
+    async def make_socket(self, af, socktype, proto=0, source=None,
+                          destination=None, timeout=None,
+                          ssl_context=None, server_hostname=None):
+        s = trio.socket.socket(af, socktype, proto)
+        stream = None
+        try:
+            if source:
+                await s.bind(_lltuple(source, af))
+            if socktype == socket.SOCK_STREAM:
+                with _maybe_timeout(timeout):
+                    await s.connect(_lltuple(destination, af))
+        except Exception:  # pragma: no cover
+            s.close()
+            raise
+        if socktype == socket.SOCK_DGRAM:
+            return DatagramSocket(s)
+        elif socktype == socket.SOCK_STREAM:
+            stream = trio.SocketStream(s)
+            s = None
+            tls = False
+            if ssl_context:
+                tls = True
+                try:
+                    stream = trio.SSLStream(stream, ssl_context,
+                                            server_hostname=server_hostname)
+                except Exception:  # pragma: no cover
+                    await stream.aclose()
+                    raise
+            return StreamSocket(af, stream, tls)
+        raise NotImplementedError('unsupported socket ' +
+                                  f'type {socktype}')    # pragma: no cover
+
+    async def sleep(self, interval):
+        await trio.sleep(interval)

=== added file 'dns/asyncbackend.py'
--- old/dns/asyncbackend.py	1970-01-01 00:00:00 +0000
+++ new/dns/asyncbackend.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,101 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.exception
+
+# pylint: disable=unused-import
+
+from dns._asyncbackend import Socket, DatagramSocket, \
+    StreamSocket, Backend  # noqa:
+
+# pylint: enable=unused-import
+
+_default_backend = None
+
+_backends = {}
+
+# Allow sniffio import to be disabled for testing purposes
+_no_sniffio = False
+
+class AsyncLibraryNotFoundError(dns.exception.DNSException):
+    pass
+
+
+def get_backend(name):
+    """Get the specified asychronous backend.
+
+    *name*, a ``str``, the name of the backend.  Currently the "trio",
+    "curio", and "asyncio" backends are available.
+
+    Raises NotImplementError if an unknown backend name is specified.
+    """
+    # pylint: disable=import-outside-toplevel,redefined-outer-name
+    backend = _backends.get(name)
+    if backend:
+        return backend
+    if name == 'trio':
+        import dns._trio_backend
+        backend = dns._trio_backend.Backend()
+    elif name == 'curio':
+        import dns._curio_backend
+        backend = dns._curio_backend.Backend()
+    elif name == 'asyncio':
+        import dns._asyncio_backend
+        backend = dns._asyncio_backend.Backend()
+    else:
+        raise NotImplementedError(f'unimplemented async backend {name}')
+    _backends[name] = backend
+    return backend
+
+
+def sniff():
+    """Attempt to determine the in-use asynchronous I/O library by using
+    the ``sniffio`` module if it is available.
+
+    Returns the name of the library, or raises AsyncLibraryNotFoundError
+    if the library cannot be determined.
+    """
+    # pylint: disable=import-outside-toplevel
+    try:
+        if _no_sniffio:
+            raise ImportError
+        import sniffio
+        try:
+            return sniffio.current_async_library()
+        except sniffio.AsyncLibraryNotFoundError:
+            raise AsyncLibraryNotFoundError('sniffio cannot determine ' +
+                                            'async library')
+    except ImportError:
+        import asyncio
+        try:
+            asyncio.get_running_loop()
+            return 'asyncio'
+        except RuntimeError:
+            raise AsyncLibraryNotFoundError('no async library detected')
+        except AttributeError:  # pragma: no cover
+            # we have to check current_task on 3.6
+            if not asyncio.Task.current_task():
+                raise AsyncLibraryNotFoundError('no async library detected')
+            return 'asyncio'
+
+
+def get_default_backend():
+    """Get the default backend, initializing it if necessary.
+    """
+    if _default_backend:
+        return _default_backend
+
+    return set_default_backend(sniff())
+
+
+def set_default_backend(name):
+    """Set the default backend.
+
+    It's not normally necessary to call this method, as
+    ``get_default_backend()`` will initialize the backend
+    appropriately in many cases.  If ``sniffio`` is not installed, or
+    in testing situations, this function allows the backend to be set
+    explicitly.
+    """
+    global _default_backend
+    _default_backend = get_backend(name)
+    return _default_backend

=== added file 'dns/asyncbackend.pyi'
--- old/dns/asyncbackend.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/asyncbackend.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,13 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+class Backend:
+    ...
+
+def get_backend(name: str) -> Backend:
+    ...
+def sniff() -> str:
+    ...
+def get_default_backend() -> Backend:
+    ...
+def set_default_backend(name: str) -> Backend:
+    ...

=== added file 'dns/asyncquery.py'
--- old/dns/asyncquery.py	1970-01-01 00:00:00 +0000
+++ new/dns/asyncquery.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,439 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Talk to a DNS server."""
+
+import socket
+import struct
+import time
+
+import dns.asyncbackend
+import dns.exception
+import dns.inet
+import dns.name
+import dns.message
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+
+from dns.query import _compute_times, _matches_destination, BadResponse, ssl, \
+    UDPMode
+
+
+# for brevity
+_lltuple = dns.inet.low_level_address_tuple
+
+
+def _source_tuple(af, address, port):
+    # Make a high level source tuple, or return None if address and port
+    # are both None
+    if address or port:
+        if address is None:
+            if af == socket.AF_INET:
+                address = '0.0.0.0'
+            elif af == socket.AF_INET6:
+                address = '::'
+            else:
+                raise NotImplementedError(f'unknown address family {af}')
+        return (address, port)
+    else:
+        return None
+
+
+def _timeout(expiration, now=None):
+    if expiration:
+        if not now:
+            now = time.time()
+        return max(expiration - now, 0)
+    else:
+        return None
+
+
+async def send_udp(sock, what, destination, expiration=None):
+    """Send a DNS message to the specified UDP socket.
+
+    *sock*, a ``dns.asyncbackend.DatagramSocket``.
+
+    *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+    *destination*, a destination tuple appropriate for the address family
+    of the socket, specifying where to send the query.
+
+    *expiration*, a ``float`` or ``None``, the absolute time at which
+    a timeout exception should be raised.  If ``None``, no timeout will
+    occur.
+
+    Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+    """
+
+    if isinstance(what, dns.message.Message):
+        what = what.to_wire()
+    sent_time = time.time()
+    n = await sock.sendto(what, destination, _timeout(expiration, sent_time))
+    return (n, sent_time)
+
+
+async def receive_udp(sock, destination=None, expiration=None,
+                      ignore_unexpected=False, one_rr_per_rrset=False,
+                      keyring=None, request_mac=b'', ignore_trailing=False,
+                      raise_on_truncation=False):
+    """Read a DNS message from a UDP socket.
+
+    *sock*, a ``dns.asyncbackend.DatagramSocket``.
+
+    See :py:func:`dns.query.receive_udp()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+
+    wire = b''
+    while 1:
+        (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration))
+        if _matches_destination(sock.family, from_address, destination,
+                                ignore_unexpected):
+            break
+    received_time = time.time()
+    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
+                              one_rr_per_rrset=one_rr_per_rrset,
+                              ignore_trailing=ignore_trailing,
+                              raise_on_truncation=raise_on_truncation)
+    return (r, received_time, from_address)
+
+async def udp(q, where, timeout=None, port=53, source=None, source_port=0,
+              ignore_unexpected=False, one_rr_per_rrset=False,
+              ignore_trailing=False, raise_on_truncation=False, sock=None,
+              backend=None):
+    """Return the response obtained after sending a query via UDP.
+
+    *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
+    the socket to use for the query.  If ``None``, the default, a
+    socket is created.  Note that if a socket is provided, the
+    *source*, *source_port*, and *backend* are ignored.
+
+    *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+    the default, then dnspython will use the default backend.
+
+    See :py:func:`dns.query.udp()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+    wire = q.to_wire()
+    (begin_time, expiration) = _compute_times(timeout)
+    s = None
+    # After 3.6 is no longer supported, this can use an AsyncExitStack.
+    try:
+        af = dns.inet.af_for_address(where)
+        destination = _lltuple((where, port), af)
+        if sock:
+            s = sock
+        else:
+            if not backend:
+                backend = dns.asyncbackend.get_default_backend()
+            stuple = _source_tuple(af, source, source_port)
+            if backend.datagram_connection_required():
+                dtuple = (where, port)
+            else:
+                dtuple = None
+            s = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple,
+                                          dtuple)
+        await send_udp(s, wire, destination, expiration)
+        (r, received_time, _) = await receive_udp(s, destination, expiration,
+                                                  ignore_unexpected,
+                                                  one_rr_per_rrset,
+                                                  q.keyring, q.mac,
+                                                  ignore_trailing,
+                                                  raise_on_truncation)
+        r.time = received_time - begin_time
+        if not q.is_response(r):
+            raise BadResponse
+        return r
+    finally:
+        if not sock and s:
+            await s.close()
+
+async def udp_with_fallback(q, where, timeout=None, port=53, source=None,
+                            source_port=0, ignore_unexpected=False,
+                            one_rr_per_rrset=False, ignore_trailing=False,
+                            udp_sock=None, tcp_sock=None, backend=None):
+    """Return the response to the query, trying UDP first and falling back
+    to TCP if UDP results in a truncated response.
+
+    *udp_sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``,
+    the socket to use for the UDP query.  If ``None``, the default, a
+    socket is created.  Note that if a socket is provided the *source*,
+    *source_port*, and *backend* are ignored for the UDP query.
+
+    *tcp_sock*, a ``dns.asyncbackend.StreamSocket``, or ``None``, the
+    socket to use for the TCP query.  If ``None``, the default, a
+    socket is created.  Note that if a socket is provided *where*,
+    *source*, *source_port*, and *backend*  are ignored for the TCP query.
+
+    *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+    the default, then dnspython will use the default backend.
+
+    See :py:func:`dns.query.udp_with_fallback()` for the documentation
+    of the other parameters, exceptions, and return type of this
+    method.
+    """
+    try:
+        response = await udp(q, where, timeout, port, source, source_port,
+                             ignore_unexpected, one_rr_per_rrset,
+                             ignore_trailing, True, udp_sock, backend)
+        return (response, False)
+    except dns.message.Truncated:
+        response = await tcp(q, where, timeout, port, source, source_port,
+                             one_rr_per_rrset, ignore_trailing, tcp_sock,
+                             backend)
+        return (response, True)
+
+
+async def send_tcp(sock, what, expiration=None):
+    """Send a DNS message to the specified TCP socket.
+
+    *sock*, a ``dns.asyncbackend.StreamSocket``.
+
+    See :py:func:`dns.query.send_tcp()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+
+    if isinstance(what, dns.message.Message):
+        what = what.to_wire()
+    l = len(what)
+    # copying the wire into tcpmsg is inefficient, but lets us
+    # avoid writev() or doing a short write that would get pushed
+    # onto the net
+    tcpmsg = struct.pack("!H", l) + what
+    sent_time = time.time()
+    await sock.sendall(tcpmsg, _timeout(expiration, sent_time))
+    return (len(tcpmsg), sent_time)
+
+
+async def _read_exactly(sock, count, expiration):
+    """Read the specified number of bytes from stream.  Keep trying until we
+    either get the desired amount, or we hit EOF.
+    """
+    s = b''
+    while count > 0:
+        n = await sock.recv(count, _timeout(expiration))
+        if n == b'':
+            raise EOFError
+        count = count - len(n)
+        s = s + n
+    return s
+
+
+async def receive_tcp(sock, expiration=None, one_rr_per_rrset=False,
+                      keyring=None, request_mac=b'', ignore_trailing=False):
+    """Read a DNS message from a TCP socket.
+
+    *sock*, a ``dns.asyncbackend.StreamSocket``.
+
+    See :py:func:`dns.query.receive_tcp()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+
+    ldata = await _read_exactly(sock, 2, expiration)
+    (l,) = struct.unpack("!H", ldata)
+    wire = await _read_exactly(sock, l, expiration)
+    received_time = time.time()
+    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
+                              one_rr_per_rrset=one_rr_per_rrset,
+                              ignore_trailing=ignore_trailing)
+    return (r, received_time)
+
+
+async def tcp(q, where, timeout=None, port=53, source=None, source_port=0,
+              one_rr_per_rrset=False, ignore_trailing=False, sock=None,
+              backend=None):
+    """Return the response obtained after sending a query via TCP.
+
+    *sock*, a ``dns.asyncbacket.StreamSocket``, or ``None``, the
+    socket to use for the query.  If ``None``, the default, a socket
+    is created.  Note that if a socket is provided
+    *where*, *port*, *source*, *source_port*, and *backend* are ignored.
+
+    *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+    the default, then dnspython will use the default backend.
+
+    See :py:func:`dns.query.tcp()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+
+    wire = q.to_wire()
+    (begin_time, expiration) = _compute_times(timeout)
+    s = None
+    # After 3.6 is no longer supported, this can use an AsyncExitStack.
+    try:
+        if sock:
+            # Verify that the socket is connected, as if it's not connected,
+            # it's not writable, and the polling in send_tcp() will time out or
+            # hang forever.
+            await sock.getpeername()
+            s = sock
+        else:
+            # These are simple (address, port) pairs, not
+            # family-dependent tuples you pass to lowlevel socket
+            # code.
+            af = dns.inet.af_for_address(where)
+            stuple = _source_tuple(af, source, source_port)
+            dtuple = (where, port)
+            if not backend:
+                backend = dns.asyncbackend.get_default_backend()
+            s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple,
+                                          dtuple, timeout)
+        await send_tcp(s, wire, expiration)
+        (r, received_time) = await receive_tcp(s, expiration, one_rr_per_rrset,
+                                               q.keyring, q.mac,
+                                               ignore_trailing)
+        r.time = received_time - begin_time
+        if not q.is_response(r):
+            raise BadResponse
+        return r
+    finally:
+        if not sock and s:
+            await s.close()
+
+async def tls(q, where, timeout=None, port=853, source=None, source_port=0,
+              one_rr_per_rrset=False, ignore_trailing=False, sock=None,
+              backend=None, ssl_context=None, server_hostname=None):
+    """Return the response obtained after sending a query via TLS.
+
+    *sock*, an ``asyncbackend.StreamSocket``, or ``None``, the socket
+    to use for the query.  If ``None``, the default, a socket is
+    created.  Note that if a socket is provided, it must be a
+    connected SSL stream socket, and *where*, *port*,
+    *source*, *source_port*, *backend*, *ssl_context*, and *server_hostname*
+    are ignored.
+
+    *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+    the default, then dnspython will use the default backend.
+
+    See :py:func:`dns.query.tls()` for the documentation of the other
+    parameters, exceptions, and return type of this method.
+    """
+    # After 3.6 is no longer supported, this can use an AsyncExitStack.
+    (begin_time, expiration) = _compute_times(timeout)
+    if not sock:
+        if ssl_context is None:
+            ssl_context = ssl.create_default_context()
+            if server_hostname is None:
+                ssl_context.check_hostname = False
+        else:
+            ssl_context = None
+            server_hostname = None
+        af = dns.inet.af_for_address(where)
+        stuple = _source_tuple(af, source, source_port)
+        dtuple = (where, port)
+        if not backend:
+            backend = dns.asyncbackend.get_default_backend()
+        s = await backend.make_socket(af, socket.SOCK_STREAM, 0, stuple,
+                                      dtuple, timeout, ssl_context,
+                                      server_hostname)
+    else:
+        s = sock
+    try:
+        timeout = _timeout(expiration)
+        response = await tcp(q, where, timeout, port, source, source_port,
+                             one_rr_per_rrset, ignore_trailing, s, backend)
+        end_time = time.time()
+        response.time = end_time - begin_time
+        return response
+    finally:
+        if not sock and s:
+            await s.close()
+
+async def inbound_xfr(where, txn_manager, query=None,
+                      port=53, timeout=None, lifetime=None, source=None,
+                      source_port=0, udp_mode=UDPMode.NEVER,
+                      backend=None):
+    """Conduct an inbound transfer and apply it via a transaction from the
+    txn_manager.
+
+    *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+    the default, then dnspython will use the default backend.
+
+    See :py:func:`dns.query.inbound_xfr()` for the documentation of
+    the other parameters, exceptions, and return type of this method.
+    """
+    if query is None:
+        (query, serial) = dns.xfr.make_query(txn_manager)
+    rdtype = query.question[0].rdtype
+    is_ixfr = rdtype == dns.rdatatype.IXFR
+    origin = txn_manager.from_wire_origin()
+    wire = query.to_wire()
+    af = dns.inet.af_for_address(where)
+    stuple = _source_tuple(af, source, source_port)
+    dtuple = (where, port)
+    (_, expiration) = _compute_times(lifetime)
+    retry = True
+    while retry:
+        retry = False
+        if is_ixfr and udp_mode != UDPMode.NEVER:
+            sock_type = socket.SOCK_DGRAM
+            is_udp = True
+        else:
+            sock_type = socket.SOCK_STREAM
+            is_udp = False
+        if not backend:
+            backend = dns.asyncbackend.get_default_backend()
+        s = await backend.make_socket(af, sock_type, 0, stuple, dtuple,
+                                      _timeout(expiration))
+        async with s:
+            if is_udp:
+                await s.sendto(wire, dtuple, _timeout(expiration))
+            else:
+                tcpmsg = struct.pack("!H", len(wire)) + wire
+                await s.sendall(tcpmsg, expiration)
+            with dns.xfr.Inbound(txn_manager, rdtype, serial,
+                                 is_udp) as inbound:
+                done = False
+                tsig_ctx = None
+                while not done:
+                    (_, mexpiration) = _compute_times(timeout)
+                    if mexpiration is None or \
+                       (expiration is not None and mexpiration > expiration):
+                        mexpiration = expiration
+                    if is_udp:
+                        destination = _lltuple((where, port), af)
+                        while True:
+                            timeout = _timeout(mexpiration)
+                            (rwire, from_address) = await s.recvfrom(65535,
+                                                                     timeout)
+                            if _matches_destination(af, from_address,
+                                                    destination, True):
+                                break
+                    else:
+                        ldata = await _read_exactly(s, 2, mexpiration)
+                        (l,) = struct.unpack("!H", ldata)
+                        rwire = await _read_exactly(s, l, mexpiration)
+                    is_ixfr = (rdtype == dns.rdatatype.IXFR)
+                    r = dns.message.from_wire(rwire, keyring=query.keyring,
+                                              request_mac=query.mac, xfr=True,
+                                              origin=origin, tsig_ctx=tsig_ctx,
+                                              multi=(not is_udp),
+                                              one_rr_per_rrset=is_ixfr)
+                    try:
+                        done = inbound.process_message(r)
+                    except dns.xfr.UseTCP:
+                        assert is_udp  # should not happen if we used TCP!
+                        if udp_mode == UDPMode.ONLY:
+                            raise
+                        done = True
+                        retry = True
+                        udp_mode = UDPMode.NEVER
+                        continue
+                    tsig_ctx = r.tsig_ctx
+                if not retry and query.keyring and not r.had_tsig:
+                    raise dns.exception.FormError("missing TSIG")

=== added file 'dns/asyncquery.pyi'
--- old/dns/asyncquery.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/asyncquery.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,43 @@
+from typing import Optional, Union, Dict, Generator, Any
+from . import tsig, rdatatype, rdataclass, name, message, asyncbackend
+
+# If the ssl import works, then
+#
+#    error: Name 'ssl' already defined (by an import)
+#
+# is expected and can be ignored.
+try:
+    import ssl
+except ImportError:
+    class ssl:    # type: ignore
+        SSLContext : Dict = {}
+
+async def udp(q : message.Message, where : str,
+              timeout : Optional[float] = None, port=53,
+              source : Optional[str] = None, source_port : Optional[int] = 0,
+              ignore_unexpected : Optional[bool] = False,
+              one_rr_per_rrset : Optional[bool] = False,
+              ignore_trailing : Optional[bool] = False,
+              sock : Optional[asyncbackend.DatagramSocket] = None,
+              backend : Optional[asyncbackend.Backend]) -> message.Message:
+    pass
+
+async def tcp(q : message.Message, where : str, timeout : float = None, port=53,
+        af : Optional[int] = None, source : Optional[str] = None,
+        source_port : Optional[int] = 0,
+        one_rr_per_rrset : Optional[bool] = False,
+        ignore_trailing : Optional[bool] = False,
+        sock : Optional[asyncbackend.StreamSocket] = None,
+        backend : Optional[asyncbackend.Backend]) -> message.Message:
+    pass
+
+async def tls(q : message.Message, where : str,
+              timeout : Optional[float] = None, port=53,
+              source : Optional[str] = None, source_port : Optional[int] = 0,
+              one_rr_per_rrset : Optional[bool] = False,
+              ignore_trailing : Optional[bool] = False,
+              sock : Optional[asyncbackend.StreamSocket] = None,
+              backend : Optional[asyncbackend.Backend],
+              ssl_context: Optional[ssl.SSLContext] = None,
+              server_hostname: Optional[str] = None) -> message.Message:
+    pass

=== added file 'dns/asyncresolver.py'
--- old/dns/asyncresolver.py	1970-01-01 00:00:00 +0000
+++ new/dns/asyncresolver.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,230 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Asynchronous DNS stub resolver."""
+
+import time
+
+import dns.asyncbackend
+import dns.asyncquery
+import dns.exception
+import dns.query
+import dns.resolver
+
+# import some resolver symbols for brevity
+from dns.resolver import NXDOMAIN, NoAnswer, NotAbsolute, NoRootSOA
+
+
+# for indentation purposes below
+_udp = dns.asyncquery.udp
+_tcp = dns.asyncquery.tcp
+
+
+class Resolver(dns.resolver.BaseResolver):
+    """Asynchronous DNS stub resolver."""
+
+    async def resolve(self, qname, rdtype=dns.rdatatype.A,
+                      rdclass=dns.rdataclass.IN,
+                      tcp=False, source=None, raise_on_no_answer=True,
+                      source_port=0, lifetime=None, search=None,
+                      backend=None):
+        """Query nameservers asynchronously to find the answer to the question.
+
+        *backend*, a ``dns.asyncbackend.Backend``, or ``None``.  If ``None``,
+        the default, then dnspython will use the default backend.
+
+        See :py:func:`dns.resolver.Resolver.resolve()` for the
+        documentation of the other parameters, exceptions, and return
+        type of this method.
+        """
+
+        resolution = dns.resolver._Resolution(self, qname, rdtype, rdclass, tcp,
+                                              raise_on_no_answer, search)
+        if not backend:
+            backend = dns.asyncbackend.get_default_backend()
+        start = time.time()
+        while True:
+            (request, answer) = resolution.next_request()
+            # Note we need to say "if answer is not None" and not just
+            # "if answer" because answer implements __len__, and python
+            # will call that.  We want to return if we have an answer
+            # object, including in cases where its length is 0.
+            if answer is not None:
+                # cache hit!
+                return answer
+            done = False
+            while not done:
+                (nameserver, port, tcp, backoff) = resolution.next_nameserver()
+                if backoff:
+                    await backend.sleep(backoff)
+                timeout = self._compute_timeout(start, lifetime)
+                try:
+                    if dns.inet.is_address(nameserver):
+                        if tcp:
+                            response = await _tcp(request, nameserver,
+                                                  timeout, port,
+                                                  source, source_port,
+                                                  backend=backend)
+                        else:
+                            response = await _udp(request, nameserver,
+                                                  timeout, port,
+                                                  source, source_port,
+                                                  raise_on_truncation=True,
+                                                  backend=backend)
+                    else:
+                        # We don't do DoH yet.
+                        raise NotImplementedError
+                except Exception as ex:
+                    (_, done) = resolution.query_result(None, ex)
+                    continue
+                (answer, done) = resolution.query_result(response, None)
+                # Note we need to say "if answer is not None" and not just
+                # "if answer" because answer implements __len__, and python
+                # will call that.  We want to return if we have an answer
+                # object, including in cases where its length is 0.
+                if answer is not None:
+                    return answer
+
+    async def resolve_address(self, ipaddr, *args, **kwargs):
+        """Use an asynchronous resolver to run a reverse query for PTR
+        records.
+
+        This utilizes the resolve() method to perform a PTR lookup on the
+        specified IP address.
+
+        *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get
+        the PTR record for.
+
+        All other arguments that can be passed to the resolve() function
+        except for rdtype and rdclass are also supported by this
+        function.
+
+        """
+
+        return await self.resolve(dns.reversename.from_address(ipaddr),
+                                  rdtype=dns.rdatatype.PTR,
+                                  rdclass=dns.rdataclass.IN,
+                                  *args, **kwargs)
+
+    # pylint: disable=redefined-outer-name
+
+    async def canonical_name(self, name):
+        """Determine the canonical name of *name*.
+
+        The canonical name is the name the resolver uses for queries
+        after all CNAME and DNAME renamings have been applied.
+
+        *name*, a ``dns.name.Name`` or ``str``, the query name.
+
+        This method can raise any exception that ``resolve()`` can
+        raise, other than ``dns.resolver.NoAnswer`` and
+        ``dns.resolver.NXDOMAIN``.
+
+        Returns a ``dns.name.Name``.
+        """
+        try:
+            answer = await self.resolve(name, raise_on_no_answer=False)
+            canonical_name = answer.canonical_name
+        except dns.resolver.NXDOMAIN as e:
+            canonical_name = e.canonical_name
+        return canonical_name
+
+
+default_resolver = None
+
+
+def get_default_resolver():
+    """Get the default asynchronous resolver, initializing it if necessary."""
+    if default_resolver is None:
+        reset_default_resolver()
+    return default_resolver
+
+
+def reset_default_resolver():
+    """Re-initialize default asynchronous resolver.
+
+    Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX
+    systems) will be re-read immediately.
+    """
+
+    global default_resolver
+    default_resolver = Resolver()
+
+
+async def resolve(qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
+                  tcp=False, source=None, raise_on_no_answer=True,
+                  source_port=0, lifetime=None, search=None, backend=None):
+    """Query nameservers asynchronously to find the answer to the question.
+
+    This is a convenience function that uses the default resolver
+    object to make the query.
+
+    See :py:func:`dns.asyncresolver.Resolver.resolve` for more
+    information on the parameters.
+    """
+
+    return await get_default_resolver().resolve(qname, rdtype, rdclass, tcp,
+                                                source, raise_on_no_answer,
+                                                source_port, lifetime, search,
+                                                backend)
+
+
+async def resolve_address(ipaddr, *args, **kwargs):
+    """Use a resolver to run a reverse query for PTR records.
+
+    See :py:func:`dns.asyncresolver.Resolver.resolve_address` for more
+    information on the parameters.
+    """
+
+    return await get_default_resolver().resolve_address(ipaddr, *args, **kwargs)
+
+async def canonical_name(name):
+    """Determine the canonical name of *name*.
+
+    See :py:func:`dns.resolver.Resolver.canonical_name` for more
+    information on the parameters and possible exceptions.
+    """
+
+    return await get_default_resolver().canonical_name(name)
+
+async def zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False,
+                        resolver=None, backend=None):
+    """Find the name of the zone which contains the specified name.
+
+    See :py:func:`dns.resolver.Resolver.zone_for_name` for more
+    information on the parameters and possible exceptions.
+    """
+
+    if isinstance(name, str):
+        name = dns.name.from_text(name, dns.name.root)
+    if resolver is None:
+        resolver = get_default_resolver()
+    if not name.is_absolute():
+        raise NotAbsolute(name)
+    while True:
+        try:
+            answer = await resolver.resolve(name, dns.rdatatype.SOA, rdclass,
+                                            tcp, backend=backend)
+            if answer.rrset.name == name:
+                return name
+            # otherwise we were CNAMEd or DNAMEd and need to look higher
+        except (NXDOMAIN, NoAnswer):
+            pass
+        try:
+            name = name.parent()
+        except dns.name.NoParent:  # pragma: no cover
+            raise NoRootSOA

=== added file 'dns/asyncresolver.pyi'
--- old/dns/asyncresolver.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/asyncresolver.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,26 @@
+from typing import Union, Optional, List, Any, Dict
+from . import exception, rdataclass, name, rdatatype, asyncbackend
+
+async def resolve(qname : str, rdtype : Union[int,str] = 0,
+                  rdclass : Union[int,str] = 0,
+                  tcp=False, source=None, raise_on_no_answer=True,
+                  source_port=0, lifetime : Optional[float]=None,
+                  search : Optional[bool]=None,
+                  backend : Optional[asyncbackend.Backend]=None):
+    ...
+async def resolve_address(self, ipaddr: str,
+                          *args: Any, **kwargs: Optional[Dict]):
+    ...
+
+class Resolver:
+    def __init__(self, filename : Optional[str] = '/etc/resolv.conf',
+                 configure : Optional[bool] = True):
+        self.nameservers : List[str]
+    async def resolve(self, qname : str, rdtype : Union[int,str] = rdatatype.A,
+                      rdclass : Union[int,str] = rdataclass.IN,
+                      tcp : bool = False, source : Optional[str] = None,
+                      raise_on_no_answer=True, source_port : int = 0,
+                      lifetime : Optional[float]=None,
+                      search : Optional[bool]=None,
+                      backend : Optional[asyncbackend.Backend]=None):
+        ...

=== added file 'dns/dnssec.py'
--- old/dns/dnssec.py	1970-01-01 00:00:00 +0000
+++ new/dns/dnssec.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,594 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Common DNSSEC-related functions and constants."""
+
+import hashlib
+import struct
+import time
+import base64
+
+import dns.enum
+import dns.exception
+import dns.name
+import dns.node
+import dns.rdataset
+import dns.rdata
+import dns.rdatatype
+import dns.rdataclass
+
+
+class UnsupportedAlgorithm(dns.exception.DNSException):
+    """The DNSSEC algorithm is not supported."""
+
+
+class ValidationFailure(dns.exception.DNSException):
+    """The DNSSEC signature is invalid."""
+
+
+class Algorithm(dns.enum.IntEnum):
+    RSAMD5 = 1
+    DH = 2
+    DSA = 3
+    ECC = 4
+    RSASHA1 = 5
+    DSANSEC3SHA1 = 6
+    RSASHA1NSEC3SHA1 = 7
+    RSASHA256 = 8
+    RSASHA512 = 10
+    ECCGOST = 12
+    ECDSAP256SHA256 = 13
+    ECDSAP384SHA384 = 14
+    ED25519 = 15
+    ED448 = 16
+    INDIRECT = 252
+    PRIVATEDNS = 253
+    PRIVATEOID = 254
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+
+def algorithm_from_text(text):
+    """Convert text into a DNSSEC algorithm value.
+
+    *text*, a ``str``, the text to convert to into an algorithm value.
+
+    Returns an ``int``.
+    """
+
+    return Algorithm.from_text(text)
+
+
+def algorithm_to_text(value):
+    """Convert a DNSSEC algorithm value to text
+
+    *value*, an ``int`` a DNSSEC algorithm.
+
+    Returns a ``str``, the name of a DNSSEC algorithm.
+    """
+
+    return Algorithm.to_text(value)
+
+
+def key_id(key):
+    """Return the key id (a 16-bit number) for the specified key.
+
+    *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY``
+
+    Returns an ``int`` between 0 and 65535
+    """
+
+    rdata = key.to_wire()
+    if key.algorithm == Algorithm.RSAMD5:
+        return (rdata[-3] << 8) + rdata[-2]
+    else:
+        total = 0
+        for i in range(len(rdata) // 2):
+            total += (rdata[2 * i] << 8) + \
+                rdata[2 * i + 1]
+        if len(rdata) % 2 != 0:
+            total += rdata[len(rdata) - 1] << 8
+        total += ((total >> 16) & 0xffff)
+        return total & 0xffff
+
+class DSDigest(dns.enum.IntEnum):
+    """DNSSEC Delgation Signer Digest Algorithm"""
+
+    SHA1 = 1
+    SHA256 = 2
+    SHA384 = 4
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+
+def make_ds(name, key, algorithm, origin=None):
+    """Create a DS record for a DNSSEC key.
+
+    *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record.
+
+    *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY``, the key the DS is about.
+
+    *algorithm*, a ``str`` or ``int`` specifying the hash algorithm.
+    The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case
+    does not matter for these strings.
+
+    *origin*, a ``dns.name.Name`` or ``None``.  If `key` is a relative name,
+    then it will be made absolute using the specified origin.
+
+    Raises ``UnsupportedAlgorithm`` if the algorithm is unknown.
+
+    Returns a ``dns.rdtypes.ANY.DS.DS``
+    """
+
+    try:
+        if isinstance(algorithm, str):
+            algorithm = DSDigest[algorithm.upper()]
+    except Exception:
+        raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
+
+    if algorithm == DSDigest.SHA1:
+        dshash = hashlib.sha1()
+    elif algorithm == DSDigest.SHA256:
+        dshash = hashlib.sha256()
+    elif algorithm == DSDigest.SHA384:
+        dshash = hashlib.sha384()
+    else:
+        raise UnsupportedAlgorithm('unsupported algorithm "%s"' % algorithm)
+
+    if isinstance(name, str):
+        name = dns.name.from_text(name, origin)
+    dshash.update(name.canonicalize().to_wire())
+    dshash.update(key.to_wire(origin=origin))
+    digest = dshash.digest()
+
+    dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, algorithm) + \
+        digest
+    return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0,
+                               len(dsrdata))
+
+
+def _find_candidate_keys(keys, rrsig):
+    value = keys.get(rrsig.signer)
+    if isinstance(value, dns.node.Node):
+        rdataset = value.get_rdataset(dns.rdataclass.IN, dns.rdatatype.DNSKEY)
+    else:
+        rdataset = value
+    if rdataset is None:
+        return None
+    return [rd for rd in rdataset if
+            rd.algorithm == rrsig.algorithm and key_id(rd) == rrsig.key_tag]
+
+
+def _is_rsa(algorithm):
+    return algorithm in (Algorithm.RSAMD5, Algorithm.RSASHA1,
+                         Algorithm.RSASHA1NSEC3SHA1, Algorithm.RSASHA256,
+                         Algorithm.RSASHA512)
+
+
+def _is_dsa(algorithm):
+    return algorithm in (Algorithm.DSA, Algorithm.DSANSEC3SHA1)
+
+
+def _is_ecdsa(algorithm):
+    return algorithm in (Algorithm.ECDSAP256SHA256, Algorithm.ECDSAP384SHA384)
+
+
+def _is_eddsa(algorithm):
+    return algorithm in (Algorithm.ED25519, Algorithm.ED448)
+
+
+def _is_gost(algorithm):
+    return algorithm == Algorithm.ECCGOST
+
+
+def _is_md5(algorithm):
+    return algorithm == Algorithm.RSAMD5
+
+
+def _is_sha1(algorithm):
+    return algorithm in (Algorithm.DSA, Algorithm.RSASHA1,
+                         Algorithm.DSANSEC3SHA1, Algorithm.RSASHA1NSEC3SHA1)
+
+
+def _is_sha256(algorithm):
+    return algorithm in (Algorithm.RSASHA256, Algorithm.ECDSAP256SHA256)
+
+
+def _is_sha384(algorithm):
+    return algorithm == Algorithm.ECDSAP384SHA384
+
+
+def _is_sha512(algorithm):
+    return algorithm == Algorithm.RSASHA512
+
+
+def _make_hash(algorithm):
+    if _is_md5(algorithm):
+        return hashes.MD5()
+    if _is_sha1(algorithm):
+        return hashes.SHA1()
+    if _is_sha256(algorithm):
+        return hashes.SHA256()
+    if _is_sha384(algorithm):
+        return hashes.SHA384()
+    if _is_sha512(algorithm):
+        return hashes.SHA512()
+    if algorithm == Algorithm.ED25519:
+        return hashes.SHA512()
+    if algorithm == Algorithm.ED448:
+        return hashes.SHAKE256(114)
+
+    raise ValidationFailure('unknown hash for algorithm %u' % algorithm)
+
+
+def _bytes_to_long(b):
+    return int.from_bytes(b, 'big')
+
+
+def _validate_signature(sig, data, key, chosen_hash):
+    if _is_rsa(key.algorithm):
+        keyptr = key.key
+        (bytes_,) = struct.unpack('!B', keyptr[0:1])
+        keyptr = keyptr[1:]
+        if bytes_ == 0:
+            (bytes_,) = struct.unpack('!H', keyptr[0:2])
+            keyptr = keyptr[2:]
+        rsa_e = keyptr[0:bytes_]
+        rsa_n = keyptr[bytes_:]
+        try:
+            public_key = rsa.RSAPublicNumbers(
+                _bytes_to_long(rsa_e),
+                _bytes_to_long(rsa_n)).public_key(default_backend())
+        except ValueError:
+            raise ValidationFailure('invalid public key')
+        public_key.verify(sig, data, padding.PKCS1v15(), chosen_hash)
+    elif _is_dsa(key.algorithm):
+        keyptr = key.key
+        (t,) = struct.unpack('!B', keyptr[0:1])
+        keyptr = keyptr[1:]
+        octets = 64 + t * 8
+        dsa_q = keyptr[0:20]
+        keyptr = keyptr[20:]
+        dsa_p = keyptr[0:octets]
+        keyptr = keyptr[octets:]
+        dsa_g = keyptr[0:octets]
+        keyptr = keyptr[octets:]
+        dsa_y = keyptr[0:octets]
+        try:
+            public_key = dsa.DSAPublicNumbers(
+                _bytes_to_long(dsa_y),
+                dsa.DSAParameterNumbers(
+                    _bytes_to_long(dsa_p),
+                    _bytes_to_long(dsa_q),
+                    _bytes_to_long(dsa_g))).public_key(default_backend())
+        except ValueError:
+            raise ValidationFailure('invalid public key')
+        public_key.verify(sig, data, chosen_hash)
+    elif _is_ecdsa(key.algorithm):
+        keyptr = key.key
+        if key.algorithm == Algorithm.ECDSAP256SHA256:
+            curve = ec.SECP256R1()
+            octets = 32
+        else:
+            curve = ec.SECP384R1()
+            octets = 48
+        ecdsa_x = keyptr[0:octets]
+        ecdsa_y = keyptr[octets:octets * 2]
+        try:
+            public_key = ec.EllipticCurvePublicNumbers(
+                curve=curve,
+                x=_bytes_to_long(ecdsa_x),
+                y=_bytes_to_long(ecdsa_y)).public_key(default_backend())
+        except ValueError:
+            raise ValidationFailure('invalid public key')
+        public_key.verify(sig, data, ec.ECDSA(chosen_hash))
+    elif _is_eddsa(key.algorithm):
+        keyptr = key.key
+        if key.algorithm == Algorithm.ED25519:
+            loader = ed25519.Ed25519PublicKey
+        else:
+            loader = ed448.Ed448PublicKey
+        try:
+            public_key = loader.from_public_bytes(keyptr)
+        except ValueError:
+            raise ValidationFailure('invalid public key')
+        public_key.verify(sig, data)
+    elif _is_gost(key.algorithm):
+        raise UnsupportedAlgorithm(
+            'algorithm "%s" not supported by dnspython' %
+            algorithm_to_text(key.algorithm))
+    else:
+        raise ValidationFailure('unknown algorithm %u' % key.algorithm)
+
+
+def _validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
+    """Validate an RRset against a single signature rdata, throwing an
+    exception if validation is not successful.
+
+    *rrset*, the RRset to validate.  This can be a
+    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+    tuple.
+
+    *rrsig*, a ``dns.rdata.Rdata``, the signature to validate.
+
+    *keys*, the key dictionary, used to find the DNSKEY associated
+    with a given name.  The dictionary is keyed by a
+    ``dns.name.Name``, and has ``dns.node.Node`` or
+    ``dns.rdataset.Rdataset`` values.
+
+    *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative
+    names.
+
+    *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to
+    use as the current time when validating.  If ``None``, the actual current
+    time is used.
+
+    Raises ``ValidationFailure`` if the signature is expired, not yet valid,
+    the public key is invalid, the algorithm is unknown, the verification
+    fails, etc.
+
+    Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by
+    dnspython but not implemented.
+    """
+
+    if isinstance(origin, str):
+        origin = dns.name.from_text(origin, dns.name.root)
+
+    candidate_keys = _find_candidate_keys(keys, rrsig)
+    if candidate_keys is None:
+        raise ValidationFailure('unknown key')
+
+    # For convenience, allow the rrset to be specified as a (name,
+    # rdataset) tuple as well as a proper rrset
+    if isinstance(rrset, tuple):
+        rrname = rrset[0]
+        rdataset = rrset[1]
+    else:
+        rrname = rrset.name
+        rdataset = rrset
+
+    if now is None:
+        now = time.time()
+    if rrsig.expiration < now:
+        raise ValidationFailure('expired')
+    if rrsig.inception > now:
+        raise ValidationFailure('not yet valid')
+
+    if _is_dsa(rrsig.algorithm):
+        sig_r = rrsig.signature[1:21]
+        sig_s = rrsig.signature[21:]
+        sig = utils.encode_dss_signature(_bytes_to_long(sig_r),
+                                         _bytes_to_long(sig_s))
+    elif _is_ecdsa(rrsig.algorithm):
+        if rrsig.algorithm == Algorithm.ECDSAP256SHA256:
+            octets = 32
+        else:
+            octets = 48
+        sig_r = rrsig.signature[0:octets]
+        sig_s = rrsig.signature[octets:]
+        sig = utils.encode_dss_signature(_bytes_to_long(sig_r),
+                                         _bytes_to_long(sig_s))
+    else:
+        sig = rrsig.signature
+
+    data = b''
+    data += rrsig.to_wire(origin=origin)[:18]
+    data += rrsig.signer.to_digestable(origin)
+
+    # Derelativize the name before considering labels.
+    rrname = rrname.derelativize(origin)
+
+    if len(rrname) - 1 < rrsig.labels:
+        raise ValidationFailure('owner name longer than RRSIG labels')
+    elif rrsig.labels < len(rrname) - 1:
+        suffix = rrname.split(rrsig.labels + 1)[1]
+        rrname = dns.name.from_text('*', suffix)
+    rrnamebuf = rrname.to_digestable()
+    rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
+                          rrsig.original_ttl)
+    for rr in sorted(rdataset):
+        data += rrnamebuf
+        data += rrfixed
+        rrdata = rr.to_digestable(origin)
+        rrlen = struct.pack('!H', len(rrdata))
+        data += rrlen
+        data += rrdata
+
+    chosen_hash = _make_hash(rrsig.algorithm)
+
+    for candidate_key in candidate_keys:
+        try:
+            _validate_signature(sig, data, candidate_key, chosen_hash)
+            return
+        except (InvalidSignature, ValidationFailure):
+            # this happens on an individual validation failure
+            continue
+    # nothing verified -- raise failure:
+    raise ValidationFailure('verify failure')
+
+
+def _validate(rrset, rrsigset, keys, origin=None, now=None):
+    """Validate an RRset against a signature RRset, throwing an exception
+    if none of the signatures validate.
+
+    *rrset*, the RRset to validate.  This can be a
+    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+    tuple.
+
+    *rrsigset*, the signature RRset.  This can be a
+    ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``)
+    tuple.
+
+    *keys*, the key dictionary, used to find the DNSKEY associated
+    with a given name.  The dictionary is keyed by a
+    ``dns.name.Name``, and has ``dns.node.Node`` or
+    ``dns.rdataset.Rdataset`` values.
+
+    *origin*, a ``dns.name.Name``, the origin to use for relative names;
+    defaults to None.
+
+    *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to
+    use as the current time when validating.  If ``None``, the actual current
+    time is used.
+
+    Raises ``ValidationFailure`` if the signature is expired, not yet valid,
+    the public key is invalid, the algorithm is unknown, the verification
+    fails, etc.
+    """
+
+    if isinstance(origin, str):
+        origin = dns.name.from_text(origin, dns.name.root)
+
+    if isinstance(rrset, tuple):
+        rrname = rrset[0]
+    else:
+        rrname = rrset.name
+
+    if isinstance(rrsigset, tuple):
+        rrsigname = rrsigset[0]
+        rrsigrdataset = rrsigset[1]
+    else:
+        rrsigname = rrsigset.name
+        rrsigrdataset = rrsigset
+
+    rrname = rrname.choose_relativity(origin)
+    rrsigname = rrsigname.choose_relativity(origin)
+    if rrname != rrsigname:
+        raise ValidationFailure("owner names do not match")
+
+    for rrsig in rrsigrdataset:
+        try:
+            _validate_rrsig(rrset, rrsig, keys, origin, now)
+            return
+        except (ValidationFailure, UnsupportedAlgorithm):
+            pass
+    raise ValidationFailure("no RRSIGs validated")
+
+
+class NSEC3Hash(dns.enum.IntEnum):
+    """NSEC3 hash algorithm"""
+
+    SHA1 = 1
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+def nsec3_hash(domain, salt, iterations, algorithm):
+    """
+    Calculate the NSEC3 hash, according to
+    https://tools.ietf.org/html/rfc5155#section-5
+
+    *domain*, a ``dns.name.Name`` or ``str``, the name to hash.
+
+    *salt*, a ``str``, ``bytes``, or ``None``, the hash salt.  If a
+    string, it is decoded as a hex string.
+
+    *iterations*, an ``int``, the number of iterations.
+
+    *algorithm*, a ``str`` or ``int``, the hash algorithm.
+    The only defined algorithm is SHA1.
+
+    Returns a ``str``, the encoded NSEC3 hash.
+    """
+
+    b32_conversion = str.maketrans(
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV"
+    )
+
+    try:
+        if isinstance(algorithm, str):
+            algorithm = NSEC3Hash[algorithm.upper()]
+    except Exception:
+        raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
+
+    if algorithm != NSEC3Hash.SHA1:
+        raise ValueError("Wrong hash algorithm (only SHA1 is supported)")
+
+    salt_encoded = salt
+    if salt is None:
+        salt_encoded = b''
+    elif isinstance(salt, str):
+        if len(salt) % 2 == 0:
+            salt_encoded = bytes.fromhex(salt)
+        else:
+            raise ValueError("Invalid salt length")
+
+    if not isinstance(domain, dns.name.Name):
+        domain = dns.name.from_text(domain)
+    domain_encoded = domain.canonicalize().to_wire()
+
+    digest = hashlib.sha1(domain_encoded + salt_encoded).digest()
+    for _ in range(iterations):
+        digest = hashlib.sha1(digest + salt_encoded).digest()
+
+    output = base64.b32encode(digest).decode("utf-8")
+    output = output.translate(b32_conversion)
+
+    return output
+
+
+def _need_pyca(*args, **kwargs):
+    raise ImportError("DNSSEC validation requires " +
+                      "python cryptography")  # pragma: no cover
+
+
+try:
+    from cryptography.exceptions import InvalidSignature
+    from cryptography.hazmat.backends import default_backend
+    from cryptography.hazmat.primitives import hashes
+    from cryptography.hazmat.primitives.asymmetric import padding
+    from cryptography.hazmat.primitives.asymmetric import utils
+    from cryptography.hazmat.primitives.asymmetric import dsa
+    from cryptography.hazmat.primitives.asymmetric import ec
+    from cryptography.hazmat.primitives.asymmetric import ed25519
+    from cryptography.hazmat.primitives.asymmetric import ed448
+    from cryptography.hazmat.primitives.asymmetric import rsa
+except ImportError:  # pragma: no cover
+    validate = _need_pyca
+    validate_rrsig = _need_pyca
+    _have_pyca = False
+else:
+    validate = _validate                # type: ignore
+    validate_rrsig = _validate_rrsig    # type: ignore
+    _have_pyca = True
+
+### BEGIN generated Algorithm constants
+
+RSAMD5 = Algorithm.RSAMD5
+DH = Algorithm.DH
+DSA = Algorithm.DSA
+ECC = Algorithm.ECC
+RSASHA1 = Algorithm.RSASHA1
+DSANSEC3SHA1 = Algorithm.DSANSEC3SHA1
+RSASHA1NSEC3SHA1 = Algorithm.RSASHA1NSEC3SHA1
+RSASHA256 = Algorithm.RSASHA256
+RSASHA512 = Algorithm.RSASHA512
+ECCGOST = Algorithm.ECCGOST
+ECDSAP256SHA256 = Algorithm.ECDSAP256SHA256
+ECDSAP384SHA384 = Algorithm.ECDSAP384SHA384
+ED25519 = Algorithm.ED25519
+ED448 = Algorithm.ED448
+INDIRECT = Algorithm.INDIRECT
+PRIVATEDNS = Algorithm.PRIVATEDNS
+PRIVATEOID = Algorithm.PRIVATEOID
+
+### END generated Algorithm constants

=== added file 'dns/dnssec.pyi'
--- old/dns/dnssec.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/dnssec.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,21 @@
+from typing import Union, Dict, Tuple, Optional
+from . import rdataset, rrset, exception, name, rdtypes, rdata, node
+import dns.rdtypes.ANY.DS as DS
+import dns.rdtypes.ANY.DNSKEY as DNSKEY
+
+_have_pyca : bool
+
+def validate_rrsig(rrset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsig : rdata.Rdata, keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin : Optional[name.Name] = None, now : Optional[int] = None) -> None:
+    ...
+
+def validate(rrset: Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], rrsigset : Union[Tuple[name.Name, rdataset.Rdataset], rrset.RRset], keys : Dict[name.Name, Union[node.Node, rdataset.Rdataset]], origin=None, now=None) -> None:
+    ...
+
+class ValidationFailure(exception.DNSException):
+    ...
+
+def make_ds(name : name.Name, key : DNSKEY.DNSKEY, algorithm : str, origin : Optional[name.Name] = None) -> DS.DS:
+    ...
+
+def nsec3_hash(domain: str, salt: Optional[Union[str, bytes]], iterations: int, algo: int) -> str:
+    ...

=== added file 'dns/e164.py'
--- old/dns/e164.py	1970-01-01 00:00:00 +0000
+++ new/dns/e164.py	2020-08-06 23:26:37 +0000
@@ -0,0 +1,104 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS E.164 helpers."""
+
+import dns.exception
+import dns.name
+import dns.resolver
+
+#: The public E.164 domain.
+public_enum_domain = dns.name.from_text('e164.arpa.')
+
+
+def from_e164(text, origin=public_enum_domain):
+    """Convert an E.164 number in textual form into a Name object whose
+    value is the ENUM domain name for that number.
+
+    Non-digits in the text are ignored, i.e. "16505551212",
+    "+1.650.555.1212" and "1 (650) 555-1212" are all the same.
+
+    *text*, a ``str``, is an E.164 number in textual form.
+
+    *origin*, a ``dns.name.Name``, the domain in which the number
+    should be constructed.  The default is ``e164.arpa.``.
+
+    Returns a ``dns.name.Name``.
+    """
+
+    parts = [d for d in text if d.isdigit()]
+    parts.reverse()
+    return dns.name.from_text('.'.join(parts), origin=origin)
+
+
+def to_e164(name, origin=public_enum_domain, want_plus_prefix=True):
+    """Convert an ENUM domain name into an E.164 number.
+
+    Note that dnspython does not have any information about preferred
+    number formats within national numbering plans, so all numbers are
+    emitted as a simple string of digits, prefixed by a '+' (unless
+    *want_plus_prefix* is ``False``).
+
+    *name* is a ``dns.name.Name``, the ENUM domain name.
+
+    *origin* is a ``dns.name.Name``, a domain containing the ENUM
+    domain name.  The name is relativized to this domain before being
+    converted to text.  If ``None``, no relativization is done.
+
+    *want_plus_prefix* is a ``bool``.  If True, add a '+' to the beginning of
+    the returned number.
+
+    Returns a ``str``.
+
+    """
+    if origin is not None:
+        name = name.relativize(origin)
+    dlabels = [d for d in name.labels if d.isdigit() and len(d) == 1]
+    if len(dlabels) != len(name.labels):
+        raise dns.exception.SyntaxError('non-digit labels in ENUM domain name')
+    dlabels.reverse()
+    text = b''.join(dlabels)
+    if want_plus_prefix:
+        text = b'+' + text
+    return text.decode()
+
+
+def query(number, domains, resolver=None):
+    """Look for NAPTR RRs for the specified number in the specified domains.
+
+    e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.'])
+
+    *number*, a ``str`` is the number to look for.
+
+    *domains* is an iterable containing ``dns.name.Name`` values.
+
+    *resolver*, a ``dns.resolver.Resolver``, is the resolver to use.  If
+    ``None``, the default resolver is used.
+    """
+
+    if resolver is None:
+        resolver = dns.resolver.get_default_resolver()
+    e_nx = dns.resolver.NXDOMAIN()
+    for domain in domains:
+        if isinstance(domain, str):
+            domain = dns.name.from_text(domain)
+        qname = dns.e164.from_e164(number, domain)
+        try:
+            return resolver.resolve(qname, 'NAPTR')
+        except dns.resolver.NXDOMAIN as e:
+            e_nx += e
+    raise e_nx

=== added file 'dns/e164.pyi'
--- old/dns/e164.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/e164.pyi	2018-12-23 00:54:24 +0000
@@ -0,0 +1,10 @@
+from typing import Optional, Iterable
+from . import name, resolver
+def from_e164(text : str, origin=name.Name(".")) -> name.Name:
+    ...
+
+def to_e164(name : name.Name, origin : Optional[name.Name] = None, want_plus_prefix=True) -> str:
+    ...
+
+def query(number : str, domains : Iterable[str], resolver : Optional[resolver.Resolver] = None) -> resolver.Answer:
+    ...

=== added file 'dns/edns.py'
--- old/dns/edns.py	1970-01-01 00:00:00 +0000
+++ new/dns/edns.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,376 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""EDNS Options"""
+
+import math
+import socket
+import struct
+
+import dns.enum
+import dns.inet
+import dns.rdata
+
+
+class OptionType(dns.enum.IntEnum):
+    #: NSID
+    NSID = 3
+    #: DAU
+    DAU = 5
+    #: DHU
+    DHU = 6
+    #: N3U
+    N3U = 7
+    #: ECS (client-subnet)
+    ECS = 8
+    #: EXPIRE
+    EXPIRE = 9
+    #: COOKIE
+    COOKIE = 10
+    #: KEEPALIVE
+    KEEPALIVE = 11
+    #: PADDING
+    PADDING = 12
+    #: CHAIN
+    CHAIN = 13
+
+    @classmethod
+    def _maximum(cls):
+        return 65535
+
+
+class Option:
+
+    """Base class for all EDNS option types."""
+
+    def __init__(self, otype):
+        """Initialize an option.
+
+        *otype*, an ``int``, is the option type.
+        """
+        self.otype = OptionType.make(otype)
+
+    def to_wire(self, file=None):
+        """Convert an option to wire format.
+
+        Returns a ``bytes`` or ``None``.
+
+        """
+        raise NotImplementedError  # pragma: no cover
+
+    @classmethod
+    def from_wire_parser(cls, otype, parser):
+        """Build an EDNS option object from wire format.
+
+        *otype*, an ``int``, is the option type.
+
+        *parser*, a ``dns.wire.Parser``, the parser, which should be
+        restructed to the option length.
+
+        Returns a ``dns.edns.Option``.
+        """
+        raise NotImplementedError  # pragma: no cover
+
+    def _cmp(self, other):
+        """Compare an EDNS option with another option of the same type.
+
+        Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*.
+        """
+        wire = self.to_wire()
+        owire = other.to_wire()
+        if wire == owire:
+            return 0
+        if wire > owire:
+            return 1
+        return -1
+
+    def __eq__(self, other):
+        if not isinstance(other, Option):
+            return False
+        if self.otype != other.otype:
+            return False
+        return self._cmp(other) == 0
+
+    def __ne__(self, other):
+        if not isinstance(other, Option):
+            return True
+        if self.otype != other.otype:
+            return True
+        return self._cmp(other) != 0
+
+    def __lt__(self, other):
+        if not isinstance(other, Option) or \
+                self.otype != other.otype:
+            return NotImplemented
+        return self._cmp(other) < 0
+
+    def __le__(self, other):
+        if not isinstance(other, Option) or \
+                self.otype != other.otype:
+            return NotImplemented
+        return self._cmp(other) <= 0
+
+    def __ge__(self, other):
+        if not isinstance(other, Option) or \
+                self.otype != other.otype:
+            return NotImplemented
+        return self._cmp(other) >= 0
+
+    def __gt__(self, other):
+        if not isinstance(other, Option) or \
+                self.otype != other.otype:
+            return NotImplemented
+        return self._cmp(other) > 0
+
+    def __str__(self):
+        return self.to_text()
+
+
+class GenericOption(Option):
+
+    """Generic Option Class
+
+    This class is used for EDNS option types for which we have no better
+    implementation.
+    """
+
+    def __init__(self, otype, data):
+        super().__init__(otype)
+        self.data = dns.rdata.Rdata._as_bytes(data, True)
+
+    def to_wire(self, file=None):
+        if file:
+            file.write(self.data)
+        else:
+            return self.data
+
+    def to_text(self):
+        return "Generic %d" % self.otype
+
+    @classmethod
+    def from_wire_parser(cls, otype, parser):
+        return cls(otype, parser.get_remaining())
+
+
+class ECSOption(Option):
+    """EDNS Client Subnet (ECS, RFC7871)"""
+
+    def __init__(self, address, srclen=None, scopelen=0):
+        """*address*, a ``str``, is the client address information.
+
+        *srclen*, an ``int``, the source prefix length, which is the
+        leftmost number of bits of the address to be used for the
+        lookup.  The default is 24 for IPv4 and 56 for IPv6.
+
+        *scopelen*, an ``int``, the scope prefix length.  This value
+        must be 0 in queries, and should be set in responses.
+        """
+
+        super().__init__(OptionType.ECS)
+        af = dns.inet.af_for_address(address)
+
+        if af == socket.AF_INET6:
+            self.family = 2
+            if srclen is None:
+                srclen = 56
+            address = dns.rdata.Rdata._as_ipv6_address(address)
+            srclen = dns.rdata.Rdata._as_int(srclen, 0, 128)
+            scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 128)
+        elif af == socket.AF_INET:
+            self.family = 1
+            if srclen is None:
+                srclen = 24
+            address = dns.rdata.Rdata._as_ipv4_address(address)
+            srclen = dns.rdata.Rdata._as_int(srclen, 0, 32)
+            scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 32)
+        else:  # pragma: no cover   (this will never happen)
+            raise ValueError('Bad address family')
+
+        self.address = address
+        self.srclen = srclen
+        self.scopelen = scopelen
+
+        addrdata = dns.inet.inet_pton(af, address)
+        nbytes = int(math.ceil(srclen / 8.0))
+
+        # Truncate to srclen and pad to the end of the last octet needed
+        # See RFC section 6
+        self.addrdata = addrdata[:nbytes]
+        nbits = srclen % 8
+        if nbits != 0:
+            last = struct.pack('B',
+                               ord(self.addrdata[-1:]) & (0xff << (8 - nbits)))
+            self.addrdata = self.addrdata[:-1] + last
+
+    def to_text(self):
+        return "ECS {}/{} scope/{}".format(self.address, self.srclen,
+                                           self.scopelen)
+
+    @staticmethod
+    def from_text(text):
+        """Convert a string into a `dns.edns.ECSOption`
+
+        *text*, a `str`, the text form of the option.
+
+        Returns a `dns.edns.ECSOption`.
+
+        Examples:
+
+        >>> import dns.edns
+        >>>
+        >>> # basic example
+        >>> dns.edns.ECSOption.from_text('1.2.3.4/24')
+        >>>
+        >>> # also understands scope
+        >>> dns.edns.ECSOption.from_text('1.2.3.4/24/32')
+        >>>
+        >>> # IPv6
+        >>> dns.edns.ECSOption.from_text('2001:4b98::1/64/64')
+        >>>
+        >>> # it understands results from `dns.edns.ECSOption.to_text()`
+        >>> dns.edns.ECSOption.from_text('ECS 1.2.3.4/24/32')
+        """
+        optional_prefix = 'ECS'
+        tokens = text.split()
+        ecs_text = None
+        if len(tokens) == 1:
+            ecs_text = tokens[0]
+        elif len(tokens) == 2:
+            if tokens[0] != optional_prefix:
+                raise ValueError('could not parse ECS from "{}"'.format(text))
+            ecs_text = tokens[1]
+        else:
+            raise ValueError('could not parse ECS from "{}"'.format(text))
+        n_slashes = ecs_text.count('/')
+        if n_slashes == 1:
+            address, srclen = ecs_text.split('/')
+            scope = 0
+        elif n_slashes == 2:
+            address, srclen, scope = ecs_text.split('/')
+        else:
+            raise ValueError('could not parse ECS from "{}"'.format(text))
+        try:
+            scope = int(scope)
+        except ValueError:
+            raise ValueError('invalid scope ' +
+                             '"{}": scope must be an integer'.format(scope))
+        try:
+            srclen = int(srclen)
+        except ValueError:
+            raise ValueError('invalid srclen ' +
+                             '"{}": srclen must be an integer'.format(srclen))
+        return ECSOption(address, srclen, scope)
+
+    def to_wire(self, file=None):
+        value = (struct.pack('!HBB', self.family, self.srclen, self.scopelen) +
+                 self.addrdata)
+        if file:
+            file.write(value)
+        else:
+            return value
+
+    @classmethod
+    def from_wire_parser(cls, otype, parser):
+        family, src, scope = parser.get_struct('!HBB')
+        addrlen = int(math.ceil(src / 8.0))
+        prefix = parser.get_bytes(addrlen)
+        if family == 1:
+            pad = 4 - addrlen
+            addr = dns.ipv4.inet_ntoa(prefix + b'\x00' * pad)
+        elif family == 2:
+            pad = 16 - addrlen
+            addr = dns.ipv6.inet_ntoa(prefix + b'\x00' * pad)
+        else:
+            raise ValueError('unsupported family')
+
+        return cls(addr, src, scope)
+
+
+_type_to_class = {
+    OptionType.ECS: ECSOption
+}
+
+def get_option_class(otype):
+    """Return the class for the specified option type.
+
+    The GenericOption class is used if a more specific class is not
+    known.
+    """
+
+    cls = _type_to_class.get(otype)
+    if cls is None:
+        cls = GenericOption
+    return cls
+
+
+def option_from_wire_parser(otype, parser):
+    """Build an EDNS option object from wire format.
+
+    *otype*, an ``int``, is the option type.
+
+    *parser*, a ``dns.wire.Parser``, the parser, which should be
+    restricted to the option length.
+
+    Returns an instance of a subclass of ``dns.edns.Option``.
+    """
+    cls = get_option_class(otype)
+    otype = OptionType.make(otype)
+    return cls.from_wire_parser(otype, parser)
+
+
+def option_from_wire(otype, wire, current, olen):
+    """Build an EDNS option object from wire format.
+
+    *otype*, an ``int``, is the option type.
+
+    *wire*, a ``bytes``, is the wire-format message.
+
+    *current*, an ``int``, is the offset in *wire* of the beginning
+    of the rdata.
+
+    *olen*, an ``int``, is the length of the wire-format option data
+
+    Returns an instance of a subclass of ``dns.edns.Option``.
+    """
+    parser = dns.wire.Parser(wire, current)
+    with parser.restrict_to(olen):
+        return option_from_wire_parser(otype, parser)
+
+def register_type(implementation, otype):
+    """Register the implementation of an option type.
+
+    *implementation*, a ``class``, is a subclass of ``dns.edns.Option``.
+
+    *otype*, an ``int``, is the option type.
+    """
+
+    _type_to_class[otype] = implementation
+
+### BEGIN generated OptionType constants
+
+NSID = OptionType.NSID
+DAU = OptionType.DAU
+DHU = OptionType.DHU
+N3U = OptionType.N3U
+ECS = OptionType.ECS
+EXPIRE = OptionType.EXPIRE
+COOKIE = OptionType.COOKIE
+KEEPALIVE = OptionType.KEEPALIVE
+PADDING = OptionType.PADDING
+CHAIN = OptionType.CHAIN
+
+### END generated OptionType constants

=== added file 'dns/entropy.py'
--- old/dns/entropy.py	1970-01-01 00:00:00 +0000
+++ new/dns/entropy.py	2020-08-06 23:26:37 +0000
@@ -0,0 +1,129 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import os
+import hashlib
+import random
+import time
+try:
+    import threading as _threading
+except ImportError:  # pragma: no cover
+    import dummy_threading as _threading    # type: ignore
+
+
+class EntropyPool:
+
+    # This is an entropy pool for Python implementations that do not
+    # have a working SystemRandom.  I'm not sure there are any, but
+    # leaving this code doesn't hurt anything as the library code
+    # is used if present.
+
+    def __init__(self, seed=None):
+        self.pool_index = 0
+        self.digest = None
+        self.next_byte = 0
+        self.lock = _threading.Lock()
+        self.hash = hashlib.sha1()
+        self.hash_len = 20
+        self.pool = bytearray(b'\0' * self.hash_len)
+        if seed is not None:
+            self._stir(bytearray(seed))
+            self.seeded = True
+            self.seed_pid = os.getpid()
+        else:
+            self.seeded = False
+            self.seed_pid = 0
+
+    def _stir(self, entropy):
+        for c in entropy:
+            if self.pool_index == self.hash_len:
+                self.pool_index = 0
+            b = c & 0xff
+            self.pool[self.pool_index] ^= b
+            self.pool_index += 1
+
+    def stir(self, entropy):
+        with self.lock:
+            self._stir(entropy)
+
+    def _maybe_seed(self):
+        if not self.seeded or self.seed_pid != os.getpid():
+            try:
+                seed = os.urandom(16)
+            except Exception:  # pragma: no cover
+                try:
+                    with open('/dev/urandom', 'rb', 0) as r:
+                        seed = r.read(16)
+                except Exception:
+                    seed = str(time.time())
+            self.seeded = True
+            self.seed_pid = os.getpid()
+            self.digest = None
+            seed = bytearray(seed)
+            self._stir(seed)
+
+    def random_8(self):
+        with self.lock:
+            self._maybe_seed()
+            if self.digest is None or self.next_byte == self.hash_len:
+                self.hash.update(bytes(self.pool))
+                self.digest = bytearray(self.hash.digest())
+                self._stir(self.digest)
+                self.next_byte = 0
+            value = self.digest[self.next_byte]
+            self.next_byte += 1
+        return value
+
+    def random_16(self):
+        return self.random_8() * 256 + self.random_8()
+
+    def random_32(self):
+        return self.random_16() * 65536 + self.random_16()
+
+    def random_between(self, first, last):
+        size = last - first + 1
+        if size > 4294967296:
+            raise ValueError('too big')
+        if size > 65536:
+            rand = self.random_32
+            max = 4294967295
+        elif size > 256:
+            rand = self.random_16
+            max = 65535
+        else:
+            rand = self.random_8
+            max = 255
+        return first + size * rand() // (max + 1)
+
+pool = EntropyPool()
+
+try:
+    system_random = random.SystemRandom()
+except Exception:  # pragma: no cover
+    system_random = None
+
+def random_16():
+    if system_random is not None:
+        return system_random.randrange(0, 65536)
+    else:
+        return pool.random_16()
+
+def between(first, last):
+    if system_random is not None:
+        return system_random.randrange(first, last + 1)
+    else:
+        return pool.random_between(first, last)

=== added file 'dns/entropy.pyi'
--- old/dns/entropy.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/entropy.pyi	2018-12-23 00:54:24 +0000
@@ -0,0 +1,10 @@
+from typing import Optional
+from random import SystemRandom
+
+system_random : Optional[SystemRandom]
+
+def random_16() -> int:
+   pass
+
+def between(first: int, last: int) -> int:
+    pass

=== added file 'dns/enum.py'
--- old/dns/enum.py	1970-01-01 00:00:00 +0000
+++ new/dns/enum.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,90 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import enum
+
+class IntEnum(enum.IntEnum):
+    @classmethod
+    def _check_value(cls, value):
+        max = cls._maximum()
+        if value < 0 or value > max:
+            name = cls._short_name()
+            raise ValueError(f"{name} must be between >= 0 and <= {max}")
+
+    @classmethod
+    def from_text(cls, text):
+        text = text.upper()
+        try:
+            return cls[text]
+        except KeyError:
+            pass
+        prefix = cls._prefix()
+        if text.startswith(prefix) and text[len(prefix):].isdigit():
+            value = int(text[len(prefix):])
+            cls._check_value(value)
+            try:
+                return cls(value)
+            except ValueError:
+                return value
+        raise cls._unknown_exception_class()
+
+    @classmethod
+    def to_text(cls, value):
+        cls._check_value(value)
+        try:
+            return cls(value).name
+        except ValueError:
+            return f"{cls._prefix()}{value}"
+
+    @classmethod
+    def make(cls, value):
+        """Convert text or a value into an enumerated type, if possible.
+
+        *value*, the ``int`` or ``str`` to convert.
+
+        Raises a class-specific exception if a ``str`` is provided that
+        cannot be converted.
+
+        Raises ``ValueError`` if the value is out of range.
+
+        Returns an enumeration from the calling class corresponding to the
+        value, if one is defined, or an ``int`` otherwise.
+        """
+
+        if isinstance(value, str):
+            return cls.from_text(value)
+        cls._check_value(value)
+        try:
+            return cls(value)
+        except ValueError:
+            return value
+
+    @classmethod
+    def _maximum(cls):
+        raise NotImplementedError  # pragma: no cover
+
+    @classmethod
+    def _short_name(cls):
+        return cls.__name__.lower()
+
+    @classmethod
+    def _prefix(cls):
+        return ''
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return ValueError

=== added file 'dns/exception.py'
--- old/dns/exception.py	1970-01-01 00:00:00 +0000
+++ new/dns/exception.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,142 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Common DNS Exceptions.
+
+Dnspython modules may also define their own exceptions, which will
+always be subclasses of ``DNSException``.
+"""
+
+class DNSException(Exception):
+    """Abstract base class shared by all dnspython exceptions.
+
+    It supports two basic modes of operation:
+
+    a) Old/compatible mode is used if ``__init__`` was called with
+    empty *kwargs*.  In compatible mode all *args* are passed
+    to the standard Python Exception class as before and all *args* are
+    printed by the standard ``__str__`` implementation.  Class variable
+    ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()``
+    if *args* is empty.
+
+    b) New/parametrized mode is used if ``__init__`` was called with
+    non-empty *kwargs*.
+    In the new mode *args* must be empty and all kwargs must match
+    those set in class variable ``supp_kwargs``. All kwargs are stored inside
+    ``self.kwargs`` and used in a new ``__str__`` implementation to construct
+    a formatted message based on the ``fmt`` class variable, a ``string``.
+
+    In the simplest case it is enough to override the ``supp_kwargs``
+    and ``fmt`` class variables to get nice parametrized messages.
+    """
+
+    msg = None  # non-parametrized message
+    supp_kwargs = set()  # accepted parameters for _fmt_kwargs (sanity check)
+    fmt = None  # message parametrized with results from _fmt_kwargs
+
+    def __init__(self, *args, **kwargs):
+        self._check_params(*args, **kwargs)
+        if kwargs:
+            self.kwargs = self._check_kwargs(**kwargs)
+            self.msg = str(self)
+        else:
+            self.kwargs = dict()  # defined but empty for old mode exceptions
+        if self.msg is None:
+            # doc string is better implicit message than empty string
+            self.msg = self.__doc__
+        if args:
+            super().__init__(*args)
+        else:
+            super().__init__(self.msg)
+
+    def _check_params(self, *args, **kwargs):
+        """Old exceptions supported only args and not kwargs.
+
+        For sanity we do not allow to mix old and new behavior."""
+        if args or kwargs:
+            assert bool(args) != bool(kwargs), \
+                'keyword arguments are mutually exclusive with positional args'
+
+    def _check_kwargs(self, **kwargs):
+        if kwargs:
+            assert set(kwargs.keys()) == self.supp_kwargs, \
+                'following set of keyword args is required: %s' % (
+                    self.supp_kwargs)
+        return kwargs
+
+    def _fmt_kwargs(self, **kwargs):
+        """Format kwargs before printing them.
+
+        Resulting dictionary has to have keys necessary for str.format call
+        on fmt class variable.
+        """
+        fmtargs = {}
+        for kw, data in kwargs.items():
+            if isinstance(data, (list, set)):
+                # convert list of <someobj> to list of str(<someobj>)
+                fmtargs[kw] = list(map(str, data))
+                if len(fmtargs[kw]) == 1:
+                    # remove list brackets [] from single-item lists
+                    fmtargs[kw] = fmtargs[kw].pop()
+            else:
+                fmtargs[kw] = data
+        return fmtargs
+
+    def __str__(self):
+        if self.kwargs and self.fmt:
+            # provide custom message constructed from keyword arguments
+            fmtargs = self._fmt_kwargs(**self.kwargs)
+            return self.fmt.format(**fmtargs)
+        else:
+            # print *args directly in the same way as old DNSException
+            return super().__str__()
+
+
+class FormError(DNSException):
+    """DNS message is malformed."""
+
+
+class SyntaxError(DNSException):
+    """Text input is malformed."""
+
+
+class UnexpectedEnd(SyntaxError):
+    """Text input ended unexpectedly."""
+
+
+class TooBig(DNSException):
+    """The DNS message is too big."""
+
+
+class Timeout(DNSException):
+    """The DNS operation timed out."""
+    supp_kwargs = {'timeout'}
+    fmt = "The DNS operation timed out after {timeout} seconds"
+
+
+class ExceptionWrapper:
+    def __init__(self, exception_class):
+        self.exception_class = exception_class
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_type is not None and not isinstance(exc_val,
+                                                   self.exception_class):
+            raise self.exception_class(str(exc_val)) from exc_val
+        return False

=== added file 'dns/exception.pyi'
--- old/dns/exception.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/exception.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,10 @@
+from typing import Set, Optional, Dict
+
+class DNSException(Exception):
+    supp_kwargs : Set[str]
+    kwargs : Optional[Dict]
+    fmt : Optional[str]
+
+class SyntaxError(DNSException): ...
+class FormError(DNSException): ...
+class Timeout(DNSException): ...

=== added file 'dns/flags.py'
--- old/dns/flags.py	1970-01-01 00:00:00 +0000
+++ new/dns/flags.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,119 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Message Flags."""
+
+import enum
+
+# Standard DNS flags
+
+class Flag(enum.IntFlag):
+    #: Query Response
+    QR = 0x8000
+    #: Authoritative Answer
+    AA = 0x0400
+    #: Truncated Response
+    TC = 0x0200
+    #: Recursion Desired
+    RD = 0x0100
+    #: Recursion Available
+    RA = 0x0080
+    #: Authentic Data
+    AD = 0x0020
+    #: Checking Disabled
+    CD = 0x0010
+
+
+# EDNS flags
+
+class EDNSFlag(enum.IntFlag):
+    #: DNSSEC answer OK
+    DO = 0x8000
+
+
+def _from_text(text, enum_class):
+    flags = 0
+    tokens = text.split()
+    for t in tokens:
+        flags |= enum_class[t.upper()]
+    return flags
+
+
+def _to_text(flags, enum_class):
+    text_flags = []
+    for k, v in enum_class.__members__.items():
+        if flags & v != 0:
+            text_flags.append(k)
+    return ' '.join(text_flags)
+
+
+def from_text(text):
+    """Convert a space-separated list of flag text values into a flags
+    value.
+
+    Returns an ``int``
+    """
+
+    return _from_text(text, Flag)
+
+
+def to_text(flags):
+    """Convert a flags value into a space-separated list of flag text
+    values.
+
+    Returns a ``str``.
+    """
+
+    return _to_text(flags, Flag)
+
+
+def edns_from_text(text):
+    """Convert a space-separated list of EDNS flag text values into a EDNS
+    flags value.
+
+    Returns an ``int``
+    """
+
+    return _from_text(text, EDNSFlag)
+
+
+def edns_to_text(flags):
+    """Convert an EDNS flags value into a space-separated list of EDNS flag
+    text values.
+
+    Returns a ``str``.
+    """
+
+    return _to_text(flags, EDNSFlag)
+
+### BEGIN generated Flag constants
+
+QR = Flag.QR
+AA = Flag.AA
+TC = Flag.TC
+RD = Flag.RD
+RA = Flag.RA
+AD = Flag.AD
+CD = Flag.CD
+
+### END generated Flag constants
+
+### BEGIN generated EDNSFlag constants
+
+DO = EDNSFlag.DO
+
+### END generated EDNSFlag constants

=== added file 'dns/grange.py'
--- old/dns/grange.py	1970-01-01 00:00:00 +0000
+++ new/dns/grange.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,69 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2012-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS GENERATE range conversion."""
+
+import dns
+
+def from_text(text):
+    """Convert the text form of a range in a ``$GENERATE`` statement to an
+    integer.
+
+    *text*, a ``str``, the textual range in ``$GENERATE`` form.
+
+    Returns a tuple of three ``int`` values ``(start, stop, step)``.
+    """
+
+    start = -1
+    stop = -1
+    step = 1
+    cur = ''
+    state = 0
+    # state   0   1   2
+    #         x - y / z
+
+    if text and text[0] == '-':
+        raise dns.exception.SyntaxError("Start cannot be a negative number")
+
+    for c in text:
+        if c == '-' and state == 0:
+            start = int(cur)
+            cur = ''
+            state = 1
+        elif c == '/':
+            stop = int(cur)
+            cur = ''
+            state = 2
+        elif c.isdigit():
+            cur += c
+        else:
+            raise dns.exception.SyntaxError("Could not parse %s" % (c))
+
+    if state == 0:
+        raise dns.exception.SyntaxError("no stop value specified")
+    elif state == 1:
+        stop = int(cur)
+    else:
+        assert state == 2
+        step = int(cur)
+
+    assert step >= 1
+    assert start >= 0
+    if start > stop:
+        raise dns.exception.SyntaxError('start must be <= stop')
+
+    return (start, stop, step)

=== added file 'dns/immutable.py'
--- old/dns/immutable.py	1970-01-01 00:00:00 +0000
+++ new/dns/immutable.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,70 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import collections.abc
+import sys
+
+# pylint: disable=unused-import
+if sys.version_info >= (3, 7):
+    odict = dict
+    from dns._immutable_ctx import immutable
+else:
+    # pragma: no cover
+    from collections import OrderedDict as odict
+    from dns._immutable_attr import immutable  # noqa
+# pylint: enable=unused-import
+
+
+@immutable
+class Dict(collections.abc.Mapping):
+    def __init__(self, dictionary, no_copy=False):
+        """Make an immutable dictionary from the specified dictionary.
+
+        If *no_copy* is `True`, then *dictionary* will be wrapped instead
+        of copied.  Only set this if you are sure there will be no external
+        references to the dictionary.
+        """
+        if no_copy and isinstance(dictionary, odict):
+            self._odict = dictionary
+        else:
+            self._odict = odict(dictionary)
+        self._hash = None
+
+    def __getitem__(self, key):
+        return self._odict.__getitem__(key)
+
+    def __hash__(self):  # pylint: disable=invalid-hash-returned
+        if self._hash is None:
+            h = 0
+            for key in sorted(self._odict.keys()):
+                h ^= hash(key)
+            object.__setattr__(self, '_hash', h)
+        # this does return an int, but pylint doesn't figure that out
+        return self._hash
+
+    def __len__(self):
+        return len(self._odict)
+
+    def __iter__(self):
+        return iter(self._odict)
+
+
+def constify(o):
+    """
+    Convert mutable types to immutable types.
+    """
+    if isinstance(o, bytearray):
+        return bytes(o)
+    if isinstance(o, tuple):
+        try:
+            hash(o)
+            return o
+        except Exception:
+            return tuple(constify(elt) for elt in o)
+    if isinstance(o, list):
+        return tuple(constify(elt) for elt in o)
+    if isinstance(o, dict):
+        cdict = odict()
+        for k, v in o.items():
+            cdict[k] = constify(v)
+        return Dict(cdict, True)
+    return o

=== added file 'dns/inet.py'
--- old/dns/inet.py	1970-01-01 00:00:00 +0000
+++ new/dns/inet.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,170 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Generic Internet address helper functions."""
+
+import socket
+
+import dns.ipv4
+import dns.ipv6
+
+
+# We assume that AF_INET and AF_INET6 are always defined.  We keep
+# these here for the benefit of any old code (unlikely though that
+# is!).
+AF_INET = socket.AF_INET
+AF_INET6 = socket.AF_INET6
+
+
+def inet_pton(family, text):
+    """Convert the textual form of a network address into its binary form.
+
+    *family* is an ``int``, the address family.
+
+    *text* is a ``str``, the textual address.
+
+    Raises ``NotImplementedError`` if the address family specified is not
+    implemented.
+
+    Returns a ``bytes``.
+    """
+
+    if family == AF_INET:
+        return dns.ipv4.inet_aton(text)
+    elif family == AF_INET6:
+        return dns.ipv6.inet_aton(text, True)
+    else:
+        raise NotImplementedError
+
+
+def inet_ntop(family, address):
+    """Convert the binary form of a network address into its textual form.
+
+    *family* is an ``int``, the address family.
+
+    *address* is a ``bytes``, the network address in binary form.
+
+    Raises ``NotImplementedError`` if the address family specified is not
+    implemented.
+
+    Returns a ``str``.
+    """
+
+    if family == AF_INET:
+        return dns.ipv4.inet_ntoa(address)
+    elif family == AF_INET6:
+        return dns.ipv6.inet_ntoa(address)
+    else:
+        raise NotImplementedError
+
+
+def af_for_address(text):
+    """Determine the address family of a textual-form network address.
+
+    *text*, a ``str``, the textual address.
+
+    Raises ``ValueError`` if the address family cannot be determined
+    from the input.
+
+    Returns an ``int``.
+    """
+
+    try:
+        dns.ipv4.inet_aton(text)
+        return AF_INET
+    except Exception:
+        try:
+            dns.ipv6.inet_aton(text, True)
+            return AF_INET6
+        except Exception:
+            raise ValueError
+
+
+def is_multicast(text):
+    """Is the textual-form network address a multicast address?
+
+    *text*, a ``str``, the textual address.
+
+    Raises ``ValueError`` if the address family cannot be determined
+    from the input.
+
+    Returns a ``bool``.
+    """
+
+    try:
+        first = dns.ipv4.inet_aton(text)[0]
+        return first >= 224 and first <= 239
+    except Exception:
+        try:
+            first = dns.ipv6.inet_aton(text, True)[0]
+            return first == 255
+        except Exception:
+            raise ValueError
+
+
+def is_address(text):
+    """Is the specified string an IPv4 or IPv6 address?
+
+    *text*, a ``str``, the textual address.
+
+    Returns a ``bool``.
+    """
+
+    try:
+        dns.ipv4.inet_aton(text)
+        return True
+    except Exception:
+        try:
+            dns.ipv6.inet_aton(text, True)
+            return True
+        except Exception:
+            return False
+
+
+def low_level_address_tuple(high_tuple, af=None):
+    """Given a "high-level" address tuple, i.e.
+    an (address, port) return the appropriate "low-level" address tuple
+    suitable for use in socket calls.
+
+    If an *af* other than ``None`` is provided, it is assumed the
+    address in the high-level tuple is valid and has that af.  If af
+    is ``None``, then af_for_address will be called.
+
+    """
+    address, port = high_tuple
+    if af is None:
+        af = af_for_address(address)
+    if af == AF_INET:
+        return (address, port)
+    elif af == AF_INET6:
+        i = address.find('%')
+        if i < 0:
+            # no scope, shortcut!
+            return (address, port, 0, 0)
+        # try to avoid getaddrinfo()
+        addrpart = address[:i]
+        scope = address[i + 1:]
+        if scope.isdigit():
+            return (addrpart, port, 0, int(scope))
+        try:
+            return (addrpart, port, 0, socket.if_nametoindex(scope))
+        except AttributeError:  # pragma: no cover  (we can't really test this)
+            ai_flags = socket.AI_NUMERICHOST
+            ((*_, tup), *_) = socket.getaddrinfo(address, port, flags=ai_flags)
+            return tup
+    else:
+        raise NotImplementedError(f'unknown address family {af}')

=== added file 'dns/inet.pyi'
--- old/dns/inet.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/inet.pyi	2018-12-23 00:54:24 +0000
@@ -0,0 +1,4 @@
+from typing import Union
+from socket import AddressFamily
+
+AF_INET6 : Union[int, AddressFamily]

=== added file 'dns/ipv4.py'
--- old/dns/ipv4.py	1970-01-01 00:00:00 +0000
+++ new/dns/ipv4.py	2020-08-06 23:26:37 +0000
@@ -0,0 +1,60 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""IPv4 helper functions."""
+
+import struct
+
+import dns.exception
+
+def inet_ntoa(address):
+    """Convert an IPv4 address in binary form to text form.
+
+    *address*, a ``bytes``, the IPv4 address in binary form.
+
+    Returns a ``str``.
+    """
+
+    if len(address) != 4:
+        raise dns.exception.SyntaxError
+    return ('%u.%u.%u.%u' % (address[0], address[1],
+                             address[2], address[3]))
+
+def inet_aton(text):
+    """Convert an IPv4 address in text form to binary form.
+
+    *text*, a ``str``, the IPv4 address in textual form.
+
+    Returns a ``bytes``.
+    """
+
+    if not isinstance(text, bytes):
+        text = text.encode()
+    parts = text.split(b'.')
+    if len(parts) != 4:
+        raise dns.exception.SyntaxError
+    for part in parts:
+        if not part.isdigit():
+            raise dns.exception.SyntaxError
+        if len(part) > 1 and part[0] == ord('0'):
+            # No leading zeros
+            raise dns.exception.SyntaxError
+    try:
+        b = [int(part) for part in parts]
+        return struct.pack('BBBB', *b)
+    except Exception:
+        raise dns.exception.SyntaxError

=== added file 'dns/ipv6.py'
--- old/dns/ipv6.py	1970-01-01 00:00:00 +0000
+++ new/dns/ipv6.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,197 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""IPv6 helper functions."""
+
+import re
+import binascii
+
+import dns.exception
+import dns.ipv4
+
+_leading_zero = re.compile(r'0+([0-9a-f]+)')
+
+def inet_ntoa(address):
+    """Convert an IPv6 address in binary form to text form.
+
+    *address*, a ``bytes``, the IPv6 address in binary form.
+
+    Raises ``ValueError`` if the address isn't 16 bytes long.
+    Returns a ``str``.
+    """
+
+    if len(address) != 16:
+        raise ValueError("IPv6 addresses are 16 bytes long")
+    hex = binascii.hexlify(address)
+    chunks = []
+    i = 0
+    l = len(hex)
+    while i < l:
+        chunk = hex[i:i + 4].decode()
+        # strip leading zeros.  we do this with an re instead of
+        # with lstrip() because lstrip() didn't support chars until
+        # python 2.2.2
+        m = _leading_zero.match(chunk)
+        if m is not None:
+            chunk = m.group(1)
+        chunks.append(chunk)
+        i += 4
+    #
+    # Compress the longest subsequence of 0-value chunks to ::
+    #
+    best_start = 0
+    best_len = 0
+    start = -1
+    last_was_zero = False
+    for i in range(8):
+        if chunks[i] != '0':
+            if last_was_zero:
+                end = i
+                current_len = end - start
+                if current_len > best_len:
+                    best_start = start
+                    best_len = current_len
+                last_was_zero = False
+        elif not last_was_zero:
+            start = i
+            last_was_zero = True
+    if last_was_zero:
+        end = 8
+        current_len = end - start
+        if current_len > best_len:
+            best_start = start
+            best_len = current_len
+    if best_len > 1:
+        if best_start == 0 and \
+           (best_len == 6 or
+            best_len == 5 and chunks[5] == 'ffff'):
+            # We have an embedded IPv4 address
+            if best_len == 6:
+                prefix = '::'
+            else:
+                prefix = '::ffff:'
+            hex = prefix + dns.ipv4.inet_ntoa(address[12:])
+        else:
+            hex = ':'.join(chunks[:best_start]) + '::' + \
+                  ':'.join(chunks[best_start + best_len:])
+    else:
+        hex = ':'.join(chunks)
+    return hex
+
+_v4_ending = re.compile(br'(.*):(\d+\.\d+\.\d+\.\d+)$')
+_colon_colon_start = re.compile(br'::.*')
+_colon_colon_end = re.compile(br'.*::$')
+
+def inet_aton(text, ignore_scope=False):
+    """Convert an IPv6 address in text form to binary form.
+
+    *text*, a ``str``, the IPv6 address in textual form.
+
+    *ignore_scope*, a ``bool``.  If ``True``, a scope will be ignored.
+    If ``False``, the default, it is an error for a scope to be present.
+
+    Returns a ``bytes``.
+    """
+
+    #
+    # Our aim here is not something fast; we just want something that works.
+    #
+    if not isinstance(text, bytes):
+        text = text.encode()
+
+    if ignore_scope:
+        parts = text.split(b'%')
+        l = len(parts)
+        if l == 2:
+            text = parts[0]
+        elif l > 2:
+            raise dns.exception.SyntaxError
+
+    if text == b'':
+        raise dns.exception.SyntaxError
+    elif text.endswith(b':') and not text.endswith(b'::'):
+        raise dns.exception.SyntaxError
+    elif text.startswith(b':') and not text.startswith(b'::'):
+        raise dns.exception.SyntaxError
+    elif text == b'::':
+        text = b'0::'
+    #
+    # Get rid of the icky dot-quad syntax if we have it.
+    #
+    m = _v4_ending.match(text)
+    if m is not None:
+        b = dns.ipv4.inet_aton(m.group(2))
+        text = (u"{}:{:02x}{:02x}:{:02x}{:02x}".format(m.group(1).decode(),
+                                                       b[0], b[1], b[2],
+                                                       b[3])).encode()
+    #
+    # Try to turn '::<whatever>' into ':<whatever>'; if no match try to
+    # turn '<whatever>::' into '<whatever>:'
+    #
+    m = _colon_colon_start.match(text)
+    if m is not None:
+        text = text[1:]
+    else:
+        m = _colon_colon_end.match(text)
+        if m is not None:
+            text = text[:-1]
+    #
+    # Now canonicalize into 8 chunks of 4 hex digits each
+    #
+    chunks = text.split(b':')
+    l = len(chunks)
+    if l > 8:
+        raise dns.exception.SyntaxError
+    seen_empty = False
+    canonical = []
+    for c in chunks:
+        if c == b'':
+            if seen_empty:
+                raise dns.exception.SyntaxError
+            seen_empty = True
+            for _ in range(0, 8 - l + 1):
+                canonical.append(b'0000')
+        else:
+            lc = len(c)
+            if lc > 4:
+                raise dns.exception.SyntaxError
+            if lc != 4:
+                c = (b'0' * (4 - lc)) + c
+            canonical.append(c)
+    if l < 8 and not seen_empty:
+        raise dns.exception.SyntaxError
+    text = b''.join(canonical)
+
+    #
+    # Finally we can go to binary.
+    #
+    try:
+        return binascii.unhexlify(text)
+    except (binascii.Error, TypeError):
+        raise dns.exception.SyntaxError
+
+_mapped_prefix = b'\x00' * 10 + b'\xff\xff'
+
+def is_mapped(address):
+    """Is the specified address a mapped IPv4 address?
+
+    *address*, a ``bytes`` is an IPv6 address in binary form.
+
+    Returns a ``bool``.
+    """
+
+    return address.startswith(_mapped_prefix)

=== added file 'dns/message.py'
--- old/dns/message.py	1970-01-01 00:00:00 +0000
+++ new/dns/message.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,1507 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Messages"""
+
+import contextlib
+import io
+import time
+
+import dns.wire
+import dns.edns
+import dns.enum
+import dns.exception
+import dns.flags
+import dns.name
+import dns.opcode
+import dns.entropy
+import dns.rcode
+import dns.rdata
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+import dns.renderer
+import dns.ttl
+import dns.tsig
+import dns.rdtypes.ANY.OPT
+import dns.rdtypes.ANY.TSIG
+
+
+class ShortHeader(dns.exception.FormError):
+    """The DNS packet passed to from_wire() is too short."""
+
+
+class TrailingJunk(dns.exception.FormError):
+    """The DNS packet passed to from_wire() has extra junk at the end of it."""
+
+
+class UnknownHeaderField(dns.exception.DNSException):
+    """The header field name was not recognized when converting from text
+    into a message."""
+
+
+class BadEDNS(dns.exception.FormError):
+    """An OPT record occurred somewhere other than
+    the additional data section."""
+
+
+class BadTSIG(dns.exception.FormError):
+    """A TSIG record occurred somewhere other than the end of
+    the additional data section."""
+
+
+class UnknownTSIGKey(dns.exception.DNSException):
+    """A TSIG with an unknown key was received."""
+
+
+class Truncated(dns.exception.DNSException):
+    """The truncated flag is set."""
+
+    supp_kwargs = {'message'}
+
+    def message(self):
+        """As much of the message as could be processed.
+
+        Returns a ``dns.message.Message``.
+        """
+        return self.kwargs['message']
+
+
+class NotQueryResponse(dns.exception.DNSException):
+    """Message is not a response to a query."""
+
+
+class ChainTooLong(dns.exception.DNSException):
+    """The CNAME chain is too long."""
+
+
+class AnswerForNXDOMAIN(dns.exception.DNSException):
+    """The rcode is NXDOMAIN but an answer was found."""
+
+class NoPreviousName(dns.exception.SyntaxError):
+    """No previous name was known."""
+
+
+class MessageSection(dns.enum.IntEnum):
+    """Message sections"""
+    QUESTION = 0
+    ANSWER = 1
+    AUTHORITY = 2
+    ADDITIONAL = 3
+
+    @classmethod
+    def _maximum(cls):
+        return 3
+
+
+DEFAULT_EDNS_PAYLOAD = 1232
+MAX_CHAIN = 16
+
+class Message:
+    """A DNS message."""
+
+    _section_enum = MessageSection
+
+    def __init__(self, id=None):
+        if id is None:
+            self.id = dns.entropy.random_16()
+        else:
+            self.id = id
+        self.flags = 0
+        self.sections = [[], [], [], []]
+        self.opt = None
+        self.request_payload = 0
+        self.keyring = None
+        self.tsig = None
+        self.request_mac = b''
+        self.xfr = False
+        self.origin = None
+        self.tsig_ctx = None
+        self.index = {}
+
+    @property
+    def question(self):
+        """ The question section."""
+        return self.sections[0]
+
+    @question.setter
+    def question(self, v):
+        self.sections[0] = v
+
+    @property
+    def answer(self):
+        """ The answer section."""
+        return self.sections[1]
+
+    @answer.setter
+    def answer(self, v):
+        self.sections[1] = v
+
+    @property
+    def authority(self):
+        """ The authority section."""
+        return self.sections[2]
+
+    @authority.setter
+    def authority(self, v):
+        self.sections[2] = v
+
+    @property
+    def additional(self):
+        """ The additional data section."""
+        return self.sections[3]
+
+    @additional.setter
+    def additional(self, v):
+        self.sections[3] = v
+
+    def __repr__(self):
+        return '<DNS message, ID ' + repr(self.id) + '>'
+
+    def __str__(self):
+        return self.to_text()
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        """Convert the message to text.
+
+        The *origin*, *relativize*, and any other keyword
+        arguments are passed to the RRset ``to_wire()`` method.
+
+        Returns a ``str``.
+        """
+
+        s = io.StringIO()
+        s.write('id %d\n' % self.id)
+        s.write('opcode %s\n' % dns.opcode.to_text(self.opcode()))
+        s.write('rcode %s\n' % dns.rcode.to_text(self.rcode()))
+        s.write('flags %s\n' % dns.flags.to_text(self.flags))
+        if self.edns >= 0:
+            s.write('edns %s\n' % self.edns)
+            if self.ednsflags != 0:
+                s.write('eflags %s\n' %
+                        dns.flags.edns_to_text(self.ednsflags))
+            s.write('payload %d\n' % self.payload)
+        for opt in self.options:
+            s.write('option %s\n' % opt.to_text())
+        for (name, which) in self._section_enum.__members__.items():
+            s.write(f';{name}\n')
+            for rrset in self.section_from_number(which):
+                s.write(rrset.to_text(origin, relativize, **kw))
+                s.write('\n')
+        #
+        # We strip off the final \n so the caller can print the result without
+        # doing weird things to get around eccentricities in Python print
+        # formatting
+        #
+        return s.getvalue()[:-1]
+
+    def __eq__(self, other):
+        """Two messages are equal if they have the same content in the
+        header, question, answer, and authority sections.
+
+        Returns a ``bool``.
+        """
+
+        if not isinstance(other, Message):
+            return False
+        if self.id != other.id:
+            return False
+        if self.flags != other.flags:
+            return False
+        for i, section in enumerate(self.sections):
+            other_section = other.sections[i]
+            for n in section:
+                if n not in other_section:
+                    return False
+            for n in other_section:
+                if n not in section:
+                    return False
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def is_response(self, other):
+        """Is *other* a response this message?
+
+        Returns a ``bool``.
+        """
+
+        if other.flags & dns.flags.QR == 0 or \
+           self.id != other.id or \
+           dns.opcode.from_flags(self.flags) != \
+           dns.opcode.from_flags(other.flags):
+            return False
+        if other.rcode() in {dns.rcode.FORMERR, dns.rcode.SERVFAIL,
+                             dns.rcode.NOTIMP, dns.rcode.REFUSED}:
+            # We don't check the question section in these cases if
+            # the other question section is empty, even though they
+            # still really ought to have a question section.
+            if len(other.question) == 0:
+                return True
+        if dns.opcode.is_update(self.flags):
+            # This is assuming the "sender doesn't include anything
+            # from the update", but we don't care to check the other
+            # case, which is that all the sections are returned and
+            # identical.
+            return True
+        for n in self.question:
+            if n not in other.question:
+                return False
+        for n in other.question:
+            if n not in self.question:
+                return False
+        return True
+
+    def section_number(self, section):
+        """Return the "section number" of the specified section for use
+        in indexing.
+
+        *section* is one of the section attributes of this message.
+
+        Raises ``ValueError`` if the section isn't known.
+
+        Returns an ``int``.
+        """
+
+        for i, our_section in enumerate(self.sections):
+            if section is our_section:
+                return self._section_enum(i)
+        raise ValueError('unknown section')
+
+    def section_from_number(self, number):
+        """Return the section list associated with the specified section
+        number.
+
+        *number* is a section number `int` or the text form of a section
+        name.
+
+        Raises ``ValueError`` if the section isn't known.
+
+        Returns a ``list``.
+        """
+
+        section = self._section_enum.make(number)
+        return self.sections[section]
+
+    def find_rrset(self, section, name, rdclass, rdtype,
+                   covers=dns.rdatatype.NONE, deleting=None, create=False,
+                   force_unique=False):
+        """Find the RRset with the given attributes in the specified section.
+
+        *section*, an ``int`` section number, or one of the section
+        attributes of this message.  This specifies the
+        the section of the message to search.  For example::
+
+            my_message.find_rrset(my_message.answer, name, rdclass, rdtype)
+            my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype)
+
+        *name*, a ``dns.name.Name``, the name of the RRset.
+
+        *rdclass*, an ``int``, the class of the RRset.
+
+        *rdtype*, an ``int``, the type of the RRset.
+
+        *covers*, an ``int`` or ``None``, the covers value of the RRset.
+        The default is ``None``.
+
+        *deleting*, an ``int`` or ``None``, the deleting value of the RRset.
+        The default is ``None``.
+
+        *create*, a ``bool``.  If ``True``, create the RRset if it is not found.
+        The created RRset is appended to *section*.
+
+        *force_unique*, a ``bool``.  If ``True`` and *create* is also ``True``,
+        create a new RRset regardless of whether a matching RRset exists
+        already.  The default is ``False``.  This is useful when creating
+        DDNS Update messages, as order matters for them.
+
+        Raises ``KeyError`` if the RRset was not found and create was
+        ``False``.
+
+        Returns a ``dns.rrset.RRset object``.
+        """
+
+        if isinstance(section, int):
+            section_number = section
+            section = self.section_from_number(section_number)
+        else:
+            section_number = self.section_number(section)
+        key = (section_number, name, rdclass, rdtype, covers, deleting)
+        if not force_unique:
+            if self.index is not None:
+                rrset = self.index.get(key)
+                if rrset is not None:
+                    return rrset
+            else:
+                for rrset in section:
+                    if rrset.full_match(name, rdclass, rdtype, covers,
+                                        deleting):
+                        return rrset
+        if not create:
+            raise KeyError
+        rrset = dns.rrset.RRset(name, rdclass, rdtype, covers, deleting)
+        section.append(rrset)
+        if self.index is not None:
+            self.index[key] = rrset
+        return rrset
+
+    def get_rrset(self, section, name, rdclass, rdtype,
+                  covers=dns.rdatatype.NONE, deleting=None, create=False,
+                  force_unique=False):
+        """Get the RRset with the given attributes in the specified section.
+
+        If the RRset is not found, None is returned.
+
+        *section*, an ``int`` section number, or one of the section
+        attributes of this message.  This specifies the
+        the section of the message to search.  For example::
+
+            my_message.get_rrset(my_message.answer, name, rdclass, rdtype)
+            my_message.get_rrset(dns.message.ANSWER, name, rdclass, rdtype)
+
+        *name*, a ``dns.name.Name``, the name of the RRset.
+
+        *rdclass*, an ``int``, the class of the RRset.
+
+        *rdtype*, an ``int``, the type of the RRset.
+
+        *covers*, an ``int`` or ``None``, the covers value of the RRset.
+        The default is ``None``.
+
+        *deleting*, an ``int`` or ``None``, the deleting value of the RRset.
+        The default is ``None``.
+
+        *create*, a ``bool``.  If ``True``, create the RRset if it is not found.
+        The created RRset is appended to *section*.
+
+        *force_unique*, a ``bool``.  If ``True`` and *create* is also ``True``,
+        create a new RRset regardless of whether a matching RRset exists
+        already.  The default is ``False``.  This is useful when creating
+        DDNS Update messages, as order matters for them.
+
+        Returns a ``dns.rrset.RRset object`` or ``None``.
+        """
+
+        try:
+            rrset = self.find_rrset(section, name, rdclass, rdtype, covers,
+                                    deleting, create, force_unique)
+        except KeyError:
+            rrset = None
+        return rrset
+
+    def to_wire(self, origin=None, max_size=0, multi=False, tsig_ctx=None,
+                **kw):
+        """Return a string containing the message in DNS compressed wire
+        format.
+
+        Additional keyword arguments are passed to the RRset ``to_wire()``
+        method.
+
+        *origin*, a ``dns.name.Name`` or ``None``, the origin to be appended
+        to any relative names.  If ``None``, and the message has an origin
+        attribute that is not ``None``, then it will be used.
+
+        *max_size*, an ``int``, the maximum size of the wire format
+        output; default is 0, which means "the message's request
+        payload, if nonzero, or 65535".
+
+        *multi*, a ``bool``, should be set to ``True`` if this message is
+        part of a multiple message sequence.
+
+        *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the
+        ongoing TSIG context, used when signing zone transfers.
+
+        Raises ``dns.exception.TooBig`` if *max_size* was exceeded.
+
+        Returns a ``bytes``.
+        """
+
+        if origin is None and self.origin is not None:
+            origin = self.origin
+        if max_size == 0:
+            if self.request_payload != 0:
+                max_size = self.request_payload
+            else:
+                max_size = 65535
+        if max_size < 512:
+            max_size = 512
+        elif max_size > 65535:
+            max_size = 65535
+        r = dns.renderer.Renderer(self.id, self.flags, max_size, origin)
+        for rrset in self.question:
+            r.add_question(rrset.name, rrset.rdtype, rrset.rdclass)
+        for rrset in self.answer:
+            r.add_rrset(dns.renderer.ANSWER, rrset, **kw)
+        for rrset in self.authority:
+            r.add_rrset(dns.renderer.AUTHORITY, rrset, **kw)
+        if self.opt is not None:
+            r.add_rrset(dns.renderer.ADDITIONAL, self.opt)
+        for rrset in self.additional:
+            r.add_rrset(dns.renderer.ADDITIONAL, rrset, **kw)
+        r.write_header()
+        if self.tsig is not None:
+            (new_tsig, ctx) = dns.tsig.sign(r.get_wire(),
+                                            self.keyring,
+                                            self.tsig[0],
+                                            int(time.time()),
+                                            self.request_mac,
+                                            tsig_ctx,
+                                            multi)
+            self.tsig.clear()
+            self.tsig.add(new_tsig)
+            r.add_rrset(dns.renderer.ADDITIONAL, self.tsig)
+            r.write_header()
+            if multi:
+                self.tsig_ctx = ctx
+        return r.get_wire()
+
+    @staticmethod
+    def _make_tsig(keyname, algorithm, time_signed, fudge, mac, original_id,
+                   error, other):
+        tsig = dns.rdtypes.ANY.TSIG.TSIG(dns.rdataclass.ANY, dns.rdatatype.TSIG,
+                                         algorithm, time_signed, fudge, mac,
+                                         original_id, error, other)
+        return dns.rrset.from_rdata(keyname, 0, tsig)
+
+    def use_tsig(self, keyring, keyname=None, fudge=300,
+                 original_id=None, tsig_error=0, other_data=b'',
+                 algorithm=dns.tsig.default_algorithm):
+        """When sending, a TSIG signature using the specified key
+        should be added.
+
+        *key*, a ``dns.tsig.Key`` is the key to use.  If a key is specified,
+        the *keyring* and *algorithm* fields are not used.
+
+        *keyring*, a ``dict``, ``callable`` or ``dns.tsig.Key``, is either
+        the TSIG keyring or key to use.
+
+        The format of a keyring dict is a mapping from TSIG key name, as
+        ``dns.name.Name`` to ``dns.tsig.Key`` or a TSIG secret, a ``bytes``.
+        If a ``dict`` *keyring* is specified but a *keyname* is not, the key
+        used will be the first key in the *keyring*.  Note that the order of
+        keys in a dictionary is not defined, so applications should supply a
+        keyname when a ``dict`` keyring is used, unless they know the keyring
+        contains only one key.  If a ``callable`` keyring is specified, the
+        callable will be called with the message and the keyname, and is
+        expected to return a key.
+
+        *keyname*, a ``dns.name.Name``, ``str`` or ``None``, the name of
+        thes TSIG key to use; defaults to ``None``.  If *keyring* is a
+        ``dict``, the key must be defined in it.  If *keyring* is a
+        ``dns.tsig.Key``, this is ignored.
+
+        *fudge*, an ``int``, the TSIG time fudge.
+
+        *original_id*, an ``int``, the TSIG original id.  If ``None``,
+        the message's id is used.
+
+        *tsig_error*, an ``int``, the TSIG error code.
+
+        *other_data*, a ``bytes``, the TSIG other data.
+
+        *algorithm*, a ``dns.name.Name``, the TSIG algorithm to use.  This is
+        only used if *keyring* is a ``dict``, and the key entry is a ``bytes``.
+        """
+
+        if isinstance(keyring, dns.tsig.Key):
+            key = keyring
+            keyname = key.name
+        elif callable(keyring):
+            key = keyring(self, keyname)
+        else:
+            if isinstance(keyname, str):
+                keyname = dns.name.from_text(keyname)
+            if keyname is None:
+                keyname = next(iter(keyring))
+            key = keyring[keyname]
+            if isinstance(key, bytes):
+                key = dns.tsig.Key(keyname, key, algorithm)
+        self.keyring = key
+        if original_id is None:
+            original_id = self.id
+        self.tsig = self._make_tsig(keyname, self.keyring.algorithm, 0, fudge,
+                                    b'', original_id, tsig_error, other_data)
+
+    @property
+    def keyname(self):
+        if self.tsig:
+            return self.tsig.name
+        else:
+            return None
+
+    @property
+    def keyalgorithm(self):
+        if self.tsig:
+            return self.tsig[0].algorithm
+        else:
+            return None
+
+    @property
+    def mac(self):
+        if self.tsig:
+            return self.tsig[0].mac
+        else:
+            return None
+
+    @property
+    def tsig_error(self):
+        if self.tsig:
+            return self.tsig[0].error
+        else:
+            return None
+
+    @property
+    def had_tsig(self):
+        return bool(self.tsig)
+
+    @staticmethod
+    def _make_opt(flags=0, payload=DEFAULT_EDNS_PAYLOAD, options=None):
+        opt = dns.rdtypes.ANY.OPT.OPT(payload, dns.rdatatype.OPT,
+                                      options or ())
+        return dns.rrset.from_rdata(dns.name.root, int(flags), opt)
+
+    def use_edns(self, edns=0, ednsflags=0, payload=DEFAULT_EDNS_PAYLOAD,
+                 request_payload=None, options=None):
+        """Configure EDNS behavior.
+
+        *edns*, an ``int``, is the EDNS level to use.  Specifying
+        ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case
+        the other parameters are ignored.  Specifying ``True`` is
+        equivalent to specifying 0, i.e. "use EDNS0".
+
+        *ednsflags*, an ``int``, the EDNS flag values.
+
+        *payload*, an ``int``, is the EDNS sender's payload field, which is the
+        maximum size of UDP datagram the sender can handle.  I.e. how big
+        a response to this message can be.
+
+        *request_payload*, an ``int``, is the EDNS payload size to use when
+        sending this message.  If not specified, defaults to the value of
+        *payload*.
+
+        *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS
+        options.
+        """
+
+        if edns is None or edns is False:
+            edns = -1
+        elif edns is True:
+            edns = 0
+        if edns < 0:
+            self.opt = None
+            self.request_payload = 0
+        else:
+            # make sure the EDNS version in ednsflags agrees with edns
+            ednsflags &= 0xFF00FFFF
+            ednsflags |= (edns << 16)
+            if options is None:
+                options = []
+            self.opt = self._make_opt(ednsflags, payload, options)
+            if request_payload is None:
+                request_payload = payload
+            self.request_payload = request_payload
+
+    @property
+    def edns(self):
+        if self.opt:
+            return (self.ednsflags & 0xff0000) >> 16
+        else:
+            return -1
+
+    @property
+    def ednsflags(self):
+        if self.opt:
+            return self.opt.ttl
+        else:
+            return 0
+
+    @ednsflags.setter
+    def ednsflags(self, v):
+        if self.opt:
+            self.opt.ttl = v
+        elif v:
+            self.opt = self._make_opt(v)
+
+    @property
+    def payload(self):
+        if self.opt:
+            return self.opt[0].payload
+        else:
+            return 0
+
+    @property
+    def options(self):
+        if self.opt:
+            return self.opt[0].options
+        else:
+            return ()
+
+    def want_dnssec(self, wanted=True):
+        """Enable or disable 'DNSSEC desired' flag in requests.
+
+        *wanted*, a ``bool``.  If ``True``, then DNSSEC data is
+        desired in the response, EDNS is enabled if required, and then
+        the DO bit is set.  If ``False``, the DO bit is cleared if
+        EDNS is enabled.
+        """
+
+        if wanted:
+            self.ednsflags |= dns.flags.DO
+        elif self.opt:
+            self.ednsflags &= ~dns.flags.DO
+
+    def rcode(self):
+        """Return the rcode.
+
+        Returns an ``int``.
+        """
+        return dns.rcode.from_flags(int(self.flags), int(self.ednsflags))
+
+    def set_rcode(self, rcode):
+        """Set the rcode.
+
+        *rcode*, an ``int``, is the rcode to set.
+        """
+        (value, evalue) = dns.rcode.to_flags(rcode)
+        self.flags &= 0xFFF0
+        self.flags |= value
+        self.ednsflags &= 0x00FFFFFF
+        self.ednsflags |= evalue
+
+    def opcode(self):
+        """Return the opcode.
+
+        Returns an ``int``.
+        """
+        return dns.opcode.from_flags(int(self.flags))
+
+    def set_opcode(self, opcode):
+        """Set the opcode.
+
+        *opcode*, an ``int``, is the opcode to set.
+        """
+        self.flags &= 0x87FF
+        self.flags |= dns.opcode.to_flags(opcode)
+
+    def _get_one_rr_per_rrset(self, value):
+        # What the caller picked is fine.
+        return value
+
+    # pylint: disable=unused-argument
+
+    def _parse_rr_header(self, section, name, rdclass, rdtype):
+        return (rdclass, rdtype, None, False)
+
+    # pylint: enable=unused-argument
+
+    def _parse_special_rr_header(self, section, count, position,
+                                 name, rdclass, rdtype):
+        if rdtype == dns.rdatatype.OPT:
+            if section != MessageSection.ADDITIONAL or self.opt or \
+               name != dns.name.root:
+                raise BadEDNS
+        elif rdtype == dns.rdatatype.TSIG:
+            if section != MessageSection.ADDITIONAL or \
+               rdclass != dns.rdatatype.ANY or \
+               position != count - 1:
+                raise BadTSIG
+        return (rdclass, rdtype, None, False)
+
+
+class ChainingResult:
+    """The result of a call to dns.message.QueryMessage.resolve_chaining().
+
+    The ``answer`` attribute is the answer RRSet, or ``None`` if it doesn't
+    exist.
+
+    The ``canonical_name`` attribute is the canonical name after all
+    chaining has been applied (this is the name as ``rrset.name`` in cases
+    where rrset is not ``None``).
+
+    The ``minimum_ttl`` attribute is the minimum TTL, i.e. the TTL to
+    use if caching the data.  It is the smallest of all the CNAME TTLs
+    and either the answer TTL if it exists or the SOA TTL and SOA
+    minimum values for negative answers.
+
+    The ``cnames`` attribute is a list of all the CNAME RRSets followed to
+    get to the canonical name.
+    """
+    def __init__(self, canonical_name, answer, minimum_ttl, cnames):
+        self.canonical_name = canonical_name
+        self.answer = answer
+        self.minimum_ttl = minimum_ttl
+        self.cnames = cnames
+
+
+class QueryMessage(Message):
+    def resolve_chaining(self):
+        """Follow the CNAME chain in the response to determine the answer
+        RRset.
+
+        Raises ``dns.message.NotQueryResponse`` if the message is not
+        a response.
+
+        Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long.
+
+        Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN
+        but an answer was found.
+
+        Raises ``dns.exception.FormError`` if the question count is not 1.
+
+        Returns a ChainingResult object.
+        """
+        if self.flags & dns.flags.QR == 0:
+            raise NotQueryResponse
+        if len(self.question) != 1:
+            raise dns.exception.FormError
+        question = self.question[0]
+        qname = question.name
+        min_ttl = dns.ttl.MAX_TTL
+        answer = None
+        count = 0
+        cnames = []
+        while count < MAX_CHAIN:
+            try:
+                answer = self.find_rrset(self.answer, qname, question.rdclass,
+                                         question.rdtype)
+                min_ttl = min(min_ttl, answer.ttl)
+                break
+            except KeyError:
+                if question.rdtype != dns.rdatatype.CNAME:
+                    try:
+                        crrset = self.find_rrset(self.answer, qname,
+                                                 question.rdclass,
+                                                 dns.rdatatype.CNAME)
+                        cnames.append(crrset)
+                        min_ttl = min(min_ttl, crrset.ttl)
+                        for rd in crrset:
+                            qname = rd.target
+                            break
+                        count += 1
+                        continue
+                    except KeyError:
+                        # Exit the chaining loop
+                        break
+                else:
+                    # Exit the chaining loop
+                    break
+        if count >= MAX_CHAIN:
+            raise ChainTooLong
+        if self.rcode() == dns.rcode.NXDOMAIN and answer is not None:
+            raise AnswerForNXDOMAIN
+        if answer is None:
+            # Further minimize the TTL with NCACHE.
+            auname = qname
+            while True:
+                # Look for an SOA RR whose owner name is a superdomain
+                # of qname.
+                try:
+                    srrset = self.find_rrset(self.authority, auname,
+                                             question.rdclass,
+                                             dns.rdatatype.SOA)
+                    min_ttl = min(min_ttl, srrset.ttl, srrset[0].minimum)
+                    break
+                except KeyError:
+                    try:
+                        auname = auname.parent()
+                    except dns.name.NoParent:
+                        break
+        return ChainingResult(qname, answer, min_ttl, cnames)
+
+    def canonical_name(self):
+        """Return the canonical name of the first name in the question
+        section.
+
+        Raises ``dns.message.NotQueryResponse`` if the message is not
+        a response.
+
+        Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long.
+
+        Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN
+        but an answer was found.
+
+        Raises ``dns.exception.FormError`` if the question count is not 1.
+        """
+        return self.resolve_chaining().canonical_name
+
+
+def _maybe_import_update():
+    # We avoid circular imports by doing this here.  We do it in another
+    # function as doing it in _message_factory_from_opcode() makes "dns"
+    # a local symbol, and the first line fails :)
+
+    # pylint: disable=redefined-outer-name,import-outside-toplevel,unused-import
+    import dns.update  # noqa: F401
+
+
+def _message_factory_from_opcode(opcode):
+    if opcode == dns.opcode.QUERY:
+        return QueryMessage
+    elif opcode == dns.opcode.UPDATE:
+        _maybe_import_update()
+        return dns.update.UpdateMessage
+    else:
+        return Message
+
+
+class _WireReader:
+
+    """Wire format reader.
+
+    parser: the binary parser
+    message: The message object being built
+    initialize_message: Callback to set message parsing options
+    question_only: Are we only reading the question?
+    one_rr_per_rrset: Put each RR into its own RRset?
+    keyring: TSIG keyring
+    ignore_trailing: Ignore trailing junk at end of request?
+    multi: Is this message part of a multi-message sequence?
+    DNS dynamic updates.
+    """
+
+    def __init__(self, wire, initialize_message, question_only=False,
+                 one_rr_per_rrset=False, ignore_trailing=False,
+                 keyring=None, multi=False):
+        self.parser = dns.wire.Parser(wire)
+        self.message = None
+        self.initialize_message = initialize_message
+        self.question_only = question_only
+        self.one_rr_per_rrset = one_rr_per_rrset
+        self.ignore_trailing = ignore_trailing
+        self.keyring = keyring
+        self.multi = multi
+
+    def _get_question(self, section_number, qcount):
+        """Read the next *qcount* records from the wire data and add them to
+        the question section.
+        """
+
+        section = self.message.sections[section_number]
+        for _ in range(qcount):
+            qname = self.parser.get_name(self.message.origin)
+            (rdtype, rdclass) = self.parser.get_struct('!HH')
+            (rdclass, rdtype, _, _) = \
+                self.message._parse_rr_header(section_number, qname, rdclass,
+                                              rdtype)
+            self.message.find_rrset(section, qname, rdclass, rdtype,
+                                    create=True, force_unique=True)
+
+    def _get_section(self, section_number, count):
+        """Read the next I{count} records from the wire data and add them to
+        the specified section.
+
+        section: the section of the message to which to add records
+        count: the number of records to read
+        """
+
+        section = self.message.sections[section_number]
+        force_unique = self.one_rr_per_rrset
+        for i in range(count):
+            rr_start = self.parser.current
+            absolute_name = self.parser.get_name()
+            if self.message.origin is not None:
+                name = absolute_name.relativize(self.message.origin)
+            else:
+                name = absolute_name
+            (rdtype, rdclass, ttl, rdlen) = self.parser.get_struct('!HHIH')
+            if rdtype in (dns.rdatatype.OPT, dns.rdatatype.TSIG):
+                (rdclass, rdtype, deleting, empty) = \
+                    self.message._parse_special_rr_header(section_number,
+                                                          count, i, name,
+                                                          rdclass, rdtype)
+            else:
+                (rdclass, rdtype, deleting, empty) = \
+                    self.message._parse_rr_header(section_number,
+                                                  name, rdclass, rdtype)
+            if empty:
+                if rdlen > 0:
+                    raise dns.exception.FormError
+                rd = None
+                covers = dns.rdatatype.NONE
+            else:
+                with self.parser.restrict_to(rdlen):
+                    rd = dns.rdata.from_wire_parser(rdclass, rdtype,
+                                                    self.parser,
+                                                    self.message.origin)
+                covers = rd.covers()
+            if self.message.xfr and rdtype == dns.rdatatype.SOA:
+                force_unique = True
+            if rdtype == dns.rdatatype.OPT:
+                self.message.opt = dns.rrset.from_rdata(name, ttl, rd)
+            elif rdtype == dns.rdatatype.TSIG:
+                if self.keyring is None:
+                    raise UnknownTSIGKey('got signed message without keyring')
+                if isinstance(self.keyring, dict):
+                    key = self.keyring.get(absolute_name)
+                    if isinstance(key, bytes):
+                        key = dns.tsig.Key(absolute_name, key, rd.algorithm)
+                elif callable(self.keyring):
+                    key = self.keyring(self.message, absolute_name)
+                else:
+                    key = self.keyring
+                if key is None:
+                    raise UnknownTSIGKey("key '%s' unknown" % name)
+                self.message.keyring = key
+                self.message.tsig_ctx = \
+                    dns.tsig.validate(self.parser.wire,
+                                      key,
+                                      absolute_name,
+                                      rd,
+                                      int(time.time()),
+                                      self.message.request_mac,
+                                      rr_start,
+                                      self.message.tsig_ctx,
+                                      self.multi)
+                self.message.tsig = dns.rrset.from_rdata(absolute_name, 0, rd)
+            else:
+                rrset = self.message.find_rrset(section, name,
+                                                rdclass, rdtype, covers,
+                                                deleting, True,
+                                                force_unique)
+                if rd is not None:
+                    if ttl > 0x7fffffff:
+                        ttl = 0
+                    rrset.add(rd, ttl)
+
+    def read(self):
+        """Read a wire format DNS message and build a dns.message.Message
+        object."""
+
+        if self.parser.remaining() < 12:
+            raise ShortHeader
+        (id, flags, qcount, ancount, aucount, adcount) = \
+            self.parser.get_struct('!HHHHHH')
+        factory = _message_factory_from_opcode(dns.opcode.from_flags(flags))
+        self.message = factory(id=id)
+        self.message.flags = dns.flags.Flag(flags)
+        self.initialize_message(self.message)
+        self.one_rr_per_rrset = \
+            self.message._get_one_rr_per_rrset(self.one_rr_per_rrset)
+        self._get_question(MessageSection.QUESTION, qcount)
+        if self.question_only:
+            return self.message
+        self._get_section(MessageSection.ANSWER, ancount)
+        self._get_section(MessageSection.AUTHORITY, aucount)
+        self._get_section(MessageSection.ADDITIONAL, adcount)
+        if not self.ignore_trailing and self.parser.remaining() != 0:
+            raise TrailingJunk
+        if self.multi and self.message.tsig_ctx and not self.message.had_tsig:
+            self.message.tsig_ctx.update(self.parser.wire)
+        return self.message
+
+
+def from_wire(wire, keyring=None, request_mac=b'', xfr=False, origin=None,
+              tsig_ctx=None, multi=False,
+              question_only=False, one_rr_per_rrset=False,
+              ignore_trailing=False, raise_on_truncation=False):
+    """Convert a DNS wire format message into a message
+    object.
+
+    *keyring*, a ``dns.tsig.Key`` or ``dict``, the key or keyring to use
+    if the message is signed.
+
+    *request_mac*, a ``bytes``.  If the message is a response to a
+    TSIG-signed request, *request_mac* should be set to the MAC of
+    that request.
+
+    *xfr*, a ``bool``, should be set to ``True`` if this message is part of
+    a zone transfer.
+
+    *origin*, a ``dns.name.Name`` or ``None``.  If the message is part
+    of a zone transfer, *origin* should be the origin name of the
+    zone.  If not ``None``, names will be relativized to the origin.
+
+    *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the
+    ongoing TSIG context, used when validating zone transfers.
+
+    *multi*, a ``bool``, should be set to ``True`` if this message is
+    part of a multiple message sequence.
+
+    *question_only*, a ``bool``.  If ``True``, read only up to
+    the end of the question section.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its
+    own RRset.
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the message.
+
+    *raise_on_truncation*, a ``bool``.  If ``True``, raise an exception if
+    the TC bit is set.
+
+    Raises ``dns.message.ShortHeader`` if the message is less than 12 octets
+    long.
+
+    Raises ``dns.message.TrailingJunk`` if there were octets in the message
+    past the end of the proper DNS message, and *ignore_trailing* is ``False``.
+
+    Raises ``dns.message.BadEDNS`` if an OPT record was in the
+    wrong section, or occurred more than once.
+
+    Raises ``dns.message.BadTSIG`` if a TSIG record was not the last
+    record of the additional data section.
+
+    Raises ``dns.message.Truncated`` if the TC flag is set and
+    *raise_on_truncation* is ``True``.
+
+    Returns a ``dns.message.Message``.
+    """
+
+    def initialize_message(message):
+        message.request_mac = request_mac
+        message.xfr = xfr
+        message.origin = origin
+        message.tsig_ctx = tsig_ctx
+
+    reader = _WireReader(wire, initialize_message, question_only,
+                         one_rr_per_rrset, ignore_trailing, keyring, multi)
+    try:
+        m = reader.read()
+    except dns.exception.FormError:
+        if reader.message and (reader.message.flags & dns.flags.TC) and \
+           raise_on_truncation:
+            raise Truncated(message=reader.message)
+        else:
+            raise
+    # Reading a truncated message might not have any errors, so we
+    # have to do this check here too.
+    if m.flags & dns.flags.TC and raise_on_truncation:
+        raise Truncated(message=m)
+
+    return m
+
+
+class _TextReader:
+
+    """Text format reader.
+
+    tok: the tokenizer.
+    message: The message object being built.
+    DNS dynamic updates.
+    last_name: The most recently read name when building a message object.
+    one_rr_per_rrset: Put each RR into its own RRset?
+    origin: The origin for relative names
+    relativize: relativize names?
+    relativize_to: the origin to relativize to.
+    """
+
+    def __init__(self, text, idna_codec, one_rr_per_rrset=False,
+                 origin=None, relativize=True, relativize_to=None):
+        self.message = None
+        self.tok = dns.tokenizer.Tokenizer(text, idna_codec=idna_codec)
+        self.last_name = None
+        self.one_rr_per_rrset = one_rr_per_rrset
+        self.origin = origin
+        self.relativize = relativize
+        self.relativize_to = relativize_to
+        self.id = None
+        self.edns = -1
+        self.ednsflags = 0
+        self.payload = DEFAULT_EDNS_PAYLOAD
+        self.rcode = None
+        self.opcode = dns.opcode.QUERY
+        self.flags = 0
+
+    def _header_line(self, _):
+        """Process one line from the text format header section."""
+
+        token = self.tok.get()
+        what = token.value
+        if what == 'id':
+            self.id = self.tok.get_int()
+        elif what == 'flags':
+            while True:
+                token = self.tok.get()
+                if not token.is_identifier():
+                    self.tok.unget(token)
+                    break
+                self.flags = self.flags | dns.flags.from_text(token.value)
+        elif what == 'edns':
+            self.edns = self.tok.get_int()
+            self.ednsflags = self.ednsflags | (self.edns << 16)
+        elif what == 'eflags':
+            if self.edns < 0:
+                self.edns = 0
+            while True:
+                token = self.tok.get()
+                if not token.is_identifier():
+                    self.tok.unget(token)
+                    break
+                self.ednsflags = self.ednsflags | \
+                    dns.flags.edns_from_text(token.value)
+        elif what == 'payload':
+            self.payload = self.tok.get_int()
+            if self.edns < 0:
+                self.edns = 0
+        elif what == 'opcode':
+            text = self.tok.get_string()
+            self.opcode = dns.opcode.from_text(text)
+            self.flags = self.flags | dns.opcode.to_flags(self.opcode)
+        elif what == 'rcode':
+            text = self.tok.get_string()
+            self.rcode = dns.rcode.from_text(text)
+        else:
+            raise UnknownHeaderField
+        self.tok.get_eol()
+
+    def _question_line(self, section_number):
+        """Process one line from the text format question section."""
+
+        section = self.message.sections[section_number]
+        token = self.tok.get(want_leading=True)
+        if not token.is_whitespace():
+            self.last_name = self.tok.as_name(token, self.message.origin,
+                                              self.relativize,
+                                              self.relativize_to)
+        name = self.last_name
+        if name is None:
+            raise NoPreviousName
+        token = self.tok.get()
+        if not token.is_identifier():
+            raise dns.exception.SyntaxError
+        # Class
+        try:
+            rdclass = dns.rdataclass.from_text(token.value)
+            token = self.tok.get()
+            if not token.is_identifier():
+                raise dns.exception.SyntaxError
+        except dns.exception.SyntaxError:
+            raise dns.exception.SyntaxError
+        except Exception:
+            rdclass = dns.rdataclass.IN
+        # Type
+        rdtype = dns.rdatatype.from_text(token.value)
+        (rdclass, rdtype, _, _) = \
+            self.message._parse_rr_header(section_number, name, rdclass, rdtype)
+        self.message.find_rrset(section, name, rdclass, rdtype, create=True,
+                                force_unique=True)
+        self.tok.get_eol()
+
+    def _rr_line(self, section_number):
+        """Process one line from the text format answer, authority, or
+        additional data sections.
+        """
+
+        section = self.message.sections[section_number]
+        # Name
+        token = self.tok.get(want_leading=True)
+        if not token.is_whitespace():
+            self.last_name = self.tok.as_name(token, self.message.origin,
+                                              self.relativize,
+                                              self.relativize_to)
+        name = self.last_name
+        if name is None:
+            raise NoPreviousName
+        token = self.tok.get()
+        if not token.is_identifier():
+            raise dns.exception.SyntaxError
+        # TTL
+        try:
+            ttl = int(token.value, 0)
+            token = self.tok.get()
+            if not token.is_identifier():
+                raise dns.exception.SyntaxError
+        except dns.exception.SyntaxError:
+            raise dns.exception.SyntaxError
+        except Exception:
+            ttl = 0
+        # Class
+        try:
+            rdclass = dns.rdataclass.from_text(token.value)
+            token = self.tok.get()
+            if not token.is_identifier():
+                raise dns.exception.SyntaxError
+        except dns.exception.SyntaxError:
+            raise dns.exception.SyntaxError
+        except Exception:
+            rdclass = dns.rdataclass.IN
+        # Type
+        rdtype = dns.rdatatype.from_text(token.value)
+        (rdclass, rdtype, deleting, empty) = \
+            self.message._parse_rr_header(section_number, name, rdclass, rdtype)
+        token = self.tok.get()
+        if empty and not token.is_eol_or_eof():
+            raise dns.exception.SyntaxError
+        if not empty and token.is_eol_or_eof():
+            raise dns.exception.UnexpectedEnd
+        if not token.is_eol_or_eof():
+            self.tok.unget(token)
+            rd = dns.rdata.from_text(rdclass, rdtype, self.tok,
+                                     self.message.origin, self.relativize,
+                                     self.relativize_to)
+            covers = rd.covers()
+        else:
+            rd = None
+            covers = dns.rdatatype.NONE
+        rrset = self.message.find_rrset(section, name,
+                                        rdclass, rdtype, covers,
+                                        deleting, True, self.one_rr_per_rrset)
+        if rd is not None:
+            rrset.add(rd, ttl)
+
+    def _make_message(self):
+        factory = _message_factory_from_opcode(self.opcode)
+        message = factory(id=self.id)
+        message.flags = self.flags
+        if self.edns >= 0:
+            message.use_edns(self.edns, self.ednsflags, self.payload)
+        if self.rcode:
+            message.set_rcode(self.rcode)
+        if self.origin:
+            message.origin = self.origin
+        return message
+
+    def read(self):
+        """Read a text format DNS message and build a dns.message.Message
+        object."""
+
+        line_method = self._header_line
+        section_number = None
+        while 1:
+            token = self.tok.get(True, True)
+            if token.is_eol_or_eof():
+                break
+            if token.is_comment():
+                u = token.value.upper()
+                if u == 'HEADER':
+                    line_method = self._header_line
+
+                if self.message:
+                    message = self.message
+                else:
+                    # If we don't have a message, create one with the current
+                    # opcode, so that we know which section names to parse.
+                    message = self._make_message()
+                try:
+                    section_number = message._section_enum.from_text(u)
+                    # We found a section name.  If we don't have a message,
+                    # use the one we just created.
+                    if not self.message:
+                        self.message = message
+                        self.one_rr_per_rrset = \
+                            message._get_one_rr_per_rrset(self.one_rr_per_rrset)
+                    if section_number == MessageSection.QUESTION:
+                        line_method = self._question_line
+                    else:
+                        line_method = self._rr_line
+                except Exception:
+                    # It's just a comment.
+                    pass
+                self.tok.get_eol()
+                continue
+            self.tok.unget(token)
+            line_method(section_number)
+        if not self.message:
+            self.message = self._make_message()
+        return self.message
+
+
+def from_text(text, idna_codec=None, one_rr_per_rrset=False,
+              origin=None, relativize=True, relativize_to=None):
+    """Convert the text format message into a message object.
+
+    The reader stops after reading the first blank line in the input to
+    facilitate reading multiple messages from a single file with
+    ``dns.message.from_file()``.
+
+    *text*, a ``str``, the text format message.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
+    is used.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, then each RR is put
+    into its own rrset.  The default is ``False``.
+
+    *origin*, a ``dns.name.Name`` (or ``None``), the
+    origin to use for relative names.
+
+    *relativize*, a ``bool``.  If true, name will be relativized.
+
+    *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+    when relativizing names.  If not set, the *origin* value will be used.
+
+    Raises ``dns.message.UnknownHeaderField`` if a header is unknown.
+
+    Raises ``dns.exception.SyntaxError`` if the text is badly formed.
+
+    Returns a ``dns.message.Message object``
+    """
+
+    # 'text' can also be a file, but we don't publish that fact
+    # since it's an implementation detail.  The official file
+    # interface is from_file().
+
+    reader = _TextReader(text, idna_codec, one_rr_per_rrset, origin,
+                         relativize, relativize_to)
+    return reader.read()
+
+
+def from_file(f, idna_codec=None, one_rr_per_rrset=False):
+    """Read the next text format message from the specified file.
+
+    Message blocks are separated by a single blank line.
+
+    *f*, a ``file`` or ``str``.  If *f* is text, it is treated as the
+    pathname of a file to open.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
+    is used.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, then each RR is put
+    into its own rrset.  The default is ``False``.
+
+    Raises ``dns.message.UnknownHeaderField`` if a header is unknown.
+
+    Raises ``dns.exception.SyntaxError`` if the text is badly formed.
+
+    Returns a ``dns.message.Message object``
+    """
+
+    with contextlib.ExitStack() as stack:
+        if isinstance(f, str):
+            f = stack.enter_context(open(f))
+        return from_text(f, idna_codec, one_rr_per_rrset)
+
+
+def make_query(qname, rdtype, rdclass=dns.rdataclass.IN, use_edns=None,
+               want_dnssec=False, ednsflags=None, payload=None,
+               request_payload=None, options=None, idna_codec=None):
+    """Make a query message.
+
+    The query name, type, and class may all be specified either
+    as objects of the appropriate type, or as strings.
+
+    The query will have a randomly chosen query id, and its DNS flags
+    will be set to dns.flags.RD.
+
+    qname, a ``dns.name.Name`` or ``str``, the query name.
+
+    *rdtype*, an ``int`` or ``str``, the desired rdata type.
+
+    *rdclass*, an ``int`` or ``str``,  the desired rdata class; the default
+    is class IN.
+
+    *use_edns*, an ``int``, ``bool`` or ``None``.  The EDNS level to use; the
+    default is None (no EDNS).
+    See the description of dns.message.Message.use_edns() for the possible
+    values for use_edns and their meanings.
+
+    *want_dnssec*, a ``bool``.  If ``True``, DNSSEC data is desired.
+
+    *ednsflags*, an ``int``, the EDNS flag values.
+
+    *payload*, an ``int``, is the EDNS sender's payload field, which is the
+    maximum size of UDP datagram the sender can handle.  I.e. how big
+    a response to this message can be.
+
+    *request_payload*, an ``int``, is the EDNS payload size to use when
+    sending this message.  If not specified, defaults to the value of
+    *payload*.
+
+    *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS
+    options.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
+    is used.
+
+    Returns a ``dns.message.QueryMessage``
+    """
+
+    if isinstance(qname, str):
+        qname = dns.name.from_text(qname, idna_codec=idna_codec)
+    rdtype = dns.rdatatype.RdataType.make(rdtype)
+    rdclass = dns.rdataclass.RdataClass.make(rdclass)
+    m = QueryMessage()
+    m.flags |= dns.flags.RD
+    m.find_rrset(m.question, qname, rdclass, rdtype, create=True,
+                 force_unique=True)
+    # only pass keywords on to use_edns if they have been set to a
+    # non-None value.  Setting a field will turn EDNS on if it hasn't
+    # been configured.
+    kwargs = {}
+    if ednsflags is not None:
+        kwargs['ednsflags'] = ednsflags
+    if payload is not None:
+        kwargs['payload'] = payload
+    if request_payload is not None:
+        kwargs['request_payload'] = request_payload
+    if options is not None:
+        kwargs['options'] = options
+    if kwargs and use_edns is None:
+        use_edns = 0
+    kwargs['edns'] = use_edns
+    m.use_edns(**kwargs)
+    m.want_dnssec(want_dnssec)
+    return m
+
+
+def make_response(query, recursion_available=False, our_payload=8192,
+                  fudge=300, tsig_error=0):
+    """Make a message which is a response for the specified query.
+    The message returned is really a response skeleton; it has all
+    of the infrastructure required of a response, but none of the
+    content.
+
+    The response's question section is a shallow copy of the query's
+    question section, so the query's question RRsets should not be
+    changed.
+
+    *query*, a ``dns.message.Message``, the query to respond to.
+
+    *recursion_available*, a ``bool``, should RA be set in the response?
+
+    *our_payload*, an ``int``, the payload size to advertise in EDNS
+    responses.
+
+    *fudge*, an ``int``, the TSIG time fudge.
+
+    *tsig_error*, an ``int``, the TSIG error.
+
+    Returns a ``dns.message.Message`` object whose specific class is
+    appropriate for the query.  For example, if query is a
+    ``dns.update.UpdateMessage``, response will be too.
+    """
+
+    if query.flags & dns.flags.QR:
+        raise dns.exception.FormError('specified query message is not a query')
+    factory = _message_factory_from_opcode(query.opcode())
+    response = factory(id=query.id)
+    response.flags = dns.flags.QR | (query.flags & dns.flags.RD)
+    if recursion_available:
+        response.flags |= dns.flags.RA
+    response.set_opcode(query.opcode())
+    response.question = list(query.question)
+    if query.edns >= 0:
+        response.use_edns(0, 0, our_payload, query.payload)
+    if query.had_tsig:
+        response.use_tsig(query.keyring, query.keyname, fudge, None,
+                          tsig_error, b'', query.keyalgorithm)
+        response.request_mac = query.mac
+    return response
+
+### BEGIN generated MessageSection constants
+
+QUESTION = MessageSection.QUESTION
+ANSWER = MessageSection.ANSWER
+AUTHORITY = MessageSection.AUTHORITY
+ADDITIONAL = MessageSection.ADDITIONAL
+
+### END generated MessageSection constants

=== added file 'dns/message.pyi'
--- old/dns/message.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/message.pyi	2021-04-07 01:51:59 +0000
@@ -0,0 +1,47 @@
+from typing import Optional, Dict, List, Tuple, Union
+from . import name, rrset, tsig, rdatatype, entropy, edns, rdataclass, rcode
+import hmac
+
+class Message:
+    def to_wire(self, origin : Optional[name.Name]=None, max_size=0, **kw) -> bytes:
+        ...
+    def find_rrset(self, section : List[rrset.RRset], name : name.Name, rdclass : int, rdtype : int,
+                   covers=rdatatype.NONE, deleting : Optional[int]=None, create=False,
+                   force_unique=False) -> rrset.RRset:
+        ...
+    def __init__(self, id : Optional[int] =None) -> None:
+        self.id : int
+        self.flags = 0
+        self.sections : List[List[rrset.RRset]] = [[], [], [], []]
+        self.opt : rrset.RRset = None
+        self.request_payload = 0
+        self.keyring = None
+        self.tsig : rrset.RRset = None
+        self.request_mac = b''
+        self.xfr = False
+        self.origin = None
+        self.tsig_ctx = None
+        self.index : Dict[Tuple[rrset.RRset, name.Name, int, int, Union[int,str], int], rrset.RRset] = {}
+
+    def is_response(self, other : Message) -> bool:
+        ...
+
+    def set_rcode(self, rcode : rcode.Rcode):
+        ...
+
+def from_text(a : str, idna_codec : Optional[name.IDNACodec] = None) -> Message:
+    ...
+
+def from_wire(wire, keyring : Optional[Dict[name.Name,bytes]] = None, request_mac = b'', xfr=False, origin=None,
+              tsig_ctx : Optional[Union[dns.tsig.HMACTSig, dns.tsig.GSSTSig]] = None, multi=False,
+              question_only=False, one_rr_per_rrset=False,
+              ignore_trailing=False) -> Message:
+    ...
+def make_response(query : Message, recursion_available=False, our_payload=8192,
+                  fudge=300) -> Message:
+    ...
+
+def make_query(qname : Union[name.Name,str], rdtype : Union[str,int], rdclass : Union[int,str] =rdataclass.IN, use_edns : Optional[bool] = None,
+               want_dnssec=False, ednsflags : Optional[int] = None, payload : Optional[int] = None,
+               request_payload : Optional[int] = None, options : Optional[List[edns.Option]] = None) -> Message:
+    ...

=== added file 'dns/name.py'
--- old/dns/name.py	1970-01-01 00:00:00 +0000
+++ new/dns/name.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,1018 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Names.
+"""
+
+import copy
+import struct
+
+import encodings.idna    # type: ignore
+try:
+    import idna          # type: ignore
+    have_idna_2008 = True
+except ImportError:  # pragma: no cover
+    have_idna_2008 = False
+
+import dns.wire
+import dns.exception
+import dns.immutable
+
+# fullcompare() result values
+
+#: The compared names have no relationship to each other.
+NAMERELN_NONE = 0
+#: the first name is a superdomain of the second.
+NAMERELN_SUPERDOMAIN = 1
+#: The first name is a subdomain of the second.
+NAMERELN_SUBDOMAIN = 2
+#: The compared names are equal.
+NAMERELN_EQUAL = 3
+#: The compared names have a common ancestor.
+NAMERELN_COMMONANCESTOR = 4
+
+
+class EmptyLabel(dns.exception.SyntaxError):
+    """A DNS label is empty."""
+
+
+class BadEscape(dns.exception.SyntaxError):
+    """An escaped code in a text format of DNS name is invalid."""
+
+
+class BadPointer(dns.exception.FormError):
+    """A DNS compression pointer points forward instead of backward."""
+
+
+class BadLabelType(dns.exception.FormError):
+    """The label type in DNS name wire format is unknown."""
+
+
+class NeedAbsoluteNameOrOrigin(dns.exception.DNSException):
+    """An attempt was made to convert a non-absolute name to
+    wire when there was also a non-absolute (or missing) origin."""
+
+
+class NameTooLong(dns.exception.FormError):
+    """A DNS name is > 255 octets long."""
+
+
+class LabelTooLong(dns.exception.SyntaxError):
+    """A DNS label is > 63 octets long."""
+
+
+class AbsoluteConcatenation(dns.exception.DNSException):
+    """An attempt was made to append anything other than the
+    empty name to an absolute DNS name."""
+
+
+class NoParent(dns.exception.DNSException):
+    """An attempt was made to get the parent of the root name
+    or the empty name."""
+
+class NoIDNA2008(dns.exception.DNSException):
+    """IDNA 2008 processing was requested but the idna module is not
+    available."""
+
+
+class IDNAException(dns.exception.DNSException):
+    """IDNA processing raised an exception."""
+
+    supp_kwargs = {'idna_exception'}
+    fmt = "IDNA processing exception: {idna_exception}"
+
+
+class IDNACodec:
+    """Abstract base class for IDNA encoder/decoders."""
+
+    def __init__(self):
+        pass
+
+    def is_idna(self, label):
+        return label.lower().startswith(b'xn--')
+
+    def encode(self, label):
+        raise NotImplementedError  # pragma: no cover
+
+    def decode(self, label):
+        # We do not apply any IDNA policy on decode.
+        if self.is_idna(label):
+            try:
+                label = label[4:].decode('punycode')
+            except Exception as e:
+                raise IDNAException(idna_exception=e)
+        return _escapify(label)
+
+
+class IDNA2003Codec(IDNACodec):
+    """IDNA 2003 encoder/decoder."""
+
+    def __init__(self, strict_decode=False):
+        """Initialize the IDNA 2003 encoder/decoder.
+
+        *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking
+        is done when decoding.  This can cause failures if the name
+        was encoded with IDNA2008.  The default is `False`.
+        """
+
+        super().__init__()
+        self.strict_decode = strict_decode
+
+    def encode(self, label):
+        """Encode *label*."""
+
+        if label == '':
+            return b''
+        try:
+            return encodings.idna.ToASCII(label)
+        except UnicodeError:
+            raise LabelTooLong
+
+    def decode(self, label):
+        """Decode *label*."""
+        if not self.strict_decode:
+            return super().decode(label)
+        if label == b'':
+            return ''
+        try:
+            return _escapify(encodings.idna.ToUnicode(label))
+        except Exception as e:
+            raise IDNAException(idna_exception=e)
+
+
+class IDNA2008Codec(IDNACodec):
+    """IDNA 2008 encoder/decoder.
+    """
+
+    def __init__(self, uts_46=False, transitional=False,
+                 allow_pure_ascii=False, strict_decode=False):
+        """Initialize the IDNA 2008 encoder/decoder.
+
+        *uts_46* is a ``bool``.  If True, apply Unicode IDNA
+        compatibility processing as described in Unicode Technical
+        Standard #46 (http://unicode.org/reports/tr46/).
+        If False, do not apply the mapping.  The default is False.
+
+        *transitional* is a ``bool``: If True, use the
+        "transitional" mode described in Unicode Technical Standard
+        #46.  The default is False.
+
+        *allow_pure_ascii* is a ``bool``.  If True, then a label which
+        consists of only ASCII characters is allowed.  This is less
+        strict than regular IDNA 2008, but is also necessary for mixed
+        names, e.g. a name with starting with "_sip._tcp." and ending
+        in an IDN suffix which would otherwise be disallowed.  The
+        default is False.
+
+        *strict_decode* is a ``bool``: If True, then IDNA2008 checking
+        is done when decoding.  This can cause failures if the name
+        was encoded with IDNA2003.  The default is False.
+        """
+        super().__init__()
+        self.uts_46 = uts_46
+        self.transitional = transitional
+        self.allow_pure_ascii = allow_pure_ascii
+        self.strict_decode = strict_decode
+
+    def encode(self, label):
+        if label == '':
+            return b''
+        if self.allow_pure_ascii and is_all_ascii(label):
+            encoded = label.encode('ascii')
+            if len(encoded) > 63:
+                raise LabelTooLong
+            return encoded
+        if not have_idna_2008:
+            raise NoIDNA2008
+        try:
+            if self.uts_46:
+                label = idna.uts46_remap(label, False, self.transitional)
+            return idna.alabel(label)
+        except idna.IDNAError as e:
+            if e.args[0] == 'Label too long':
+                raise LabelTooLong
+            else:
+                raise IDNAException(idna_exception=e)
+
+    def decode(self, label):
+        if not self.strict_decode:
+            return super().decode(label)
+        if label == b'':
+            return ''
+        if not have_idna_2008:
+            raise NoIDNA2008
+        try:
+            ulabel = idna.ulabel(label)
+            if self.uts_46:
+                ulabel = idna.uts46_remap(ulabel, False, self.transitional)
+            return _escapify(ulabel)
+        except (idna.IDNAError, UnicodeError) as e:
+            raise IDNAException(idna_exception=e)
+
+_escaped = b'"().;\\@$'
+_escaped_text = '"().;\\@$'
+
+IDNA_2003_Practical = IDNA2003Codec(False)
+IDNA_2003_Strict = IDNA2003Codec(True)
+IDNA_2003 = IDNA_2003_Practical
+IDNA_2008_Practical = IDNA2008Codec(True, False, True, False)
+IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False)
+IDNA_2008_Strict = IDNA2008Codec(False, False, False, True)
+IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False)
+IDNA_2008 = IDNA_2008_Practical
+
+def _escapify(label):
+    """Escape the characters in label which need it.
+    @returns: the escaped string
+    @rtype: string"""
+    if isinstance(label, bytes):
+        # Ordinary DNS label mode.  Escape special characters and values
+        # < 0x20 or > 0x7f.
+        text = ''
+        for c in label:
+            if c in _escaped:
+                text += '\\' + chr(c)
+            elif c > 0x20 and c < 0x7F:
+                text += chr(c)
+            else:
+                text += '\\%03d' % c
+        return text
+
+    # Unicode label mode.  Escape only special characters and values < 0x20
+    text = ''
+    for c in label:
+        if c in _escaped_text:
+            text += '\\' + c
+        elif c <= '\x20':
+            text += '\\%03d' % ord(c)
+        else:
+            text += c
+    return text
+
+def _validate_labels(labels):
+    """Check for empty labels in the middle of a label sequence,
+    labels that are too long, and for too many labels.
+
+    Raises ``dns.name.NameTooLong`` if the name as a whole is too long.
+
+    Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root
+    label) and appears in a position other than the end of the label
+    sequence
+
+    """
+
+    l = len(labels)
+    total = 0
+    i = -1
+    j = 0
+    for label in labels:
+        ll = len(label)
+        total += ll + 1
+        if ll > 63:
+            raise LabelTooLong
+        if i < 0 and label == b'':
+            i = j
+        j += 1
+    if total > 255:
+        raise NameTooLong
+    if i >= 0 and i != l - 1:
+        raise EmptyLabel
+
+
+def _maybe_convert_to_binary(label):
+    """If label is ``str``, convert it to ``bytes``.  If it is already
+    ``bytes`` just return it.
+
+    """
+
+    if isinstance(label, bytes):
+        return label
+    if isinstance(label, str):
+        return label.encode()
+    raise ValueError  # pragma: no cover
+
+
+@dns.immutable.immutable
+class Name:
+
+    """A DNS name.
+
+    The dns.name.Name class represents a DNS name as a tuple of
+    labels.  Each label is a ``bytes`` in DNS wire format.  Instances
+    of the class are immutable.
+    """
+
+    __slots__ = ['labels']
+
+    def __init__(self, labels):
+        """*labels* is any iterable whose values are ``str`` or ``bytes``.
+        """
+
+        labels = [_maybe_convert_to_binary(x) for x in labels]
+        self.labels = tuple(labels)
+        _validate_labels(self.labels)
+
+    def __copy__(self):
+        return Name(self.labels)
+
+    def __deepcopy__(self, memo):
+        return Name(copy.deepcopy(self.labels, memo))
+
+    def __getstate__(self):
+        # Names can be pickled
+        return {'labels': self.labels}
+
+    def __setstate__(self, state):
+        super().__setattr__('labels', state['labels'])
+        _validate_labels(self.labels)
+
+    def is_absolute(self):
+        """Is the most significant label of this name the root label?
+
+        Returns a ``bool``.
+        """
+
+        return len(self.labels) > 0 and self.labels[-1] == b''
+
+    def is_wild(self):
+        """Is this name wild?  (I.e. Is the least significant label '*'?)
+
+        Returns a ``bool``.
+        """
+
+        return len(self.labels) > 0 and self.labels[0] == b'*'
+
+    def __hash__(self):
+        """Return a case-insensitive hash of the name.
+
+        Returns an ``int``.
+        """
+
+        h = 0
+        for label in self.labels:
+            for c in label.lower():
+                h += (h << 3) + c
+        return h
+
+    def fullcompare(self, other):
+        """Compare two names, returning a 3-tuple
+        ``(relation, order, nlabels)``.
+
+        *relation* describes the relation ship between the names,
+        and is one of: ``dns.name.NAMERELN_NONE``,
+        ``dns.name.NAMERELN_SUPERDOMAIN``, ``dns.name.NAMERELN_SUBDOMAIN``,
+        ``dns.name.NAMERELN_EQUAL``, or ``dns.name.NAMERELN_COMMONANCESTOR``.
+
+        *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and ==
+        0 if *self* == *other*.  A relative name is always less than an
+        absolute name.  If both names have the same relativity, then
+        the DNSSEC order relation is used to order them.
+
+        *nlabels* is the number of significant labels that the two names
+        have in common.
+
+        Here are some examples.  Names ending in "." are absolute names,
+        those not ending in "." are relative names.
+
+        =============  =============  ===========  =====  =======
+        self           other          relation     order  nlabels
+        =============  =============  ===========  =====  =======
+        www.example.   www.example.   equal        0      3
+        www.example.   example.       subdomain    > 0    2
+        example.       www.example.   superdomain  < 0    2
+        example1.com.  example2.com.  common anc.  < 0    2
+        example1       example2.      none         < 0    0
+        example1.      example2       none         > 0    0
+        =============  =============  ===========  =====  =======
+        """
+
+        sabs = self.is_absolute()
+        oabs = other.is_absolute()
+        if sabs != oabs:
+            if sabs:
+                return (NAMERELN_NONE, 1, 0)
+            else:
+                return (NAMERELN_NONE, -1, 0)
+        l1 = len(self.labels)
+        l2 = len(other.labels)
+        ldiff = l1 - l2
+        if ldiff < 0:
+            l = l1
+        else:
+            l = l2
+
+        order = 0
+        nlabels = 0
+        namereln = NAMERELN_NONE
+        while l > 0:
+            l -= 1
+            l1 -= 1
+            l2 -= 1
+            label1 = self.labels[l1].lower()
+            label2 = other.labels[l2].lower()
+            if label1 < label2:
+                order = -1
+                if nlabels > 0:
+                    namereln = NAMERELN_COMMONANCESTOR
+                return (namereln, order, nlabels)
+            elif label1 > label2:
+                order = 1
+                if nlabels > 0:
+                    namereln = NAMERELN_COMMONANCESTOR
+                return (namereln, order, nlabels)
+            nlabels += 1
+        order = ldiff
+        if ldiff < 0:
+            namereln = NAMERELN_SUPERDOMAIN
+        elif ldiff > 0:
+            namereln = NAMERELN_SUBDOMAIN
+        else:
+            namereln = NAMERELN_EQUAL
+        return (namereln, order, nlabels)
+
+    def is_subdomain(self, other):
+        """Is self a subdomain of other?
+
+        Note that the notion of subdomain includes equality, e.g.
+        "dnpython.org" is a subdomain of itself.
+
+        Returns a ``bool``.
+        """
+
+        (nr, _, _) = self.fullcompare(other)
+        if nr == NAMERELN_SUBDOMAIN or nr == NAMERELN_EQUAL:
+            return True
+        return False
+
+    def is_superdomain(self, other):
+        """Is self a superdomain of other?
+
+        Note that the notion of superdomain includes equality, e.g.
+        "dnpython.org" is a superdomain of itself.
+
+        Returns a ``bool``.
+        """
+
+        (nr, _, _) = self.fullcompare(other)
+        if nr == NAMERELN_SUPERDOMAIN or nr == NAMERELN_EQUAL:
+            return True
+        return False
+
+    def canonicalize(self):
+        """Return a name which is equal to the current name, but is in
+        DNSSEC canonical form.
+        """
+
+        return Name([x.lower() for x in self.labels])
+
+    def __eq__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] == 0
+        else:
+            return False
+
+    def __ne__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] != 0
+        else:
+            return True
+
+    def __lt__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] < 0
+        else:
+            return NotImplemented
+
+    def __le__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] <= 0
+        else:
+            return NotImplemented
+
+    def __ge__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] >= 0
+        else:
+            return NotImplemented
+
+    def __gt__(self, other):
+        if isinstance(other, Name):
+            return self.fullcompare(other)[1] > 0
+        else:
+            return NotImplemented
+
+    def __repr__(self):
+        return '<DNS name ' + self.__str__() + '>'
+
+    def __str__(self):
+        return self.to_text(False)
+
+    def to_text(self, omit_final_dot=False):
+        """Convert name to DNS text format.
+
+        *omit_final_dot* is a ``bool``.  If True, don't emit the final
+        dot (denoting the root label) for absolute names.  The default
+        is False.
+
+        Returns a ``str``.
+        """
+
+        if len(self.labels) == 0:
+            return '@'
+        if len(self.labels) == 1 and self.labels[0] == b'':
+            return '.'
+        if omit_final_dot and self.is_absolute():
+            l = self.labels[:-1]
+        else:
+            l = self.labels
+        s = '.'.join(map(_escapify, l))
+        return s
+
+    def to_unicode(self, omit_final_dot=False, idna_codec=None):
+        """Convert name to Unicode text format.
+
+        IDN ACE labels are converted to Unicode.
+
+        *omit_final_dot* is a ``bool``.  If True, don't emit the final
+        dot (denoting the root label) for absolute names.  The default
+        is False.
+        *idna_codec* specifies the IDNA encoder/decoder.  If None, the
+        dns.name.IDNA_2003_Practical encoder/decoder is used.
+        The IDNA_2003_Practical decoder does
+        not impose any policy, it just decodes punycode, so if you
+        don't want checking for compliance, you can use this decoder
+        for IDNA2008 as well.
+
+        Returns a ``str``.
+        """
+
+        if len(self.labels) == 0:
+            return '@'
+        if len(self.labels) == 1 and self.labels[0] == b'':
+            return '.'
+        if omit_final_dot and self.is_absolute():
+            l = self.labels[:-1]
+        else:
+            l = self.labels
+        if idna_codec is None:
+            idna_codec = IDNA_2003_Practical
+        return '.'.join([idna_codec.decode(x) for x in l])
+
+    def to_digestable(self, origin=None):
+        """Convert name to a format suitable for digesting in hashes.
+
+        The name is canonicalized and converted to uncompressed wire
+        format.  All names in wire format are absolute.  If the name
+        is a relative name, then an origin must be supplied.
+
+        *origin* is a ``dns.name.Name`` or ``None``.  If the name is
+        relative and origin is not ``None``, then origin will be appended
+        to the name.
+
+        Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
+        relative and no origin was provided.
+
+        Returns a ``bytes``.
+        """
+
+        return self.to_wire(origin=origin, canonicalize=True)
+
+    def to_wire(self, file=None, compress=None, origin=None,
+                canonicalize=False):
+        """Convert name to wire format, possibly compressing it.
+
+        *file* is the file where the name is emitted (typically an
+        io.BytesIO file).  If ``None`` (the default), a ``bytes``
+        containing the wire name will be returned.
+
+        *compress*, a ``dict``, is the compression table to use.  If
+        ``None`` (the default), names will not be compressed.  Note that
+        the compression code assumes that compression offset 0 is the
+        start of *file*, and thus compression will not be correct
+        if this is not the case.
+
+        *origin* is a ``dns.name.Name`` or ``None``.  If the name is
+        relative and origin is not ``None``, then *origin* will be appended
+        to it.
+
+        *canonicalize*, a ``bool``, indicates whether the name should
+        be canonicalized; that is, converted to a format suitable for
+        digesting in hashes.
+
+        Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is
+        relative and no origin was provided.
+
+        Returns a ``bytes`` or ``None``.
+        """
+
+        if file is None:
+            out = bytearray()
+            for label in self.labels:
+                out.append(len(label))
+                if canonicalize:
+                    out += label.lower()
+                else:
+                    out += label
+            if not self.is_absolute():
+                if origin is None or not origin.is_absolute():
+                    raise NeedAbsoluteNameOrOrigin
+                for label in origin.labels:
+                    out.append(len(label))
+                    if canonicalize:
+                        out += label.lower()
+                    else:
+                        out += label
+            return bytes(out)
+
+        if not self.is_absolute():
+            if origin is None or not origin.is_absolute():
+                raise NeedAbsoluteNameOrOrigin
+            labels = list(self.labels)
+            labels.extend(list(origin.labels))
+        else:
+            labels = self.labels
+        i = 0
+        for label in labels:
+            n = Name(labels[i:])
+            i += 1
+            if compress is not None:
+                pos = compress.get(n)
+            else:
+                pos = None
+            if pos is not None:
+                value = 0xc000 + pos
+                s = struct.pack('!H', value)
+                file.write(s)
+                break
+            else:
+                if compress is not None and len(n) > 1:
+                    pos = file.tell()
+                    if pos <= 0x3fff:
+                        compress[n] = pos
+                l = len(label)
+                file.write(struct.pack('!B', l))
+                if l > 0:
+                    if canonicalize:
+                        file.write(label.lower())
+                    else:
+                        file.write(label)
+
+    def __len__(self):
+        """The length of the name (in labels).
+
+        Returns an ``int``.
+        """
+
+        return len(self.labels)
+
+    def __getitem__(self, index):
+        return self.labels[index]
+
+    def __add__(self, other):
+        return self.concatenate(other)
+
+    def __sub__(self, other):
+        return self.relativize(other)
+
+    def split(self, depth):
+        """Split a name into a prefix and suffix names at the specified depth.
+
+        *depth* is an ``int`` specifying the number of labels in the suffix
+
+        Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the
+        name.
+
+        Returns the tuple ``(prefix, suffix)``.
+        """
+
+        l = len(self.labels)
+        if depth == 0:
+            return (self, dns.name.empty)
+        elif depth == l:
+            return (dns.name.empty, self)
+        elif depth < 0 or depth > l:
+            raise ValueError(
+                'depth must be >= 0 and <= the length of the name')
+        return (Name(self[: -depth]), Name(self[-depth:]))
+
+    def concatenate(self, other):
+        """Return a new name which is the concatenation of self and other.
+
+        Raises ``dns.name.AbsoluteConcatenation`` if the name is
+        absolute and *other* is not the empty name.
+
+        Returns a ``dns.name.Name``.
+        """
+
+        if self.is_absolute() and len(other) > 0:
+            raise AbsoluteConcatenation
+        labels = list(self.labels)
+        labels.extend(list(other.labels))
+        return Name(labels)
+
+    def relativize(self, origin):
+        """If the name is a subdomain of *origin*, return a new name which is
+        the name relative to origin.  Otherwise return the name.
+
+        For example, relativizing ``www.dnspython.org.`` to origin
+        ``dnspython.org.`` returns the name ``www``.  Relativizing ``example.``
+        to origin ``dnspython.org.`` returns ``example.``.
+
+        Returns a ``dns.name.Name``.
+        """
+
+        if origin is not None and self.is_subdomain(origin):
+            return Name(self[: -len(origin)])
+        else:
+            return self
+
+    def derelativize(self, origin):
+        """If the name is a relative name, return a new name which is the
+        concatenation of the name and origin.  Otherwise return the name.
+
+        For example, derelativizing ``www`` to origin ``dnspython.org.``
+        returns the name ``www.dnspython.org.``.  Derelativizing ``example.``
+        to origin ``dnspython.org.`` returns ``example.``.
+
+        Returns a ``dns.name.Name``.
+        """
+
+        if not self.is_absolute():
+            return self.concatenate(origin)
+        else:
+            return self
+
+    def choose_relativity(self, origin=None, relativize=True):
+        """Return a name with the relativity desired by the caller.
+
+        If *origin* is ``None``, then the name is returned.
+        Otherwise, if *relativize* is ``True`` the name is
+        relativized, and if *relativize* is ``False`` the name is
+        derelativized.
+
+        Returns a ``dns.name.Name``.
+        """
+
+        if origin:
+            if relativize:
+                return self.relativize(origin)
+            else:
+                return self.derelativize(origin)
+        else:
+            return self
+
+    def parent(self):
+        """Return the parent of the name.
+
+        For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``.
+
+        Raises ``dns.name.NoParent`` if the name is either the root name or the
+        empty name, and thus has no parent.
+
+        Returns a ``dns.name.Name``.
+        """
+
+        if self == root or self == empty:
+            raise NoParent
+        return Name(self.labels[1:])
+
+#: The root name, '.'
+root = Name([b''])
+
+#: The empty name.
+empty = Name([])
+
+def from_unicode(text, origin=root, idna_codec=None):
+    """Convert unicode text into a Name object.
+
+    Labels are encoded in IDN ACE form according to rules specified by
+    the IDNA codec.
+
+    *text*, a ``str``, is the text to convert into a name.
+
+    *origin*, a ``dns.name.Name``, specifies the origin to
+    append to non-absolute names.  The default is the root name.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
+    is used.
+
+    Returns a ``dns.name.Name``.
+    """
+
+    if not isinstance(text, str):
+        raise ValueError("input to from_unicode() must be a unicode string")
+    if not (origin is None or isinstance(origin, Name)):
+        raise ValueError("origin must be a Name or None")
+    labels = []
+    label = ''
+    escaping = False
+    edigits = 0
+    total = 0
+    if idna_codec is None:
+        idna_codec = IDNA_2003
+    if text == '@':
+        text = ''
+    if text:
+        if text in ['.', '\u3002', '\uff0e', '\uff61']:
+            return Name([b''])        # no Unicode "u" on this constant!
+        for c in text:
+            if escaping:
+                if edigits == 0:
+                    if c.isdigit():
+                        total = int(c)
+                        edigits += 1
+                    else:
+                        label += c
+                        escaping = False
+                else:
+                    if not c.isdigit():
+                        raise BadEscape
+                    total *= 10
+                    total += int(c)
+                    edigits += 1
+                    if edigits == 3:
+                        escaping = False
+                        label += chr(total)
+            elif c in ['.', '\u3002', '\uff0e', '\uff61']:
+                if len(label) == 0:
+                    raise EmptyLabel
+                labels.append(idna_codec.encode(label))
+                label = ''
+            elif c == '\\':
+                escaping = True
+                edigits = 0
+                total = 0
+            else:
+                label += c
+        if escaping:
+            raise BadEscape
+        if len(label) > 0:
+            labels.append(idna_codec.encode(label))
+        else:
+            labels.append(b'')
+
+    if (len(labels) == 0 or labels[-1] != b'') and origin is not None:
+        labels.extend(list(origin.labels))
+    return Name(labels)
+
+def is_all_ascii(text):
+    for c in text:
+        if ord(c) > 0x7f:
+            return False
+    return True
+
+def from_text(text, origin=root, idna_codec=None):
+    """Convert text into a Name object.
+
+    *text*, a ``str``, is the text to convert into a name.
+
+    *origin*, a ``dns.name.Name``, specifies the origin to
+    append to non-absolute names.  The default is the root name.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder.  If ``None``, the default IDNA 2003 encoder/decoder
+    is used.
+
+    Returns a ``dns.name.Name``.
+    """
+
+    if isinstance(text, str):
+        if not is_all_ascii(text):
+            # Some codepoint in the input text is > 127, so IDNA applies.
+            return from_unicode(text, origin, idna_codec)
+        # The input is all ASCII, so treat this like an ordinary non-IDNA
+        # domain name.  Note that "all ASCII" is about the input text,
+        # not the codepoints in the domain name.  E.g. if text has value
+        #
+        # r'\150\151\152\153\154\155\156\157\158\159'
+        #
+        # then it's still "all ASCII" even though the domain name has
+        # codepoints > 127.
+        text = text.encode('ascii')
+    if not isinstance(text, bytes):
+        raise ValueError("input to from_text() must be a string")
+    if not (origin is None or isinstance(origin, Name)):
+        raise ValueError("origin must be a Name or None")
+    labels = []
+    label = b''
+    escaping = False
+    edigits = 0
+    total = 0
+    if text == b'@':
+        text = b''
+    if text:
+        if text == b'.':
+            return Name([b''])
+        for c in text:
+            byte_ = struct.pack('!B', c)
+            if escaping:
+                if edigits == 0:
+                    if byte_.isdigit():
+                        total = int(byte_)
+                        edigits += 1
+                    else:
+                        label += byte_
+                        escaping = False
+                else:
+                    if not byte_.isdigit():
+                        raise BadEscape
+                    total *= 10
+                    total += int(byte_)
+                    edigits += 1
+                    if edigits == 3:
+                        escaping = False
+                        label += struct.pack('!B', total)
+            elif byte_ == b'.':
+                if len(label) == 0:
+                    raise EmptyLabel
+                labels.append(label)
+                label = b''
+            elif byte_ == b'\\':
+                escaping = True
+                edigits = 0
+                total = 0
+            else:
+                label += byte_
+        if escaping:
+            raise BadEscape
+        if len(label) > 0:
+            labels.append(label)
+        else:
+            labels.append(b'')
+    if (len(labels) == 0 or labels[-1] != b'') and origin is not None:
+        labels.extend(list(origin.labels))
+    return Name(labels)
+
+
+def from_wire_parser(parser):
+    """Convert possibly compressed wire format into a Name.
+
+    *parser* is a dns.wire.Parser.
+
+    Raises ``dns.name.BadPointer`` if a compression pointer did not
+    point backwards in the message.
+
+    Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
+
+    Returns a ``dns.name.Name``
+    """
+
+    labels = []
+    biggest_pointer = parser.current
+    with parser.restore_furthest():
+        count = parser.get_uint8()
+        while count != 0:
+            if count < 64:
+                labels.append(parser.get_bytes(count))
+            elif count >= 192:
+                current = (count & 0x3f) * 256 + parser.get_uint8()
+                if current >= biggest_pointer:
+                    raise BadPointer
+                biggest_pointer = current
+                parser.seek(current)
+            else:
+                raise BadLabelType
+            count = parser.get_uint8()
+        labels.append(b'')
+    return Name(labels)
+
+
+def from_wire(message, current):
+    """Convert possibly compressed wire format into a Name.
+
+    *message* is a ``bytes`` containing an entire DNS message in DNS
+    wire form.
+
+    *current*, an ``int``, is the offset of the beginning of the name
+    from the start of the message
+
+    Raises ``dns.name.BadPointer`` if a compression pointer did not
+    point backwards in the message.
+
+    Raises ``dns.name.BadLabelType`` if an invalid label type was encountered.
+
+    Returns a ``(dns.name.Name, int)`` tuple consisting of the name
+    that was read and the number of bytes of the wire format message
+    which were consumed reading it.
+    """
+
+    if not isinstance(message, bytes):
+        raise ValueError("input to from_wire() must be a byte string")
+    parser = dns.wire.Parser(message, current)
+    name = from_wire_parser(parser)
+    return (name, parser.current - current)

=== added file 'dns/name.pyi'
--- old/dns/name.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/name.pyi	2021-04-07 01:51:59 +0000
@@ -0,0 +1,40 @@
+from typing import Optional, Union, Tuple, Iterable, List
+
+have_idna_2008: bool
+
+class Name:
+    def is_subdomain(self, o : Name) -> bool: ...
+    def is_superdomain(self, o : Name) -> bool: ...
+    def __init__(self, labels : Iterable[Union[bytes,str]]) -> None:
+        self.labels : List[bytes]
+    def is_absolute(self) -> bool: ...
+    def is_wild(self) -> bool: ...
+    def fullcompare(self, other) -> Tuple[int,int,int]: ...
+    def canonicalize(self) -> Name: ...
+    def __eq__(self, other) -> bool: ...
+    def __ne__(self, other) -> bool: ...
+    def __lt__(self, other : Name) -> bool: ...
+    def __le__(self, other : Name) -> bool: ...
+    def __ge__(self, other : Name) -> bool: ...
+    def __gt__(self, other : Name) -> bool: ...
+    def to_text(self, omit_final_dot=False) -> str: ...
+    def to_unicode(self, omit_final_dot=False, idna_codec=None) -> str: ...
+    def to_digestable(self, origin=None) -> bytes: ...
+    def to_wire(self, file=None, compress=None, origin=None,
+                canonicalize=False) -> Optional[bytes]: ...
+    def __add__(self, other : Name) -> Name: ...
+    def __sub__(self, other : Name) -> Name: ...
+    def split(self, depth) -> List[Tuple[str,str]]: ...
+    def concatenate(self, other : Name) -> Name: ...
+    def relativize(self, origin) -> Name: ...
+    def derelativize(self, origin) -> Name: ...
+    def choose_relativity(self, origin : Optional[Name] = None, relativize=True) -> Name: ...
+    def parent(self) -> Name: ...
+
+class IDNACodec:
+    pass
+
+def from_text(text, origin : Optional[Name] = Name('.'), idna_codec : Optional[IDNACodec] = None) -> Name:
+    ...
+
+empty : Name

=== added file 'dns/namedict.py'
--- old/dns/namedict.py	1970-01-01 00:00:00 +0000
+++ new/dns/namedict.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,108 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+# Copyright (C) 2016 Coresec Systems AB
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND CORESEC SYSTEMS AB DISCLAIMS ALL
+# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CORESEC
+# SYSTEMS AB BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS name dictionary"""
+
+from collections.abc import MutableMapping
+
+import dns.name
+
+
+class NameDict(MutableMapping):
+    """A dictionary whose keys are dns.name.Name objects.
+
+    In addition to being like a regular Python dictionary, this
+    dictionary can also get the deepest match for a given key.
+    """
+
+    __slots__ = ["max_depth", "max_depth_items", "__store"]
+
+    def __init__(self, *args, **kwargs):
+        super().__init__()
+        self.__store = dict()
+        #: the maximum depth of the keys that have ever been added
+        self.max_depth = 0
+        #: the number of items of maximum depth
+        self.max_depth_items = 0
+        self.update(dict(*args, **kwargs))
+
+    def __update_max_depth(self, key):
+        if len(key) == self.max_depth:
+            self.max_depth_items = self.max_depth_items + 1
+        elif len(key) > self.max_depth:
+            self.max_depth = len(key)
+            self.max_depth_items = 1
+
+    def __getitem__(self, key):
+        return self.__store[key]
+
+    def __setitem__(self, key, value):
+        if not isinstance(key, dns.name.Name):
+            raise ValueError('NameDict key must be a name')
+        self.__store[key] = value
+        self.__update_max_depth(key)
+
+    def __delitem__(self, key):
+        self.__store.pop(key)
+        if len(key) == self.max_depth:
+            self.max_depth_items = self.max_depth_items - 1
+        if self.max_depth_items == 0:
+            self.max_depth = 0
+            for k in self.__store:
+                self.__update_max_depth(k)
+
+    def __iter__(self):
+        return iter(self.__store)
+
+    def __len__(self):
+        return len(self.__store)
+
+    def has_key(self, key):
+        return key in self.__store
+
+    def get_deepest_match(self, name):
+        """Find the deepest match to *name* in the dictionary.
+
+        The deepest match is the longest name in the dictionary which is
+        a superdomain of *name*.  Note that *superdomain* includes matching
+        *name* itself.
+
+        *name*, a ``dns.name.Name``, the name to find.
+
+        Returns a ``(key, value)`` where *key* is the deepest
+        ``dns.name.Name``, and *value* is the value associated with *key*.
+        """
+
+        depth = len(name)
+        if depth > self.max_depth:
+            depth = self.max_depth
+        for i in range(-depth, 0):
+            n = dns.name.Name(name[i:])
+            if n in self:
+                return (n, self[n])
+        v = self[dns.name.empty]
+        return (dns.name.empty, v)

=== added file 'dns/node.py'
--- old/dns/node.py	1970-01-01 00:00:00 +0000
+++ new/dns/node.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,189 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS nodes.  A node is a set of rdatasets."""
+
+import io
+
+import dns.rdataset
+import dns.rdatatype
+import dns.renderer
+
+
+class Node:
+
+    """A Node is a set of rdatasets."""
+
+    __slots__ = ['rdatasets']
+
+    def __init__(self):
+        # the set of rdatasets, represented as a list.
+        self.rdatasets = []
+
+    def to_text(self, name, **kw):
+        """Convert a node to text format.
+
+        Each rdataset at the node is printed.  Any keyword arguments
+        to this method are passed on to the rdataset's to_text() method.
+
+        *name*, a ``dns.name.Name`` or ``str``, the owner name of the
+        rdatasets.
+
+        Returns a ``str``.
+
+        """
+
+        s = io.StringIO()
+        for rds in self.rdatasets:
+            if len(rds) > 0:
+                s.write(rds.to_text(name, **kw))
+                s.write('\n')
+        return s.getvalue()[:-1]
+
+    def __repr__(self):
+        return '<DNS node ' + str(id(self)) + '>'
+
+    def __eq__(self, other):
+        #
+        # This is inefficient.  Good thing we don't need to do it much.
+        #
+        for rd in self.rdatasets:
+            if rd not in other.rdatasets:
+                return False
+        for rd in other.rdatasets:
+            if rd not in self.rdatasets:
+                return False
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __len__(self):
+        return len(self.rdatasets)
+
+    def __iter__(self):
+        return iter(self.rdatasets)
+
+    def find_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
+                      create=False):
+        """Find an rdataset matching the specified properties in the
+        current node.
+
+        *rdclass*, an ``int``, the class of the rdataset.
+
+        *rdtype*, an ``int``, the type of the rdataset.
+
+        *covers*, an ``int`` or ``None``, the covered type.
+        Usually this value is ``dns.rdatatype.NONE``, but if the
+        rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``,
+        then the covers value will be the rdata type the SIG/RRSIG
+        covers.  The library treats the SIG and RRSIG types as if they
+        were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).
+        This makes RRSIGs much easier to work with than if RRSIGs
+        covering different rdata types were aggregated into a single
+        RRSIG rdataset.
+
+        *create*, a ``bool``.  If True, create the rdataset if it is not found.
+
+        Raises ``KeyError`` if an rdataset of the desired type and class does
+        not exist and *create* is not ``True``.
+
+        Returns a ``dns.rdataset.Rdataset``.
+        """
+
+        for rds in self.rdatasets:
+            if rds.match(rdclass, rdtype, covers):
+                return rds
+        if not create:
+            raise KeyError
+        rds = dns.rdataset.Rdataset(rdclass, rdtype)
+        self.rdatasets.append(rds)
+        return rds
+
+    def get_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE,
+                     create=False):
+        """Get an rdataset matching the specified properties in the
+        current node.
+
+        None is returned if an rdataset of the specified type and
+        class does not exist and *create* is not ``True``.
+
+        *rdclass*, an ``int``, the class of the rdataset.
+
+        *rdtype*, an ``int``, the type of the rdataset.
+
+        *covers*, an ``int``, the covered type.  Usually this value is
+        dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or
+        dns.rdatatype.RRSIG, then the covers value will be the rdata
+        type the SIG/RRSIG covers.  The library treats the SIG and RRSIG
+        types as if they were a family of
+        types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA).  This makes RRSIGs much
+        easier to work with than if RRSIGs covering different rdata
+        types were aggregated into a single RRSIG rdataset.
+
+        *create*, a ``bool``.  If True, create the rdataset if it is not found.
+
+        Returns a ``dns.rdataset.Rdataset`` or ``None``.
+        """
+
+        try:
+            rds = self.find_rdataset(rdclass, rdtype, covers, create)
+        except KeyError:
+            rds = None
+        return rds
+
+    def delete_rdataset(self, rdclass, rdtype, covers=dns.rdatatype.NONE):
+        """Delete the rdataset matching the specified properties in the
+        current node.
+
+        If a matching rdataset does not exist, it is not an error.
+
+        *rdclass*, an ``int``, the class of the rdataset.
+
+        *rdtype*, an ``int``, the type of the rdataset.
+
+        *covers*, an ``int``, the covered type.
+        """
+
+        rds = self.get_rdataset(rdclass, rdtype, covers)
+        if rds is not None:
+            self.rdatasets.remove(rds)
+
+    def replace_rdataset(self, replacement):
+        """Replace an rdataset.
+
+        It is not an error if there is no rdataset matching *replacement*.
+
+        Ownership of the *replacement* object is transferred to the node;
+        in other words, this method does not store a copy of *replacement*
+        at the node, it stores *replacement* itself.
+
+        *replacement*, a ``dns.rdataset.Rdataset``.
+
+        Raises ``ValueError`` if *replacement* is not a
+        ``dns.rdataset.Rdataset``.
+        """
+
+        if not isinstance(replacement, dns.rdataset.Rdataset):
+            raise ValueError('replacement is not an rdataset')
+        if isinstance(replacement, dns.rrset.RRset):
+            # RRsets are not good replacements as the match() method
+            # is not compatible.
+            replacement = replacement.to_rdataset()
+        self.delete_rdataset(replacement.rdclass, replacement.rdtype,
+                             replacement.covers)
+        self.rdatasets.append(replacement)

=== added file 'dns/node.pyi'
--- old/dns/node.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/node.pyi	2018-12-23 00:54:24 +0000
@@ -0,0 +1,17 @@
+from typing import List, Optional, Union
+from . import rdataset, rdatatype, name
+class Node:
+    def __init__(self):
+        self.rdatasets : List[rdataset.Rdataset]
+    def to_text(self, name : Union[str,name.Name], **kw) -> str:
+        ...
+    def find_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE,
+                      create=False) -> rdataset.Rdataset:
+        ...
+    def get_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE,
+                     create=False) -> Optional[rdataset.Rdataset]:
+        ...
+    def delete_rdataset(self, rdclass : int, rdtype : int, covers=rdatatype.NONE):
+        ...
+    def replace_rdataset(self, replacement : rdataset.Rdataset) -> None:
+        ...

=== added file 'dns/opcode.py'
--- old/dns/opcode.py	1970-01-01 00:00:00 +0000
+++ new/dns/opcode.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,115 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Opcodes."""
+
+import dns.enum
+import dns.exception
+
+class Opcode(dns.enum.IntEnum):
+    #: Query
+    QUERY = 0
+    #: Inverse Query (historical)
+    IQUERY = 1
+    #: Server Status (unspecified and unimplemented anywhere)
+    STATUS = 2
+    #: Notify
+    NOTIFY = 4
+    #: Dynamic Update
+    UPDATE = 5
+
+    @classmethod
+    def _maximum(cls):
+        return 15
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownOpcode
+
+
+class UnknownOpcode(dns.exception.DNSException):
+    """An DNS opcode is unknown."""
+
+
+def from_text(text):
+    """Convert text into an opcode.
+
+    *text*, a ``str``, the textual opcode
+
+    Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
+
+    Returns an ``int``.
+    """
+
+    return Opcode.from_text(text)
+
+
+def from_flags(flags):
+    """Extract an opcode from DNS message flags.
+
+    *flags*, an ``int``, the DNS flags.
+
+    Returns an ``int``.
+    """
+
+    return (flags & 0x7800) >> 11
+
+
+def to_flags(value):
+    """Convert an opcode to a value suitable for ORing into DNS message
+    flags.
+
+    *value*, an ``int``, the DNS opcode value.
+
+    Returns an ``int``.
+    """
+
+    return (value << 11) & 0x7800
+
+
+def to_text(value):
+    """Convert an opcode to text.
+
+    *value*, an ``int`` the opcode value,
+
+    Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown.
+
+    Returns a ``str``.
+    """
+
+    return Opcode.to_text(value)
+
+
+def is_update(flags):
+    """Is the opcode in flags UPDATE?
+
+    *flags*, an ``int``, the DNS message flags.
+
+    Returns a ``bool``.
+    """
+
+    return from_flags(flags) == Opcode.UPDATE
+
+### BEGIN generated Opcode constants
+
+QUERY = Opcode.QUERY
+IQUERY = Opcode.IQUERY
+STATUS = Opcode.STATUS
+NOTIFY = Opcode.NOTIFY
+UPDATE = Opcode.UPDATE
+
+### END generated Opcode constants

=== added file 'dns/py.typed'
=== added file 'dns/query.py'
--- old/dns/query.py	1970-01-01 00:00:00 +0000
+++ new/dns/query.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,1094 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Talk to a DNS server."""
+
+import contextlib
+import enum
+import errno
+import os
+import selectors
+import socket
+import struct
+import time
+import base64
+import urllib.parse
+
+import dns.exception
+import dns.inet
+import dns.name
+import dns.message
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.serial
+import dns.xfr
+
+try:
+    import requests
+    from requests_toolbelt.adapters.source import SourceAddressAdapter
+    from requests_toolbelt.adapters.host_header_ssl import HostHeaderSSLAdapter
+    have_doh = True
+except ImportError:  # pragma: no cover
+    have_doh = False
+
+try:
+    import ssl
+except ImportError:  # pragma: no cover
+    class ssl:    # type: ignore
+
+        class WantReadException(Exception):
+            pass
+
+        class WantWriteException(Exception):
+            pass
+
+        class SSLSocket:
+            pass
+
+        def create_default_context(self, *args, **kwargs):
+            raise Exception('no ssl support')
+
+# Function used to create a socket.  Can be overridden if needed in special
+# situations.
+socket_factory = socket.socket
+
+class UnexpectedSource(dns.exception.DNSException):
+    """A DNS query response came from an unexpected address or port."""
+
+
+class BadResponse(dns.exception.FormError):
+    """A DNS query response does not respond to the question asked."""
+
+
+class NoDOH(dns.exception.DNSException):
+    """DNS over HTTPS (DOH) was requested but the requests module is not
+    available."""
+
+
+# for backwards compatibility
+TransferError = dns.xfr.TransferError
+
+
+def _compute_times(timeout):
+    now = time.time()
+    if timeout is None:
+        return (now, None)
+    else:
+        return (now, now + timeout)
+
+
+def _wait_for(fd, readable, writable, _, expiration):
+    # Use the selected selector class to wait for any of the specified
+    # events.  An "expiration" absolute time is converted into a relative
+    # timeout.
+    #
+    # The unused parameter is 'error', which is always set when
+    # selecting for read or write, and we have no error-only selects.
+
+    if readable and isinstance(fd, ssl.SSLSocket) and fd.pending() > 0:
+        return True
+    sel = _selector_class()
+    events = 0
+    if readable:
+        events |= selectors.EVENT_READ
+    if writable:
+        events |= selectors.EVENT_WRITE
+    if events:
+        sel.register(fd, events)
+    if expiration is None:
+        timeout = None
+    else:
+        timeout = expiration - time.time()
+        if timeout <= 0.0:
+            raise dns.exception.Timeout
+    if not sel.select(timeout):
+        raise dns.exception.Timeout
+
+
+def _set_selector_class(selector_class):
+    # Internal API. Do not use.
+
+    global _selector_class
+
+    _selector_class = selector_class
+
+if hasattr(selectors, 'PollSelector'):
+    # Prefer poll() on platforms that support it because it has no
+    # limits on the maximum value of a file descriptor (plus it will
+    # be more efficient for high values).
+    _selector_class = selectors.PollSelector
+else:
+    _selector_class = selectors.SelectSelector  # pragma: no cover
+
+
+def _wait_for_readable(s, expiration):
+    _wait_for(s, True, False, True, expiration)
+
+
+def _wait_for_writable(s, expiration):
+    _wait_for(s, False, True, True, expiration)
+
+
+def _addresses_equal(af, a1, a2):
+    # Convert the first value of the tuple, which is a textual format
+    # address into binary form, so that we are not confused by different
+    # textual representations of the same address
+    try:
+        n1 = dns.inet.inet_pton(af, a1[0])
+        n2 = dns.inet.inet_pton(af, a2[0])
+    except dns.exception.SyntaxError:
+        return False
+    return n1 == n2 and a1[1:] == a2[1:]
+
+
+def _matches_destination(af, from_address, destination, ignore_unexpected):
+    # Check that from_address is appropriate for a response to a query
+    # sent to destination.
+    if not destination:
+        return True
+    if _addresses_equal(af, from_address, destination) or \
+       (dns.inet.is_multicast(destination[0]) and
+        from_address[1:] == destination[1:]):
+        return True
+    elif ignore_unexpected:
+        return False
+    raise UnexpectedSource(f'got a response from {from_address} instead of '
+                           f'{destination}')
+
+
+def _destination_and_source(where, port, source, source_port,
+                            where_must_be_address=True):
+    # Apply defaults and compute destination and source tuples
+    # suitable for use in connect(), sendto(), or bind().
+    af = None
+    destination = None
+    try:
+        af = dns.inet.af_for_address(where)
+        destination = where
+    except Exception:
+        if where_must_be_address:
+            raise
+        # URLs are ok so eat the exception
+    if source:
+        saf = dns.inet.af_for_address(source)
+        if af:
+            # We know the destination af, so source had better agree!
+            if saf != af:
+                raise ValueError('different address families for source ' +
+                                 'and destination')
+        else:
+            # We didn't know the destination af, but we know the source,
+            # so that's our af.
+            af = saf
+    if source_port and not source:
+        # Caller has specified a source_port but not an address, so we
+        # need to return a source, and we need to use the appropriate
+        # wildcard address as the address.
+        if af == socket.AF_INET:
+            source = '0.0.0.0'
+        elif af == socket.AF_INET6:
+            source = '::'
+        else:
+            raise ValueError('source_port specified but address family is '
+                             'unknown')
+    # Convert high-level (address, port) tuples into low-level address
+    # tuples.
+    if destination:
+        destination = dns.inet.low_level_address_tuple((destination, port), af)
+    if source:
+        source = dns.inet.low_level_address_tuple((source, source_port), af)
+    return (af, destination, source)
+
+def _make_socket(af, type, source, ssl_context=None, server_hostname=None):
+    s = socket_factory(af, type)
+    try:
+        s.setblocking(False)
+        if source is not None:
+            s.bind(source)
+        if ssl_context:
+            return ssl_context.wrap_socket(s, do_handshake_on_connect=False,
+                                           server_hostname=server_hostname)
+        else:
+            return s
+    except Exception:
+        s.close()
+        raise
+
+def https(q, where, timeout=None, port=443, source=None, source_port=0,
+          one_rr_per_rrset=False, ignore_trailing=False,
+          session=None, path='/dns-query', post=True,
+          bootstrap_address=None, verify=True):
+    """Return the response obtained after sending a query via DNS-over-HTTPS.
+
+    *q*, a ``dns.message.Message``, the query to send.
+
+    *where*, a ``str``, the nameserver IP address or the full URL. If an IP
+    address is given, the URL will be constructed using the following schema:
+    https://<IP-address>:<port>/<path>.
+
+    *timeout*, a ``float`` or ``None``, the number of seconds to
+    wait before the query times out. If ``None``, the default, wait forever.
+
+    *port*, a ``int``, the port to send the query to. The default is 443.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own
+    RRset.
+
+    *ignore_trailing*, a ``bool``. If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *session*, a ``requests.session.Session``.  If provided, the session to use
+    to send the queries.
+
+    *path*, a ``str``. If *where* is an IP address, then *path* will be used to
+    construct the URL to send the DNS query to.
+
+    *post*, a ``bool``. If ``True``, the default, POST method will be used.
+
+    *bootstrap_address*, a ``str``, the IP address to use to bypass the
+    system's DNS resolver.
+
+    *verify*, a ``str``, containing a path to a certificate file or directory.
+
+    Returns a ``dns.message.Message``.
+    """
+
+    if not have_doh:
+        raise NoDOH  # pragma: no cover
+
+    wire = q.to_wire()
+    (af, _, source) = _destination_and_source(where, port, source, source_port,
+                                              False)
+    transport_adapter = None
+    headers = {
+        "accept": "application/dns-message"
+    }
+    if af is not None:
+        if af == socket.AF_INET:
+            url = 'https://{}:{}{}'.format(where, port, path)
+        elif af == socket.AF_INET6:
+            url = 'https://[{}]:{}{}'.format(where, port, path)
+    elif bootstrap_address is not None:
+        split_url = urllib.parse.urlsplit(where)
+        headers['Host'] = split_url.hostname
+        url = where.replace(split_url.hostname, bootstrap_address)
+        transport_adapter = HostHeaderSSLAdapter()
+    else:
+        url = where
+    if source is not None:
+        # set source port and source address
+        transport_adapter = SourceAddressAdapter(source)
+
+    with contextlib.ExitStack() as stack:
+        if not session:
+            session = stack.enter_context(requests.sessions.Session())
+
+        if transport_adapter:
+            session.mount(url, transport_adapter)
+
+        # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH
+        # GET and POST examples
+        if post:
+            headers.update({
+                "content-type": "application/dns-message",
+                "content-length": str(len(wire))
+            })
+            response = session.post(url, headers=headers, data=wire,
+                                    timeout=timeout, verify=verify)
+        else:
+            wire = base64.urlsafe_b64encode(wire).rstrip(b"=")
+            response = session.get(url, headers=headers,
+                                   timeout=timeout, verify=verify,
+                                   params={"dns": wire})
+
+    # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH
+    # status codes
+    if response.status_code < 200 or response.status_code > 299:
+        raise ValueError('{} responded with status code {}'
+                         '\nResponse body: {}'.format(where,
+                                                      response.status_code,
+                                                      response.content))
+    r = dns.message.from_wire(response.content,
+                              keyring=q.keyring,
+                              request_mac=q.request_mac,
+                              one_rr_per_rrset=one_rr_per_rrset,
+                              ignore_trailing=ignore_trailing)
+    r.time = response.elapsed
+    if not q.is_response(r):
+        raise BadResponse
+    return r
+
+def _udp_recv(sock, max_size, expiration):
+    """Reads a datagram from the socket.
+    A Timeout exception will be raised if the operation is not completed
+    by the expiration time.
+    """
+    while True:
+        try:
+            return sock.recvfrom(max_size)
+        except BlockingIOError:
+            _wait_for_readable(sock, expiration)
+
+
+def _udp_send(sock, data, destination, expiration):
+    """Sends the specified datagram to destination over the socket.
+    A Timeout exception will be raised if the operation is not completed
+    by the expiration time.
+    """
+    while True:
+        try:
+            if destination:
+                return sock.sendto(data, destination)
+            else:
+                return sock.send(data)
+        except BlockingIOError:  # pragma: no cover
+            _wait_for_writable(sock, expiration)
+
+
+def send_udp(sock, what, destination, expiration=None):
+    """Send a DNS message to the specified UDP socket.
+
+    *sock*, a ``socket``.
+
+    *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+    *destination*, a destination tuple appropriate for the address family
+    of the socket, specifying where to send the query.
+
+    *expiration*, a ``float`` or ``None``, the absolute time at which
+    a timeout exception should be raised.  If ``None``, no timeout will
+    occur.
+
+    Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+    """
+
+    if isinstance(what, dns.message.Message):
+        what = what.to_wire()
+    sent_time = time.time()
+    n = _udp_send(sock, what, destination, expiration)
+    return (n, sent_time)
+
+
+def receive_udp(sock, destination=None, expiration=None,
+                ignore_unexpected=False, one_rr_per_rrset=False,
+                keyring=None, request_mac=b'', ignore_trailing=False,
+                raise_on_truncation=False):
+    """Read a DNS message from a UDP socket.
+
+    *sock*, a ``socket``.
+
+    *destination*, a destination tuple appropriate for the address family
+    of the socket, specifying where the message is expected to arrive from.
+    When receiving a response, this would be where the associated query was
+    sent.
+
+    *expiration*, a ``float`` or ``None``, the absolute time at which
+    a timeout exception should be raised.  If ``None``, no timeout will
+    occur.
+
+    *ignore_unexpected*, a ``bool``.  If ``True``, ignore responses from
+    unexpected sources.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *keyring*, a ``dict``, the keyring to use for TSIG.
+
+    *request_mac*, a ``bytes``, the MAC of the request (for TSIG).
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *raise_on_truncation*, a ``bool``.  If ``True``, raise an exception if
+    the TC bit is set.
+
+    Raises if the message is malformed, if network errors occur, of if
+    there is a timeout.
+
+    If *destination* is not ``None``, returns a ``(dns.message.Message, float)``
+    tuple of the received message and the received time.
+
+    If *destination* is ``None``, returns a
+    ``(dns.message.Message, float, tuple)``
+    tuple of the received message, the received time, and the address where
+    the message arrived from.
+    """
+
+    wire = b''
+    while True:
+        (wire, from_address) = _udp_recv(sock, 65535, expiration)
+        if _matches_destination(sock.family, from_address, destination,
+                                ignore_unexpected):
+            break
+    received_time = time.time()
+    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
+                              one_rr_per_rrset=one_rr_per_rrset,
+                              ignore_trailing=ignore_trailing,
+                              raise_on_truncation=raise_on_truncation)
+    if destination:
+        return (r, received_time)
+    else:
+        return (r, received_time, from_address)
+
+def udp(q, where, timeout=None, port=53, source=None, source_port=0,
+        ignore_unexpected=False, one_rr_per_rrset=False, ignore_trailing=False,
+        raise_on_truncation=False, sock=None):
+    """Return the response obtained after sending a query via UDP.
+
+    *q*, a ``dns.message.Message``, the query to send
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address,  where
+    to send the message.
+
+    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+    query times out.  If ``None``, the default, wait forever.
+
+    *port*, an ``int``, the port send the message to.  The default is 53.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *ignore_unexpected*, a ``bool``.  If ``True``, ignore responses from
+    unexpected sources.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *raise_on_truncation*, a ``bool``.  If ``True``, raise an exception if
+    the TC bit is set.
+
+    *sock*, a ``socket.socket``, or ``None``, the socket to use for the
+    query.  If ``None``, the default, a socket is created.  Note that
+    if a socket is provided, it must be a nonblocking datagram socket,
+    and the *source* and *source_port* are ignored.
+
+    Returns a ``dns.message.Message``.
+    """
+
+    wire = q.to_wire()
+    (af, destination, source) = _destination_and_source(where, port,
+                                                        source, source_port)
+    (begin_time, expiration) = _compute_times(timeout)
+    with contextlib.ExitStack() as stack:
+        if sock:
+            s = sock
+        else:
+            s = stack.enter_context(_make_socket(af, socket.SOCK_DGRAM, source))
+        send_udp(s, wire, destination, expiration)
+        (r, received_time) = receive_udp(s, destination, expiration,
+                                         ignore_unexpected, one_rr_per_rrset,
+                                         q.keyring, q.mac, ignore_trailing,
+                                         raise_on_truncation)
+        r.time = received_time - begin_time
+        if not q.is_response(r):
+            raise BadResponse
+        return r
+
+def udp_with_fallback(q, where, timeout=None, port=53, source=None,
+                      source_port=0, ignore_unexpected=False,
+                      one_rr_per_rrset=False, ignore_trailing=False,
+                      udp_sock=None, tcp_sock=None):
+    """Return the response to the query, trying UDP first and falling back
+    to TCP if UDP results in a truncated response.
+
+    *q*, a ``dns.message.Message``, the query to send
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address,  where
+    to send the message.
+
+    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+    query times out.  If ``None``, the default, wait forever.
+
+    *port*, an ``int``, the port send the message to.  The default is 53.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *ignore_unexpected*, a ``bool``.  If ``True``, ignore responses from
+    unexpected sources.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the
+    UDP query.  If ``None``, the default, a socket is created.  Note that
+    if a socket is provided, it must be a nonblocking datagram socket,
+    and the *source* and *source_port* are ignored for the UDP query.
+
+    *tcp_sock*, a ``socket.socket``, or ``None``, the socket to use for the
+    TCP query.  If ``None``, the default, a socket is created.  Note that
+    if a socket is provided, it must be a nonblocking connected stream
+    socket, and *where*, *source* and *source_port* are ignored for the TCP
+    query.
+
+    Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True``
+    if and only if TCP was used.
+    """
+    try:
+        response = udp(q, where, timeout, port, source, source_port,
+                       ignore_unexpected, one_rr_per_rrset,
+                       ignore_trailing, True, udp_sock)
+        return (response, False)
+    except dns.message.Truncated:
+        response = tcp(q, where, timeout, port, source, source_port,
+                       one_rr_per_rrset, ignore_trailing, tcp_sock)
+        return (response, True)
+
+def _net_read(sock, count, expiration):
+    """Read the specified number of bytes from sock.  Keep trying until we
+    either get the desired amount, or we hit EOF.
+    A Timeout exception will be raised if the operation is not completed
+    by the expiration time.
+    """
+    s = b''
+    while count > 0:
+        try:
+            n = sock.recv(count)
+            if n == b'':
+                raise EOFError
+            count -= len(n)
+            s += n
+        except (BlockingIOError, ssl.SSLWantReadError):
+            _wait_for_readable(sock, expiration)
+        except ssl.SSLWantWriteError:  # pragma: no cover
+            _wait_for_writable(sock, expiration)
+    return s
+
+
+def _net_write(sock, data, expiration):
+    """Write the specified data to the socket.
+    A Timeout exception will be raised if the operation is not completed
+    by the expiration time.
+    """
+    current = 0
+    l = len(data)
+    while current < l:
+        try:
+            current += sock.send(data[current:])
+        except (BlockingIOError, ssl.SSLWantWriteError):
+            _wait_for_writable(sock, expiration)
+        except ssl.SSLWantReadError:  # pragma: no cover
+            _wait_for_readable(sock, expiration)
+
+
+def send_tcp(sock, what, expiration=None):
+    """Send a DNS message to the specified TCP socket.
+
+    *sock*, a ``socket``.
+
+    *what*, a ``bytes`` or ``dns.message.Message``, the message to send.
+
+    *expiration*, a ``float`` or ``None``, the absolute time at which
+    a timeout exception should be raised.  If ``None``, no timeout will
+    occur.
+
+    Returns an ``(int, float)`` tuple of bytes sent and the sent time.
+    """
+
+    if isinstance(what, dns.message.Message):
+        what = what.to_wire()
+    l = len(what)
+    # copying the wire into tcpmsg is inefficient, but lets us
+    # avoid writev() or doing a short write that would get pushed
+    # onto the net
+    tcpmsg = struct.pack("!H", l) + what
+    sent_time = time.time()
+    _net_write(sock, tcpmsg, expiration)
+    return (len(tcpmsg), sent_time)
+
+def receive_tcp(sock, expiration=None, one_rr_per_rrset=False,
+                keyring=None, request_mac=b'', ignore_trailing=False):
+    """Read a DNS message from a TCP socket.
+
+    *sock*, a ``socket``.
+
+    *expiration*, a ``float`` or ``None``, the absolute time at which
+    a timeout exception should be raised.  If ``None``, no timeout will
+    occur.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *keyring*, a ``dict``, the keyring to use for TSIG.
+
+    *request_mac*, a ``bytes``, the MAC of the request (for TSIG).
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    Raises if the message is malformed, if network errors occur, of if
+    there is a timeout.
+
+    Returns a ``(dns.message.Message, float)`` tuple of the received message
+    and the received time.
+    """
+
+    ldata = _net_read(sock, 2, expiration)
+    (l,) = struct.unpack("!H", ldata)
+    wire = _net_read(sock, l, expiration)
+    received_time = time.time()
+    r = dns.message.from_wire(wire, keyring=keyring, request_mac=request_mac,
+                              one_rr_per_rrset=one_rr_per_rrset,
+                              ignore_trailing=ignore_trailing)
+    return (r, received_time)
+
+def _connect(s, address, expiration):
+    err = s.connect_ex(address)
+    if err == 0:
+        return
+    if err in (errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY):
+        _wait_for_writable(s, expiration)
+        err = s.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+    if err != 0:
+        raise OSError(err, os.strerror(err))
+
+
+def tcp(q, where, timeout=None, port=53, source=None, source_port=0,
+        one_rr_per_rrset=False, ignore_trailing=False, sock=None):
+    """Return the response obtained after sending a query via TCP.
+
+    *q*, a ``dns.message.Message``, the query to send
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address, where
+    to send the message.
+
+    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+    query times out.  If ``None``, the default, wait forever.
+
+    *port*, an ``int``, the port send the message to.  The default is 53.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *sock*, a ``socket.socket``, or ``None``, the socket to use for the
+    query.  If ``None``, the default, a socket is created.  Note that
+    if a socket is provided, it must be a nonblocking connected stream
+    socket, and *where*, *port*, *source* and *source_port* are ignored.
+
+    Returns a ``dns.message.Message``.
+    """
+
+    wire = q.to_wire()
+    (begin_time, expiration) = _compute_times(timeout)
+    with contextlib.ExitStack() as stack:
+        if sock:
+            s = sock
+        else:
+            (af, destination, source) = _destination_and_source(where, port,
+                                                                source,
+                                                                source_port)
+            s = stack.enter_context(_make_socket(af, socket.SOCK_STREAM,
+                                                 source))
+            _connect(s, destination, expiration)
+        send_tcp(s, wire, expiration)
+        (r, received_time) = receive_tcp(s, expiration, one_rr_per_rrset,
+                                         q.keyring, q.mac, ignore_trailing)
+        r.time = received_time - begin_time
+        if not q.is_response(r):
+            raise BadResponse
+        return r
+
+
+def _tls_handshake(s, expiration):
+    while True:
+        try:
+            s.do_handshake()
+            return
+        except ssl.SSLWantReadError:
+            _wait_for_readable(s, expiration)
+        except ssl.SSLWantWriteError:  # pragma: no cover
+            _wait_for_writable(s, expiration)
+
+
+def tls(q, where, timeout=None, port=853, source=None, source_port=0,
+        one_rr_per_rrset=False, ignore_trailing=False, sock=None,
+        ssl_context=None, server_hostname=None):
+    """Return the response obtained after sending a query via TLS.
+
+    *q*, a ``dns.message.Message``, the query to send
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address,  where
+    to send the message.
+
+    *timeout*, a ``float`` or ``None``, the number of seconds to wait before the
+    query times out.  If ``None``, the default, wait forever.
+
+    *port*, an ``int``, the port send the message to.  The default is 853.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *one_rr_per_rrset*, a ``bool``.  If ``True``, put each RR into its own
+    RRset.
+
+    *ignore_trailing*, a ``bool``.  If ``True``, ignore trailing
+    junk at end of the received message.
+
+    *sock*, an ``ssl.SSLSocket``, or ``None``, the socket to use for
+    the query.  If ``None``, the default, a socket is created.  Note
+    that if a socket is provided, it must be a nonblocking connected
+    SSL stream socket, and *where*, *port*, *source*, *source_port*,
+    and *ssl_context* are ignored.
+
+    *ssl_context*, an ``ssl.SSLContext``, the context to use when establishing
+    a TLS connection. If ``None``, the default, creates one with the default
+    configuration.
+
+    *server_hostname*, a ``str`` containing the server's hostname.  The
+    default is ``None``, which means that no hostname is known, and if an
+    SSL context is created, hostname checking will be disabled.
+
+    Returns a ``dns.message.Message``.
+
+    """
+
+    if sock:
+        #
+        # If a socket was provided, there's no special TLS handling needed.
+        #
+        return tcp(q, where, timeout, port, source, source_port,
+                   one_rr_per_rrset, ignore_trailing, sock)
+
+    wire = q.to_wire()
+    (begin_time, expiration) = _compute_times(timeout)
+    (af, destination, source) = _destination_and_source(where, port,
+                                                        source, source_port)
+    if ssl_context is None and not sock:
+        ssl_context = ssl.create_default_context()
+        if server_hostname is None:
+            ssl_context.check_hostname = False
+
+    with _make_socket(af, socket.SOCK_STREAM, source, ssl_context=ssl_context,
+                      server_hostname=server_hostname) as s:
+        _connect(s, destination, expiration)
+        _tls_handshake(s, expiration)
+        send_tcp(s, wire, expiration)
+        (r, received_time) = receive_tcp(s, expiration, one_rr_per_rrset,
+                                         q.keyring, q.mac, ignore_trailing)
+        r.time = received_time - begin_time
+        if not q.is_response(r):
+            raise BadResponse
+        return r
+
+
+def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN,
+        timeout=None, port=53, keyring=None, keyname=None, relativize=True,
+        lifetime=None, source=None, source_port=0, serial=0,
+        use_udp=False, keyalgorithm=dns.tsig.default_algorithm):
+    """Return a generator for the responses to a zone transfer.
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address,  where
+    to send the message.
+
+    *zone*, a ``dns.name.Name`` or ``str``, the name of the zone to transfer.
+
+    *rdtype*, an ``int`` or ``str``, the type of zone transfer.  The
+    default is ``dns.rdatatype.AXFR``.  ``dns.rdatatype.IXFR`` can be
+    used to do an incremental transfer instead.
+
+    *rdclass*, an ``int`` or ``str``, the class of the zone transfer.
+    The default is ``dns.rdataclass.IN``.
+
+    *timeout*, a ``float``, the number of seconds to wait for each
+    response message.  If None, the default, wait forever.
+
+    *port*, an ``int``, the port send the message to.  The default is 53.
+
+    *keyring*, a ``dict``, the keyring to use for TSIG.
+
+    *keyname*, a ``dns.name.Name`` or ``str``, the name of the TSIG
+    key to use.
+
+    *relativize*, a ``bool``.  If ``True``, all names in the zone will be
+    relativized to the zone origin.  It is essential that the
+    relativize setting matches the one specified to
+    ``dns.zone.from_xfr()`` if using this generator to make a zone.
+
+    *lifetime*, a ``float``, the total number of seconds to spend
+    doing the transfer.  If ``None``, the default, then there is no
+    limit on the time the transfer may take.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *serial*, an ``int``, the SOA serial number to use as the base for
+    an IXFR diff sequence (only meaningful if *rdtype* is
+    ``dns.rdatatype.IXFR``).
+
+    *use_udp*, a ``bool``.  If ``True``, use UDP (only meaningful for IXFR).
+
+    *keyalgorithm*, a ``dns.name.Name`` or ``str``, the TSIG algorithm to use.
+
+    Raises on errors, and so does the generator.
+
+    Returns a generator of ``dns.message.Message`` objects.
+    """
+
+    if isinstance(zone, str):
+        zone = dns.name.from_text(zone)
+    rdtype = dns.rdatatype.RdataType.make(rdtype)
+    q = dns.message.make_query(zone, rdtype, rdclass)
+    if rdtype == dns.rdatatype.IXFR:
+        rrset = dns.rrset.from_text(zone, 0, 'IN', 'SOA',
+                                    '. . %u 0 0 0 0' % serial)
+        q.authority.append(rrset)
+    if keyring is not None:
+        q.use_tsig(keyring, keyname, algorithm=keyalgorithm)
+    wire = q.to_wire()
+    (af, destination, source) = _destination_and_source(where, port,
+                                                        source, source_port)
+    if use_udp and rdtype != dns.rdatatype.IXFR:
+        raise ValueError('cannot do a UDP AXFR')
+    sock_type = socket.SOCK_DGRAM if use_udp else socket.SOCK_STREAM
+    with _make_socket(af, sock_type, source) as s:
+        (_, expiration) = _compute_times(lifetime)
+        _connect(s, destination, expiration)
+        l = len(wire)
+        if use_udp:
+            _udp_send(s, wire, None, expiration)
+        else:
+            tcpmsg = struct.pack("!H", l) + wire
+            _net_write(s, tcpmsg, expiration)
+        done = False
+        delete_mode = True
+        expecting_SOA = False
+        soa_rrset = None
+        if relativize:
+            origin = zone
+            oname = dns.name.empty
+        else:
+            origin = None
+            oname = zone
+        tsig_ctx = None
+        while not done:
+            (_, mexpiration) = _compute_times(timeout)
+            if mexpiration is None or \
+               (expiration is not None and mexpiration > expiration):
+                mexpiration = expiration
+            if use_udp:
+                (wire, _) = _udp_recv(s, 65535, mexpiration)
+            else:
+                ldata = _net_read(s, 2, mexpiration)
+                (l,) = struct.unpack("!H", ldata)
+                wire = _net_read(s, l, mexpiration)
+            is_ixfr = (rdtype == dns.rdatatype.IXFR)
+            r = dns.message.from_wire(wire, keyring=q.keyring,
+                                      request_mac=q.mac, xfr=True,
+                                      origin=origin, tsig_ctx=tsig_ctx,
+                                      multi=True, one_rr_per_rrset=is_ixfr)
+            rcode = r.rcode()
+            if rcode != dns.rcode.NOERROR:
+                raise TransferError(rcode)
+            tsig_ctx = r.tsig_ctx
+            answer_index = 0
+            if soa_rrset is None:
+                if not r.answer or r.answer[0].name != oname:
+                    raise dns.exception.FormError(
+                        "No answer or RRset not for qname")
+                rrset = r.answer[0]
+                if rrset.rdtype != dns.rdatatype.SOA:
+                    raise dns.exception.FormError("first RRset is not an SOA")
+                answer_index = 1
+                soa_rrset = rrset.copy()
+                if rdtype == dns.rdatatype.IXFR:
+                    if dns.serial.Serial(soa_rrset[0].serial) <= serial:
+                        #
+                        # We're already up-to-date.
+                        #
+                        done = True
+                    else:
+                        expecting_SOA = True
+            #
+            # Process SOAs in the answer section (other than the initial
+            # SOA in the first message).
+            #
+            for rrset in r.answer[answer_index:]:
+                if done:
+                    raise dns.exception.FormError("answers after final SOA")
+                if rrset.rdtype == dns.rdatatype.SOA and rrset.name == oname:
+                    if expecting_SOA:
+                        if rrset[0].serial != serial:
+                            raise dns.exception.FormError(
+                                "IXFR base serial mismatch")
+                        expecting_SOA = False
+                    elif rdtype == dns.rdatatype.IXFR:
+                        delete_mode = not delete_mode
+                    #
+                    # If this SOA RRset is equal to the first we saw then we're
+                    # finished. If this is an IXFR we also check that we're
+                    # seeing the record in the expected part of the response.
+                    #
+                    if rrset == soa_rrset and \
+                            (rdtype == dns.rdatatype.AXFR or
+                             (rdtype == dns.rdatatype.IXFR and delete_mode)):
+                        done = True
+                elif expecting_SOA:
+                    #
+                    # We made an IXFR request and are expecting another
+                    # SOA RR, but saw something else, so this must be an
+                    # AXFR response.
+                    #
+                    rdtype = dns.rdatatype.AXFR
+                    expecting_SOA = False
+            if done and q.keyring and not r.had_tsig:
+                raise dns.exception.FormError("missing TSIG")
+            yield r
+
+
+class UDPMode(enum.IntEnum):
+    """How should UDP be used in an IXFR from :py:func:`inbound_xfr()`?
+
+    NEVER means "never use UDP; always use TCP"
+    TRY_FIRST means "try to use UDP but fall back to TCP if needed"
+    ONLY means "raise ``dns.xfr.UseTCP`` if trying UDP does not succeed"
+    """
+    NEVER = 0
+    TRY_FIRST = 1
+    ONLY = 2
+
+
+def inbound_xfr(where, txn_manager, query=None,
+                port=53, timeout=None, lifetime=None, source=None,
+                source_port=0, udp_mode=UDPMode.NEVER):
+    """Conduct an inbound transfer and apply it via a transaction from the
+    txn_manager.
+
+    *where*, a ``str`` containing an IPv4 or IPv6 address,  where
+    to send the message.
+
+    *txn_manager*, a ``dns.transaction.TransactionManager``, the txn_manager
+    for this transfer (typically a ``dns.zone.Zone``).
+
+    *query*, the query to send.  If not supplied, a default query is
+    constructed using information from the *txn_manager*.
+
+    *port*, an ``int``, the port send the message to.  The default is 53.
+
+    *timeout*, a ``float``, the number of seconds to wait for each
+    response message.  If None, the default, wait forever.
+
+    *lifetime*, a ``float``, the total number of seconds to spend
+    doing the transfer.  If ``None``, the default, then there is no
+    limit on the time the transfer may take.
+
+    *source*, a ``str`` containing an IPv4 or IPv6 address, specifying
+    the source address.  The default is the wildcard address.
+
+    *source_port*, an ``int``, the port from which to send the message.
+    The default is 0.
+
+    *udp_mode*, a ``dns.query.UDPMode``, determines how UDP is used
+    for IXFRs.  The default is ``dns.UDPMode.NEVER``, i.e. only use
+    TCP.  Other possibilites are ``dns.UDPMode.TRY_FIRST``, which
+    means "try UDP but fallback to TCP if needed", and
+    ``dns.UDPMode.ONLY``, which means "try UDP and raise
+    ``dns.xfr.UseTCP`` if it does not succeeed.
+
+    Raises on errors.
+    """
+    if query is None:
+        (query, serial) = dns.xfr.make_query(txn_manager)
+    rdtype = query.question[0].rdtype
+    is_ixfr = rdtype == dns.rdatatype.IXFR
+    origin = txn_manager.from_wire_origin()
+    wire = query.to_wire()
+    (af, destination, source) = _destination_and_source(where, port,
+                                                        source, source_port)
+    (_, expiration) = _compute_times(lifetime)
+    retry = True
+    while retry:
+        retry = False
+        if is_ixfr and udp_mode != UDPMode.NEVER:
+            sock_type = socket.SOCK_DGRAM
+            is_udp = True
+        else:
+            sock_type = socket.SOCK_STREAM
+            is_udp = False
+        with _make_socket(af, sock_type, source) as s:
+            _connect(s, destination, expiration)
+            if is_udp:
+                _udp_send(s, wire, None, expiration)
+            else:
+                tcpmsg = struct.pack("!H", len(wire)) + wire
+                _net_write(s, tcpmsg, expiration)
+            with dns.xfr.Inbound(txn_manager, rdtype, serial,
+                                 is_udp) as inbound:
+                done = False
+                tsig_ctx = None
+                while not done:
+                    (_, mexpiration) = _compute_times(timeout)
+                    if mexpiration is None or \
+                       (expiration is not None and mexpiration > expiration):
+                        mexpiration = expiration
+                    if is_udp:
+                        (rwire, _) = _udp_recv(s, 65535, mexpiration)
+                    else:
+                        ldata = _net_read(s, 2, mexpiration)
+                        (l,) = struct.unpack("!H", ldata)
+                        rwire = _net_read(s, l, mexpiration)
+                    r = dns.message.from_wire(rwire, keyring=query.keyring,
+                                              request_mac=query.mac, xfr=True,
+                                              origin=origin, tsig_ctx=tsig_ctx,
+                                              multi=(not is_udp),
+                                              one_rr_per_rrset=is_ixfr)
+                    try:
+                        done = inbound.process_message(r)
+                    except dns.xfr.UseTCP:
+                        assert is_udp  # should not happen if we used TCP!
+                        if udp_mode == UDPMode.ONLY:
+                            raise
+                        done = True
+                        retry = True
+                        udp_mode = UDPMode.NEVER
+                        continue
+                    tsig_ctx = r.tsig_ctx
+                if not retry and query.keyring and not r.had_tsig:
+                    raise dns.exception.FormError("missing TSIG")

=== added file 'dns/query.pyi'
--- old/dns/query.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/query.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,64 @@
+from typing import Optional, Union, Dict, Generator, Any
+from . import tsig, rdatatype, rdataclass, name, message
+from requests.sessions import Session
+
+import socket
+
+# If the ssl import works, then
+#
+#    error: Name 'ssl' already defined (by an import)
+#
+# is expected and can be ignored.
+try:
+    import ssl
+except ImportError:
+    class ssl:    # type: ignore
+        SSLContext : Dict = {}
+
+have_doh: bool
+
+def https(q : message.Message, where: str, timeout : Optional[float] = None,
+          port : Optional[int] = 443, source : Optional[str] = None,
+          source_port : Optional[int] = 0,
+          session: Optional[Session] = None,
+          path : Optional[str] = '/dns-query', post : Optional[bool] = True,
+          bootstrap_address : Optional[str] = None,
+          verify : Optional[bool] = True) -> message.Message:
+    pass
+
+def tcp(q : message.Message, where : str, timeout : float = None, port=53,
+        af : Optional[int] = None, source : Optional[str] = None,
+        source_port : Optional[int] = 0,
+        one_rr_per_rrset : Optional[bool] = False,
+        ignore_trailing : Optional[bool] = False,
+        sock : Optional[socket.socket] = None) -> message.Message:
+    pass
+
+def xfr(where : None, zone : Union[name.Name,str], rdtype=rdatatype.AXFR,
+        rdclass=rdataclass.IN,
+        timeout : Optional[float] = None, port=53,
+        keyring : Optional[Dict[name.Name, bytes]] = None,
+        keyname : Union[str,name.Name]= None, relativize=True,
+        lifetime : Optional[float] = None,
+        source : Optional[str] = None, source_port=0, serial=0,
+        use_udp : Optional[bool] = False,
+        keyalgorithm=tsig.default_algorithm) \
+        -> Generator[Any,Any,message.Message]:
+    pass
+
+def udp(q : message.Message, where : str, timeout : Optional[float] = None,
+        port=53, source : Optional[str] = None, source_port : Optional[int] = 0,
+        ignore_unexpected : Optional[bool] = False,
+        one_rr_per_rrset : Optional[bool] = False,
+        ignore_trailing : Optional[bool] = False,
+        sock : Optional[socket.socket] = None) -> message.Message:
+    pass
+
+def tls(q : message.Message, where : str, timeout : Optional[float] = None,
+        port=53, source : Optional[str] = None, source_port : Optional[int] = 0,
+        one_rr_per_rrset : Optional[bool] = False,
+        ignore_trailing : Optional[bool] = False,
+        sock : Optional[socket.socket] = None,
+        ssl_context: Optional[ssl.SSLContext] = None,
+        server_hostname: Optional[str] = None) -> message.Message:
+    pass

=== added file 'dns/rcode.py'
--- old/dns/rcode.py	1970-01-01 00:00:00 +0000
+++ new/dns/rcode.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,164 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Result Codes."""
+
+import dns.enum
+import dns.exception
+
+class Rcode(dns.enum.IntEnum):
+    #: No error
+    NOERROR = 0
+    #: Format error
+    FORMERR = 1
+    #: Server failure
+    SERVFAIL = 2
+    #: Name does not exist ("Name Error" in RFC 1025 terminology).
+    NXDOMAIN = 3
+    #: Not implemented
+    NOTIMP = 4
+    #: Refused
+    REFUSED = 5
+    #: Name exists.
+    YXDOMAIN = 6
+    #: RRset exists.
+    YXRRSET = 7
+    #: RRset does not exist.
+    NXRRSET = 8
+    #: Not authoritative.
+    NOTAUTH = 9
+    #: Name not in zone.
+    NOTZONE = 10
+    #: DSO-TYPE Not Implemented
+    DSOTYPENI = 11
+    #: Bad EDNS version.
+    BADVERS = 16
+    #: TSIG Signature Failure
+    BADSIG = 16
+    #: Key not recognized.
+    BADKEY = 17
+    #: Signature out of time window.
+    BADTIME = 18
+    #: Bad TKEY Mode.
+    BADMODE = 19
+    #: Duplicate key name.
+    BADNAME = 20
+    #: Algorithm not supported.
+    BADALG = 21
+    #: Bad Truncation
+    BADTRUNC = 22
+    #: Bad/missing Server Cookie
+    BADCOOKIE = 23
+
+    @classmethod
+    def _maximum(cls):
+        return 4095
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownRcode
+
+
+class UnknownRcode(dns.exception.DNSException):
+    """A DNS rcode is unknown."""
+
+
+def from_text(text):
+    """Convert text into an rcode.
+
+    *text*, a ``str``, the textual rcode or an integer in textual form.
+
+    Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown.
+
+    Returns an ``int``.
+    """
+
+    return Rcode.from_text(text)
+
+
+def from_flags(flags, ednsflags):
+    """Return the rcode value encoded by flags and ednsflags.
+
+    *flags*, an ``int``, the DNS flags field.
+
+    *ednsflags*, an ``int``, the EDNS flags field.
+
+    Raises ``ValueError`` if rcode is < 0 or > 4095
+
+    Returns an ``int``.
+    """
+
+    value = (flags & 0x000f) | ((ednsflags >> 20) & 0xff0)
+    return value
+
+
+def to_flags(value):
+    """Return a (flags, ednsflags) tuple which encodes the rcode.
+
+    *value*, an ``int``, the rcode.
+
+    Raises ``ValueError`` if rcode is < 0 or > 4095.
+
+    Returns an ``(int, int)`` tuple.
+    """
+
+    if value < 0 or value > 4095:
+        raise ValueError('rcode must be >= 0 and <= 4095')
+    v = value & 0xf
+    ev = (value & 0xff0) << 20
+    return (v, ev)
+
+
+def to_text(value, tsig=False):
+    """Convert rcode into text.
+
+    *value*, an ``int``, the rcode.
+
+    Raises ``ValueError`` if rcode is < 0 or > 4095.
+
+    Returns a ``str``.
+    """
+
+    if tsig and value == Rcode.BADVERS:
+        return 'BADSIG'
+    return Rcode.to_text(value)
+
+### BEGIN generated Rcode constants
+
+NOERROR = Rcode.NOERROR
+FORMERR = Rcode.FORMERR
+SERVFAIL = Rcode.SERVFAIL
+NXDOMAIN = Rcode.NXDOMAIN
+NOTIMP = Rcode.NOTIMP
+REFUSED = Rcode.REFUSED
+YXDOMAIN = Rcode.YXDOMAIN
+YXRRSET = Rcode.YXRRSET
+NXRRSET = Rcode.NXRRSET
+NOTAUTH = Rcode.NOTAUTH
+NOTZONE = Rcode.NOTZONE
+DSOTYPENI = Rcode.DSOTYPENI
+BADVERS = Rcode.BADVERS
+BADSIG = Rcode.BADSIG
+BADKEY = Rcode.BADKEY
+BADTIME = Rcode.BADTIME
+BADMODE = Rcode.BADMODE
+BADNAME = Rcode.BADNAME
+BADALG = Rcode.BADALG
+BADTRUNC = Rcode.BADTRUNC
+BADCOOKIE = Rcode.BADCOOKIE
+
+### END generated Rcode constants

=== added file 'dns/rdata.py'
--- old/dns/rdata.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdata.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,719 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdata."""
+
+from importlib import import_module
+import base64
+import binascii
+import io
+import inspect
+import itertools
+import random
+
+import dns.wire
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdataclass
+import dns.rdatatype
+import dns.tokenizer
+import dns.ttl
+
+_chunksize = 32
+
+
+def _wordbreak(data, chunksize=_chunksize):
+    """Break a binary string into chunks of chunksize characters separated by
+    a space.
+    """
+
+    if not chunksize:
+        return data.decode()
+    return b' '.join([data[i:i + chunksize]
+                      for i
+                      in range(0, len(data), chunksize)]).decode()
+
+
+def _hexify(data, chunksize=_chunksize, **kw):
+    """Convert a binary string into its hex encoding, broken up into chunks
+    of chunksize characters separated by a space.
+    """
+
+    return _wordbreak(binascii.hexlify(data), chunksize)
+
+
+def _base64ify(data, chunksize=_chunksize, **kw):
+    """Convert a binary string into its base64 encoding, broken up into chunks
+    of chunksize characters separated by a space.
+    """
+
+    return _wordbreak(base64.b64encode(data), chunksize)
+
+
+__escaped = b'"\\'
+
+def _escapify(qstring):
+    """Escape the characters in a quoted string which need it."""
+
+    if isinstance(qstring, str):
+        qstring = qstring.encode()
+    if not isinstance(qstring, bytearray):
+        qstring = bytearray(qstring)
+
+    text = ''
+    for c in qstring:
+        if c in __escaped:
+            text += '\\' + chr(c)
+        elif c >= 0x20 and c < 0x7F:
+            text += chr(c)
+        else:
+            text += '\\%03d' % c
+    return text
+
+
+def _truncate_bitmap(what):
+    """Determine the index of greatest byte that isn't all zeros, and
+    return the bitmap that contains all the bytes less than that index.
+    """
+
+    for i in range(len(what) - 1, -1, -1):
+        if what[i] != 0:
+            return what[0: i + 1]
+    return what[0:1]
+
+# So we don't have to edit all the rdata classes...
+_constify = dns.immutable.constify
+
+
+@dns.immutable.immutable
+class Rdata:
+    """Base class for all DNS rdata types."""
+
+    __slots__ = ['rdclass', 'rdtype', 'rdcomment']
+
+    def __init__(self, rdclass, rdtype):
+        """Initialize an rdata.
+
+        *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+        *rdtype*, an ``int`` is the rdatatype of the Rdata.
+        """
+
+        self.rdclass = self._as_rdataclass(rdclass)
+        self.rdtype = self._as_rdatatype(rdtype)
+        self.rdcomment = None
+
+    def _get_all_slots(self):
+        return itertools.chain.from_iterable(getattr(cls, '__slots__', [])
+                                             for cls in self.__class__.__mro__)
+
+    def __getstate__(self):
+        # We used to try to do a tuple of all slots here, but it
+        # doesn't work as self._all_slots isn't available at
+        # __setstate__() time.  Before that we tried to store a tuple
+        # of __slots__, but that didn't work as it didn't store the
+        # slots defined by ancestors.  This older way didn't fail
+        # outright, but ended up with partially broken objects, e.g.
+        # if you unpickled an A RR it wouldn't have rdclass and rdtype
+        # attributes, and would compare badly.
+        state = {}
+        for slot in self._get_all_slots():
+            state[slot] = getattr(self, slot)
+        return state
+
+    def __setstate__(self, state):
+        for slot, val in state.items():
+            object.__setattr__(self, slot, val)
+        if not hasattr(self, 'rdcomment'):
+            # Pickled rdata from 2.0.x might not have a rdcomment, so add
+            # it if needed.
+            object.__setattr__(self, 'rdcomment', None)
+
+    def covers(self):
+        """Return the type a Rdata covers.
+
+        DNS SIG/RRSIG rdatas apply to a specific type; this type is
+        returned by the covers() function.  If the rdata type is not
+        SIG or RRSIG, dns.rdatatype.NONE is returned.  This is useful when
+        creating rdatasets, allowing the rdataset to contain only RRSIGs
+        of a particular type, e.g. RRSIG(NS).
+
+        Returns an ``int``.
+        """
+
+        return dns.rdatatype.NONE
+
+    def extended_rdatatype(self):
+        """Return a 32-bit type value, the least significant 16 bits of
+        which are the ordinary DNS type, and the upper 16 bits of which are
+        the "covered" type, if any.
+
+        Returns an ``int``.
+        """
+
+        return self.covers() << 16 | self.rdtype
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        """Convert an rdata to text format.
+
+        Returns a ``str``.
+        """
+
+        raise NotImplementedError  # pragma: no cover
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        raise NotImplementedError  # pragma: no cover
+
+    def to_wire(self, file=None, compress=None, origin=None,
+                canonicalize=False):
+        """Convert an rdata to wire format.
+
+        Returns a ``bytes`` or ``None``.
+        """
+
+        if file:
+            return self._to_wire(file, compress, origin, canonicalize)
+        else:
+            f = io.BytesIO()
+            self._to_wire(f, compress, origin, canonicalize)
+            return f.getvalue()
+
+    def to_generic(self, origin=None):
+        """Creates a dns.rdata.GenericRdata equivalent of this rdata.
+
+        Returns a ``dns.rdata.GenericRdata``.
+        """
+        return dns.rdata.GenericRdata(self.rdclass, self.rdtype,
+                                      self.to_wire(origin=origin))
+
+    def to_digestable(self, origin=None):
+        """Convert rdata to a format suitable for digesting in hashes.  This
+        is also the DNSSEC canonical form.
+
+        Returns a ``bytes``.
+        """
+
+        return self.to_wire(origin=origin, canonicalize=True)
+
+    def __repr__(self):
+        covers = self.covers()
+        if covers == dns.rdatatype.NONE:
+            ctext = ''
+        else:
+            ctext = '(' + dns.rdatatype.to_text(covers) + ')'
+        return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
+               dns.rdatatype.to_text(self.rdtype) + ctext + ' rdata: ' + \
+               str(self) + '>'
+
+    def __str__(self):
+        return self.to_text()
+
+    def _cmp(self, other):
+        """Compare an rdata with another rdata of the same rdtype and
+        rdclass.
+
+        Return < 0 if self < other in the DNSSEC ordering, 0 if self
+        == other, and > 0 if self > other.
+
+        """
+        our = self.to_digestable(dns.name.root)
+        their = other.to_digestable(dns.name.root)
+        if our == their:
+            return 0
+        elif our > their:
+            return 1
+        else:
+            return -1
+
+    def __eq__(self, other):
+        if not isinstance(other, Rdata):
+            return False
+        if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+            return False
+        return self._cmp(other) == 0
+
+    def __ne__(self, other):
+        if not isinstance(other, Rdata):
+            return True
+        if self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+            return True
+        return self._cmp(other) != 0
+
+    def __lt__(self, other):
+        if not isinstance(other, Rdata) or \
+                self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+
+            return NotImplemented
+        return self._cmp(other) < 0
+
+    def __le__(self, other):
+        if not isinstance(other, Rdata) or \
+                self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+            return NotImplemented
+        return self._cmp(other) <= 0
+
+    def __ge__(self, other):
+        if not isinstance(other, Rdata) or \
+                self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+            return NotImplemented
+        return self._cmp(other) >= 0
+
+    def __gt__(self, other):
+        if not isinstance(other, Rdata) or \
+                self.rdclass != other.rdclass or self.rdtype != other.rdtype:
+            return NotImplemented
+        return self._cmp(other) > 0
+
+    def __hash__(self):
+        return hash(self.to_digestable(dns.name.root))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        raise NotImplementedError  # pragma: no cover
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        raise NotImplementedError  # pragma: no cover
+
+    def replace(self, **kwargs):
+        """
+        Create a new Rdata instance based on the instance replace was
+        invoked on. It is possible to pass different parameters to
+        override the corresponding properties of the base Rdata.
+
+        Any field specific to the Rdata type can be replaced, but the
+        *rdtype* and *rdclass* fields cannot.
+
+        Returns an instance of the same Rdata subclass as *self*.
+        """
+
+        # Get the constructor parameters.
+        parameters = inspect.signature(self.__init__).parameters
+
+        # Ensure that all of the arguments correspond to valid fields.
+        # Don't allow rdclass or rdtype to be changed, though.
+        for key in kwargs:
+            if key == 'rdcomment':
+                continue
+            if key not in parameters:
+                raise AttributeError("'{}' object has no attribute '{}'"
+                                     .format(self.__class__.__name__, key))
+            if key in ('rdclass', 'rdtype'):
+                raise AttributeError("Cannot overwrite '{}' attribute '{}'"
+                                     .format(self.__class__.__name__, key))
+
+        # Construct the parameter list.  For each field, use the value in
+        # kwargs if present, and the current value otherwise.
+        args = (kwargs.get(key, getattr(self, key)) for key in parameters)
+
+        # Create, validate, and return the new object.
+        rd = self.__class__(*args)
+        # The comment is not set in the constructor, so give it special
+        # handling.
+        rdcomment = kwargs.get('rdcomment', self.rdcomment)
+        if rdcomment is not None:
+            object.__setattr__(rd, 'rdcomment', rdcomment)
+        return rd
+
+    # Type checking and conversion helpers.  These are class methods as
+    # they don't touch object state and may be useful to others.
+
+    @classmethod
+    def _as_rdataclass(cls, value):
+        return dns.rdataclass.RdataClass.make(value)
+
+    @classmethod
+    def _as_rdatatype(cls, value):
+        return dns.rdatatype.RdataType.make(value)
+
+    @classmethod
+    def _as_bytes(cls, value, encode=False, max_length=None, empty_ok=True):
+        if encode and isinstance(value, str):
+            value = value.encode()
+        elif isinstance(value, bytearray):
+            value = bytes(value)
+        elif not isinstance(value, bytes):
+            raise ValueError('not bytes')
+        if max_length is not None and len(value) > max_length:
+            raise ValueError('too long')
+        if not empty_ok and len(value) == 0:
+            raise ValueError('empty bytes not allowed')
+        return value
+
+    @classmethod
+    def _as_name(cls, value):
+        # Note that proper name conversion (e.g. with origin and IDNA
+        # awareness) is expected to be done via from_text.  This is just
+        # a simple thing for people invoking the constructor directly.
+        if isinstance(value, str):
+            return dns.name.from_text(value)
+        elif not isinstance(value, dns.name.Name):
+            raise ValueError('not a name')
+        return value
+
+    @classmethod
+    def _as_uint8(cls, value):
+        if not isinstance(value, int):
+            raise ValueError('not an integer')
+        if value < 0 or value > 255:
+            raise ValueError('not a uint8')
+        return value
+
+    @classmethod
+    def _as_uint16(cls, value):
+        if not isinstance(value, int):
+            raise ValueError('not an integer')
+        if value < 0 or value > 65535:
+            raise ValueError('not a uint16')
+        return value
+
+    @classmethod
+    def _as_uint32(cls, value):
+        if not isinstance(value, int):
+            raise ValueError('not an integer')
+        if value < 0 or value > 4294967295:
+            raise ValueError('not a uint32')
+        return value
+
+    @classmethod
+    def _as_uint48(cls, value):
+        if not isinstance(value, int):
+            raise ValueError('not an integer')
+        if value < 0 or value > 281474976710655:
+            raise ValueError('not a uint48')
+        return value
+
+    @classmethod
+    def _as_int(cls, value, low=None, high=None):
+        if not isinstance(value, int):
+            raise ValueError('not an integer')
+        if low is not None and value < low:
+            raise ValueError('value too small')
+        if high is not None and value > high:
+            raise ValueError('value too large')
+        return value
+
+    @classmethod
+    def _as_ipv4_address(cls, value):
+        if isinstance(value, str):
+            # call to check validity
+            dns.ipv4.inet_aton(value)
+            return value
+        elif isinstance(value, bytes):
+            return dns.ipv4.inet_ntoa(value)
+        else:
+            raise ValueError('not an IPv4 address')
+
+    @classmethod
+    def _as_ipv6_address(cls, value):
+        if isinstance(value, str):
+            # call to check validity
+            dns.ipv6.inet_aton(value)
+            return value
+        elif isinstance(value, bytes):
+            return dns.ipv6.inet_ntoa(value)
+        else:
+            raise ValueError('not an IPv6 address')
+
+    @classmethod
+    def _as_bool(cls, value):
+        if isinstance(value, bool):
+            return value
+        else:
+            raise ValueError('not a boolean')
+
+    @classmethod
+    def _as_ttl(cls, value):
+        if isinstance(value, int):
+            return cls._as_int(value, 0, dns.ttl.MAX_TTL)
+        elif isinstance(value, str):
+            return dns.ttl.from_text(value)
+        else:
+            raise ValueError('not a TTL')
+
+    @classmethod
+    def _as_tuple(cls, value, as_value):
+        try:
+            # For user convenience, if value is a singleton of the list
+            # element type, wrap it in a tuple.
+            return (as_value(value),)
+        except Exception:
+            # Otherwise, check each element of the iterable *value*
+            # against *as_value*.
+            return tuple(as_value(v) for v in value)
+
+    # Processing order
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        items = list(iterable)
+        random.shuffle(items)
+        return items
+
+
+class GenericRdata(Rdata):
+
+    """Generic Rdata Class
+
+    This class is used for rdata types for which we have no better
+    implementation.  It implements the DNS "unknown RRs" scheme.
+    """
+
+    __slots__ = ['data']
+
+    def __init__(self, rdclass, rdtype, data):
+        super().__init__(rdclass, rdtype)
+        object.__setattr__(self, 'data', data)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return r'\# %d ' % len(self.data) + _hexify(self.data, **kw)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        token = tok.get()
+        if not token.is_identifier() or token.value != r'\#':
+            raise dns.exception.SyntaxError(
+                r'generic rdata does not start with \#')
+        length = tok.get_int()
+        hex = tok.concatenate_remaining_identifiers().encode()
+        data = binascii.unhexlify(hex)
+        if len(data) != length:
+            raise dns.exception.SyntaxError(
+                'generic rdata hex data has wrong length')
+        return cls(rdclass, rdtype, data)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(self.data)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        return cls(rdclass, rdtype, parser.get_remaining())
+
+_rdata_classes = {}
+_module_prefix = 'dns.rdtypes'
+
+def get_rdata_class(rdclass, rdtype):
+    cls = _rdata_classes.get((rdclass, rdtype))
+    if not cls:
+        cls = _rdata_classes.get((dns.rdatatype.ANY, rdtype))
+        if not cls:
+            rdclass_text = dns.rdataclass.to_text(rdclass)
+            rdtype_text = dns.rdatatype.to_text(rdtype)
+            rdtype_text = rdtype_text.replace('-', '_')
+            try:
+                mod = import_module('.'.join([_module_prefix,
+                                              rdclass_text, rdtype_text]))
+                cls = getattr(mod, rdtype_text)
+                _rdata_classes[(rdclass, rdtype)] = cls
+            except ImportError:
+                try:
+                    mod = import_module('.'.join([_module_prefix,
+                                                  'ANY', rdtype_text]))
+                    cls = getattr(mod, rdtype_text)
+                    _rdata_classes[(dns.rdataclass.ANY, rdtype)] = cls
+                    _rdata_classes[(rdclass, rdtype)] = cls
+                except ImportError:
+                    pass
+    if not cls:
+        cls = GenericRdata
+        _rdata_classes[(rdclass, rdtype)] = cls
+    return cls
+
+
+def from_text(rdclass, rdtype, tok, origin=None, relativize=True,
+              relativize_to=None, idna_codec=None):
+    """Build an rdata object from text format.
+
+    This function attempts to dynamically load a class which
+    implements the specified rdata class and type.  If there is no
+    class-and-type-specific implementation, the GenericRdata class
+    is used.
+
+    Once a class is chosen, its from_text() class method is called
+    with the parameters to this function.
+
+    If *tok* is a ``str``, then a tokenizer is created and the string
+    is used as its input.
+
+    *rdclass*, an ``int``, the rdataclass.
+
+    *rdtype*, an ``int``, the rdatatype.
+
+    *tok*, a ``dns.tokenizer.Tokenizer`` or a ``str``.
+
+    *origin*, a ``dns.name.Name`` (or ``None``), the
+    origin to use for relative names.
+
+    *relativize*, a ``bool``.  If true, name will be relativized.
+
+    *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+    when relativizing names.  If not set, the *origin* value will be used.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder to use if a tokenizer needs to be created.  If
+    ``None``, the default IDNA 2003 encoder/decoder is used.  If a
+    tokenizer is not created, then the codec associated with the tokenizer
+    is the one that is used.
+
+    Returns an instance of the chosen Rdata subclass.
+
+    """
+    if isinstance(tok, str):
+        tok = dns.tokenizer.Tokenizer(tok, idna_codec=idna_codec)
+    rdclass = dns.rdataclass.RdataClass.make(rdclass)
+    rdtype = dns.rdatatype.RdataType.make(rdtype)
+    cls = get_rdata_class(rdclass, rdtype)
+    with dns.exception.ExceptionWrapper(dns.exception.SyntaxError):
+        rdata = None
+        if cls != GenericRdata:
+            # peek at first token
+            token = tok.get()
+            tok.unget(token)
+            if token.is_identifier() and \
+               token.value == r'\#':
+                #
+                # Known type using the generic syntax.  Extract the
+                # wire form from the generic syntax, and then run
+                # from_wire on it.
+                #
+                grdata = GenericRdata.from_text(rdclass, rdtype, tok, origin,
+                                                relativize, relativize_to)
+                rdata = from_wire(rdclass, rdtype, grdata.data, 0,
+                                  len(grdata.data), origin)
+                #
+                # If this comparison isn't equal, then there must have been
+                # compressed names in the wire format, which is an error,
+                # there being no reasonable context to decompress with.
+                #
+                rwire = rdata.to_wire()
+                if rwire != grdata.data:
+                    raise dns.exception.SyntaxError('compressed data in '
+                                                    'generic syntax form '
+                                                    'of known rdatatype')
+        if rdata is None:
+            rdata = cls.from_text(rdclass, rdtype, tok, origin, relativize,
+                                  relativize_to)
+        token = tok.get_eol_as_token()
+        if token.comment is not None:
+            object.__setattr__(rdata, 'rdcomment', token.comment)
+        return rdata
+
+
+def from_wire_parser(rdclass, rdtype, parser, origin=None):
+    """Build an rdata object from wire format
+
+    This function attempts to dynamically load a class which
+    implements the specified rdata class and type.  If there is no
+    class-and-type-specific implementation, the GenericRdata class
+    is used.
+
+    Once a class is chosen, its from_wire() class method is called
+    with the parameters to this function.
+
+    *rdclass*, an ``int``, the rdataclass.
+
+    *rdtype*, an ``int``, the rdatatype.
+
+    *parser*, a ``dns.wire.Parser``, the parser, which should be
+    restricted to the rdata length.
+
+    *origin*, a ``dns.name.Name`` (or ``None``).  If not ``None``,
+    then names will be relativized to this origin.
+
+    Returns an instance of the chosen Rdata subclass.
+    """
+
+    rdclass = dns.rdataclass.RdataClass.make(rdclass)
+    rdtype = dns.rdatatype.RdataType.make(rdtype)
+    cls = get_rdata_class(rdclass, rdtype)
+    with dns.exception.ExceptionWrapper(dns.exception.FormError):
+        return cls.from_wire_parser(rdclass, rdtype, parser, origin)
+
+
+def from_wire(rdclass, rdtype, wire, current, rdlen, origin=None):
+    """Build an rdata object from wire format
+
+    This function attempts to dynamically load a class which
+    implements the specified rdata class and type.  If there is no
+    class-and-type-specific implementation, the GenericRdata class
+    is used.
+
+    Once a class is chosen, its from_wire() class method is called
+    with the parameters to this function.
+
+    *rdclass*, an ``int``, the rdataclass.
+
+    *rdtype*, an ``int``, the rdatatype.
+
+    *wire*, a ``bytes``, the wire-format message.
+
+    *current*, an ``int``, the offset in wire of the beginning of
+    the rdata.
+
+    *rdlen*, an ``int``, the length of the wire-format rdata
+
+    *origin*, a ``dns.name.Name`` (or ``None``).  If not ``None``,
+    then names will be relativized to this origin.
+
+    Returns an instance of the chosen Rdata subclass.
+    """
+    parser = dns.wire.Parser(wire, current)
+    with parser.restrict_to(rdlen):
+        return from_wire_parser(rdclass, rdtype, parser, origin)
+
+
+class RdatatypeExists(dns.exception.DNSException):
+    """DNS rdatatype already exists."""
+    supp_kwargs = {'rdclass', 'rdtype'}
+    fmt = "The rdata type with class {rdclass} and rdtype {rdtype} " + \
+        "already exists."
+
+
+def register_type(implementation, rdtype, rdtype_text, is_singleton=False,
+                  rdclass=dns.rdataclass.IN):
+    """Dynamically register a module to handle an rdatatype.
+
+    *implementation*, a module implementing the type in the usual dnspython
+    way.
+
+    *rdtype*, an ``int``, the rdatatype to register.
+
+    *rdtype_text*, a ``str``, the textual form of the rdatatype.
+
+    *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
+    RRsets of the type can have only one member.)
+
+    *rdclass*, the rdataclass of the type, or ``dns.rdataclass.ANY`` if
+    it applies to all classes.
+    """
+
+    existing_cls = get_rdata_class(rdclass, rdtype)
+    if existing_cls != GenericRdata or dns.rdatatype.is_metatype(rdtype):
+        raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype)
+    try:
+        if dns.rdatatype.RdataType(rdtype).name != rdtype_text:
+            raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype)
+    except ValueError:
+        pass
+    _rdata_classes[(rdclass, rdtype)] = getattr(implementation,
+                                                rdtype_text.replace('-', '_'))
+    dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton)

=== added file 'dns/rdata.pyi'
--- old/dns/rdata.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/rdata.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,19 @@
+from typing import Dict, Tuple, Any, Optional, BinaryIO
+from .name import Name, IDNACodec
+class Rdata:
+    def __init__(self):
+        self.address : str
+    def to_wire(self, file : Optional[BinaryIO], compress : Optional[Dict[Name,int]], origin : Optional[Name], canonicalize : Optional[bool]) -> Optional[bytes]:
+        ...
+    @classmethod
+    def from_text(cls, rdclass : int, rdtype : int, tok, origin=None, relativize=True):
+        ...
+_rdata_modules : Dict[Tuple[Any,Rdata],Any]
+
+def from_text(rdclass : int, rdtype : int, tok : Optional[str], origin : Optional[Name] = None,
+              relativize : bool = True, relativize_to : Optional[Name] = None,
+              idna_codec : Optional[IDNACodec] = None):
+    ...
+
+def from_wire(rdclass : int, rdtype : int, wire : bytes, current : int, rdlen : int, origin : Optional[Name] = None):
+    ...

=== added file 'dns/rdataclass.py'
--- old/dns/rdataclass.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdataclass.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,115 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Rdata Classes."""
+
+import dns.enum
+import dns.exception
+
+class RdataClass(dns.enum.IntEnum):
+    """DNS Rdata Class"""
+    RESERVED0 = 0
+    IN = 1
+    INTERNET = IN
+    CH = 3
+    CHAOS = CH
+    HS = 4
+    HESIOD = HS
+    NONE = 254
+    ANY = 255
+
+    @classmethod
+    def _maximum(cls):
+        return 65535
+
+    @classmethod
+    def _short_name(cls):
+        return "class"
+
+    @classmethod
+    def _prefix(cls):
+        return "CLASS"
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownRdataclass
+
+
+_metaclasses = {RdataClass.NONE, RdataClass.ANY}
+
+
+class UnknownRdataclass(dns.exception.DNSException):
+    """A DNS class is unknown."""
+
+
+def from_text(text):
+    """Convert text into a DNS rdata class value.
+
+    The input text can be a defined DNS RR class mnemonic or
+    instance of the DNS generic class syntax.
+
+    For example, "IN" and "CLASS1" will both result in a value of 1.
+
+    Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown.
+
+    Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
+
+    Returns an ``int``.
+    """
+
+    return RdataClass.from_text(text)
+
+
+def to_text(value):
+    """Convert a DNS rdata class value to text.
+
+    If the value has a known mnemonic, it will be used, otherwise the
+    DNS generic class syntax will be used.
+
+    Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535.
+
+    Returns a ``str``.
+    """
+
+    return RdataClass.to_text(value)
+
+
+def is_metaclass(rdclass):
+    """True if the specified class is a metaclass.
+
+    The currently defined metaclasses are ANY and NONE.
+
+    *rdclass* is an ``int``.
+    """
+
+    if rdclass in _metaclasses:
+        return True
+    return False
+
+### BEGIN generated RdataClass constants
+
+RESERVED0 = RdataClass.RESERVED0
+IN = RdataClass.IN
+INTERNET = RdataClass.INTERNET
+CH = RdataClass.CH
+CHAOS = RdataClass.CHAOS
+HS = RdataClass.HS
+HESIOD = RdataClass.HESIOD
+NONE = RdataClass.NONE
+ANY = RdataClass.ANY
+
+### END generated RdataClass constants

=== added file 'dns/rdataset.py'
--- old/dns/rdataset.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdataset.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,456 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdatasets (an rdataset is a set of rdatas of a given type and class)"""
+
+import io
+import random
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdatatype
+import dns.rdataclass
+import dns.rdata
+import dns.set
+
+# define SimpleSet here for backwards compatibility
+SimpleSet = dns.set.Set
+
+
+class DifferingCovers(dns.exception.DNSException):
+    """An attempt was made to add a DNS SIG/RRSIG whose covered type
+    is not the same as that of the other rdatas in the rdataset."""
+
+
+class IncompatibleTypes(dns.exception.DNSException):
+    """An attempt was made to add DNS RR data of an incompatible type."""
+
+
+class Rdataset(dns.set.Set):
+
+    """A DNS rdataset."""
+
+    __slots__ = ['rdclass', 'rdtype', 'covers', 'ttl']
+
+    def __init__(self, rdclass, rdtype, covers=dns.rdatatype.NONE, ttl=0):
+        """Create a new rdataset of the specified class and type.
+
+        *rdclass*, an ``int``, the rdataclass.
+
+        *rdtype*, an ``int``, the rdatatype.
+
+        *covers*, an ``int``, the covered rdatatype.
+
+        *ttl*, an ``int``, the TTL.
+        """
+
+        super().__init__()
+        self.rdclass = rdclass
+        self.rdtype = rdtype
+        self.covers = covers
+        self.ttl = ttl
+
+    def _clone(self):
+        obj = super()._clone()
+        obj.rdclass = self.rdclass
+        obj.rdtype = self.rdtype
+        obj.covers = self.covers
+        obj.ttl = self.ttl
+        return obj
+
+    def update_ttl(self, ttl):
+        """Perform TTL minimization.
+
+        Set the TTL of the rdataset to be the lesser of the set's current
+        TTL or the specified TTL.  If the set contains no rdatas, set the TTL
+        to the specified TTL.
+
+        *ttl*, an ``int`` or ``str``.
+        """
+        ttl = dns.ttl.make(ttl)
+        if len(self) == 0:
+            self.ttl = ttl
+        elif ttl < self.ttl:
+            self.ttl = ttl
+
+    def add(self, rd, ttl=None):  # pylint: disable=arguments-differ
+        """Add the specified rdata to the rdataset.
+
+        If the optional *ttl* parameter is supplied, then
+        ``self.update_ttl(ttl)`` will be called prior to adding the rdata.
+
+        *rd*, a ``dns.rdata.Rdata``, the rdata
+
+        *ttl*, an ``int``, the TTL.
+
+        Raises ``dns.rdataset.IncompatibleTypes`` if the type and class
+        do not match the type and class of the rdataset.
+
+        Raises ``dns.rdataset.DifferingCovers`` if the type is a signature
+        type and the covered type does not match that of the rdataset.
+        """
+
+        #
+        # If we're adding a signature, do some special handling to
+        # check that the signature covers the same type as the
+        # other rdatas in this rdataset.  If this is the first rdata
+        # in the set, initialize the covers field.
+        #
+        if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:
+            raise IncompatibleTypes
+        if ttl is not None:
+            self.update_ttl(ttl)
+        if self.rdtype == dns.rdatatype.RRSIG or \
+           self.rdtype == dns.rdatatype.SIG:
+            covers = rd.covers()
+            if len(self) == 0 and self.covers == dns.rdatatype.NONE:
+                self.covers = covers
+            elif self.covers != covers:
+                raise DifferingCovers
+        if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:
+            self.clear()
+        super().add(rd)
+
+    def union_update(self, other):
+        self.update_ttl(other.ttl)
+        super().union_update(other)
+
+    def intersection_update(self, other):
+        self.update_ttl(other.ttl)
+        super().intersection_update(other)
+
+    def update(self, other):
+        """Add all rdatas in other to self.
+
+        *other*, a ``dns.rdataset.Rdataset``, the rdataset from which
+        to update.
+        """
+
+        self.update_ttl(other.ttl)
+        super().update(other)
+
+    def _rdata_repr(self):
+        def maybe_truncate(s):
+            if len(s) > 100:
+                return s[:100] + '...'
+            return s
+        return '[%s]' % ', '.join('<%s>' % maybe_truncate(str(rr))
+                                  for rr in self)
+
+    def __repr__(self):
+        if self.covers == 0:
+            ctext = ''
+        else:
+            ctext = '(' + dns.rdatatype.to_text(self.covers) + ')'
+        return '<DNS ' + dns.rdataclass.to_text(self.rdclass) + ' ' + \
+               dns.rdatatype.to_text(self.rdtype) + ctext + \
+               ' rdataset: ' + self._rdata_repr() + '>'
+
+    def __str__(self):
+        return self.to_text()
+
+    def __eq__(self, other):
+        if not isinstance(other, Rdataset):
+            return False
+        if self.rdclass != other.rdclass or \
+           self.rdtype != other.rdtype or \
+           self.covers != other.covers:
+            return False
+        return super().__eq__(other)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def to_text(self, name=None, origin=None, relativize=True,
+                override_rdclass=None, want_comments=False, **kw):
+        """Convert the rdataset into DNS zone file format.
+
+        See ``dns.name.Name.choose_relativity`` for more information
+        on how *origin* and *relativize* determine the way names
+        are emitted.
+
+        Any additional keyword arguments are passed on to the rdata
+        ``to_text()`` method.
+
+        *name*, a ``dns.name.Name``.  If name is not ``None``, emit RRs with
+        *name* as the owner name.
+
+        *origin*, a ``dns.name.Name`` or ``None``, the origin for relative
+        names.
+
+        *relativize*, a ``bool``.  If ``True``, names will be relativized
+        to *origin*.
+
+        *override_rdclass*, a ``dns.rdataclass.RdataClass`` or ``None``.
+        If not ``None``, use this class instead of the Rdataset's class.
+
+        *want_comments*, a ``bool``.  If ``True``, emit comments for rdata
+        which have them.  The default is ``False``.
+        """
+
+        if name is not None:
+            name = name.choose_relativity(origin, relativize)
+            ntext = str(name)
+            pad = ' '
+        else:
+            ntext = ''
+            pad = ''
+        s = io.StringIO()
+        if override_rdclass is not None:
+            rdclass = override_rdclass
+        else:
+            rdclass = self.rdclass
+        if len(self) == 0:
+            #
+            # Empty rdatasets are used for the question section, and in
+            # some dynamic updates, so we don't need to print out the TTL
+            # (which is meaningless anyway).
+            #
+            s.write('{}{}{} {}\n'.format(ntext, pad,
+                                         dns.rdataclass.to_text(rdclass),
+                                         dns.rdatatype.to_text(self.rdtype)))
+        else:
+            for rd in self:
+                extra = ''
+                if want_comments:
+                    if rd.rdcomment:
+                        extra = f' ;{rd.rdcomment}'
+                s.write('%s%s%d %s %s %s%s\n' %
+                        (ntext, pad, self.ttl, dns.rdataclass.to_text(rdclass),
+                         dns.rdatatype.to_text(self.rdtype),
+                         rd.to_text(origin=origin, relativize=relativize,
+                                    **kw),
+                         extra))
+        #
+        # We strip off the final \n for the caller's convenience in printing
+        #
+        return s.getvalue()[:-1]
+
+    def to_wire(self, name, file, compress=None, origin=None,
+                override_rdclass=None, want_shuffle=True):
+        """Convert the rdataset to wire format.
+
+        *name*, a ``dns.name.Name`` is the owner name to use.
+
+        *file* is the file where the name is emitted (typically a
+        BytesIO file).
+
+        *compress*, a ``dict``, is the compression table to use.  If
+        ``None`` (the default), names will not be compressed.
+
+        *origin* is a ``dns.name.Name`` or ``None``.  If the name is
+        relative and origin is not ``None``, then *origin* will be appended
+        to it.
+
+        *override_rdclass*, an ``int``, is used as the class instead of the
+        class of the rdataset.  This is useful when rendering rdatasets
+        associated with dynamic updates.
+
+        *want_shuffle*, a ``bool``.  If ``True``, then the order of the
+        Rdatas within the Rdataset will be shuffled before rendering.
+
+        Returns an ``int``, the number of records emitted.
+        """
+
+        if override_rdclass is not None:
+            rdclass = override_rdclass
+            want_shuffle = False
+        else:
+            rdclass = self.rdclass
+        file.seek(0, io.SEEK_END)
+        if len(self) == 0:
+            name.to_wire(file, compress, origin)
+            stuff = struct.pack("!HHIH", self.rdtype, rdclass, 0, 0)
+            file.write(stuff)
+            return 1
+        else:
+            if want_shuffle:
+                l = list(self)
+                random.shuffle(l)
+            else:
+                l = self
+            for rd in l:
+                name.to_wire(file, compress, origin)
+                stuff = struct.pack("!HHIH", self.rdtype, rdclass,
+                                    self.ttl, 0)
+                file.write(stuff)
+                start = file.tell()
+                rd.to_wire(file, compress, origin)
+                end = file.tell()
+                assert end - start < 65536
+                file.seek(start - 2)
+                stuff = struct.pack("!H", end - start)
+                file.write(stuff)
+                file.seek(0, io.SEEK_END)
+            return len(self)
+
+    def match(self, rdclass, rdtype, covers):
+        """Returns ``True`` if this rdataset matches the specified class,
+        type, and covers.
+        """
+        if self.rdclass == rdclass and \
+           self.rdtype == rdtype and \
+           self.covers == covers:
+            return True
+        return False
+
+    def processing_order(self):
+        """Return rdatas in a valid processing order according to the type's
+        specification.  For example, MX records are in preference order from
+        lowest to highest preferences, with items of the same perference
+        shuffled.
+
+        For types that do not define a processing order, the rdatas are
+        simply shuffled.
+        """
+        if len(self) == 0:
+            return []
+        else:
+            return self[0]._processing_order(iter(self))
+
+
+@dns.immutable.immutable
+class ImmutableRdataset(Rdataset):
+
+    """An immutable DNS rdataset."""
+
+    _clone_class = Rdataset
+
+    def __init__(self, rdataset):
+        """Create an immutable rdataset from the specified rdataset."""
+
+        super().__init__(rdataset.rdclass, rdataset.rdtype, rdataset.covers,
+                         rdataset.ttl)
+        self.items = dns.immutable.Dict(rdataset.items)
+
+    def update_ttl(self, ttl):
+        raise TypeError('immutable')
+
+    def add(self, rd, ttl=None):
+        raise TypeError('immutable')
+
+    def union_update(self, other):
+        raise TypeError('immutable')
+
+    def intersection_update(self, other):
+        raise TypeError('immutable')
+
+    def update(self, other):
+        raise TypeError('immutable')
+
+    def __delitem__(self, i):
+        raise TypeError('immutable')
+
+    def __ior__(self, other):
+        raise TypeError('immutable')
+
+    def __iand__(self, other):
+        raise TypeError('immutable')
+
+    def __iadd__(self, other):
+        raise TypeError('immutable')
+
+    def __isub__(self, other):
+        raise TypeError('immutable')
+
+    def clear(self):
+        raise TypeError('immutable')
+
+    def __copy__(self):
+        return ImmutableRdataset(super().copy())
+
+    def copy(self):
+        return ImmutableRdataset(super().copy())
+
+    def union(self, other):
+        return ImmutableRdataset(super().union(other))
+
+    def intersection(self, other):
+        return ImmutableRdataset(super().intersection(other))
+
+    def difference(self, other):
+        return ImmutableRdataset(super().difference(other))
+
+
+def from_text_list(rdclass, rdtype, ttl, text_rdatas, idna_codec=None,
+                   origin=None, relativize=True, relativize_to=None):
+    """Create an rdataset with the specified class, type, and TTL, and with
+    the specified list of rdatas in text format.
+
+    *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA
+    encoder/decoder to use; if ``None``, the default IDNA 2003
+    encoder/decoder is used.
+
+    *origin*, a ``dns.name.Name`` (or ``None``), the
+    origin to use for relative names.
+
+    *relativize*, a ``bool``.  If true, name will be relativized.
+
+    *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use
+    when relativizing names.  If not set, the *origin* value will be used.
+
+    Returns a ``dns.rdataset.Rdataset`` object.
+    """
+
+    rdclass = dns.rdataclass.RdataClass.make(rdclass)
+    rdtype = dns.rdatatype.RdataType.make(rdtype)
+    r = Rdataset(rdclass, rdtype)
+    r.update_ttl(ttl)
+    for t in text_rdatas:
+        rd = dns.rdata.from_text(r.rdclass, r.rdtype, t, origin, relativize,
+                                 relativize_to, idna_codec)
+        r.add(rd)
+    return r
+
+
+def from_text(rdclass, rdtype, ttl, *text_rdatas):
+    """Create an rdataset with the specified class, type, and TTL, and with
+    the specified rdatas in text format.
+
+    Returns a ``dns.rdataset.Rdataset`` object.
+    """
+
+    return from_text_list(rdclass, rdtype, ttl, text_rdatas)
+
+
+def from_rdata_list(ttl, rdatas):
+    """Create an rdataset with the specified TTL, and with
+    the specified list of rdata objects.
+
+    Returns a ``dns.rdataset.Rdataset`` object.
+    """
+
+    if len(rdatas) == 0:
+        raise ValueError("rdata list must not be empty")
+    r = None
+    for rd in rdatas:
+        if r is None:
+            r = Rdataset(rd.rdclass, rd.rdtype)
+            r.update_ttl(ttl)
+        r.add(rd)
+    return r
+
+
+def from_rdata(ttl, *rdatas):
+    """Create an rdataset with the specified TTL, and with
+    the specified rdata objects.
+
+    Returns a ``dns.rdataset.Rdataset`` object.
+    """
+
+    return from_rdata_list(ttl, rdatas)

=== added file 'dns/rdataset.pyi'
--- old/dns/rdataset.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/rdataset.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,58 @@
+from typing import Optional, Dict, List, Union
+from io import BytesIO
+from . import exception, name, set, rdatatype, rdata, rdataset
+
+class DifferingCovers(exception.DNSException):
+    """An attempt was made to add a DNS SIG/RRSIG whose covered type
+    is not the same as that of the other rdatas in the rdataset."""
+
+
+class IncompatibleTypes(exception.DNSException):
+    """An attempt was made to add DNS RR data of an incompatible type."""
+
+
+class Rdataset(set.Set):
+    def __init__(self, rdclass, rdtype, covers=rdatatype.NONE, ttl=0):
+        self.rdclass : int = rdclass
+        self.rdtype : int = rdtype
+        self.covers : int = covers
+        self.ttl : int = ttl
+
+    def update_ttl(self, ttl : int) -> None:
+        ...
+
+    def add(self, rd : rdata.Rdata, ttl : Optional[int] =None):
+        ...
+
+    def union_update(self, other : Rdataset):
+        ...
+
+    def intersection_update(self, other : Rdataset):
+        ...
+
+    def update(self, other : Rdataset):
+        ...
+
+    def to_text(self, name : Optional[name.Name] =None, origin : Optional[name.Name] =None, relativize=True,
+                override_rdclass : Optional[int] =None, **kw) -> bytes:
+        ...
+
+    def to_wire(self, name : Optional[name.Name], file : BytesIO, compress : Optional[Dict[name.Name, int]] = None, origin : Optional[name.Name] = None,
+                override_rdclass : Optional[int] = None, want_shuffle=True) -> int:
+        ...
+
+    def match(self, rdclass : int, rdtype : int, covers : int) -> bool:
+        ...
+
+
+def from_text_list(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, text_rdatas : str, idna_codec : Optional[name.IDNACodec] = None) -> rdataset.Rdataset:
+    ...
+
+def from_text(rdclass : Union[int,str], rdtype : Union[int,str], ttl : int, *text_rdatas : str) -> rdataset.Rdataset:
+    ...
+
+def from_rdata_list(ttl : int, rdatas : List[rdata.Rdata]) -> rdataset.Rdataset:
+    ...
+
+def from_rdata(ttl : int, *rdatas : List[rdata.Rdata]) -> rdataset.Rdataset:
+    ...

=== added file 'dns/rdatatype.py'
--- old/dns/rdatatype.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdatatype.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,305 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS Rdata Types."""
+
+import dns.enum
+import dns.exception
+
+class RdataType(dns.enum.IntEnum):
+    """DNS Rdata Type"""
+    TYPE0 = 0
+    NONE = 0
+    A = 1
+    NS = 2
+    MD = 3
+    MF = 4
+    CNAME = 5
+    SOA = 6
+    MB = 7
+    MG = 8
+    MR = 9
+    NULL = 10
+    WKS = 11
+    PTR = 12
+    HINFO = 13
+    MINFO = 14
+    MX = 15
+    TXT = 16
+    RP = 17
+    AFSDB = 18
+    X25 = 19
+    ISDN = 20
+    RT = 21
+    NSAP = 22
+    NSAP_PTR = 23
+    SIG = 24
+    KEY = 25
+    PX = 26
+    GPOS = 27
+    AAAA = 28
+    LOC = 29
+    NXT = 30
+    SRV = 33
+    NAPTR = 35
+    KX = 36
+    CERT = 37
+    A6 = 38
+    DNAME = 39
+    OPT = 41
+    APL = 42
+    DS = 43
+    SSHFP = 44
+    IPSECKEY = 45
+    RRSIG = 46
+    NSEC = 47
+    DNSKEY = 48
+    DHCID = 49
+    NSEC3 = 50
+    NSEC3PARAM = 51
+    TLSA = 52
+    SMIMEA = 53
+    HIP = 55
+    NINFO = 56
+    CDS = 59
+    CDNSKEY = 60
+    OPENPGPKEY = 61
+    CSYNC = 62
+    ZONEMD = 63
+    SVCB = 64
+    HTTPS = 65
+    SPF = 99
+    UNSPEC = 103
+    EUI48 = 108
+    EUI64 = 109
+    TKEY = 249
+    TSIG = 250
+    IXFR = 251
+    AXFR = 252
+    MAILB = 253
+    MAILA = 254
+    ANY = 255
+    URI = 256
+    CAA = 257
+    AVC = 258
+    AMTRELAY = 260
+    TA = 32768
+    DLV = 32769
+
+    @classmethod
+    def _maximum(cls):
+        return 65535
+
+    @classmethod
+    def _short_name(cls):
+        return "type"
+
+    @classmethod
+    def _prefix(cls):
+        return "TYPE"
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownRdatatype
+
+_registered_by_text = {}
+_registered_by_value = {}
+
+_metatypes = {RdataType.OPT}
+
+_singletons = {RdataType.SOA, RdataType.NXT, RdataType.DNAME,
+               RdataType.NSEC, RdataType.CNAME}
+
+
+class UnknownRdatatype(dns.exception.DNSException):
+    """DNS resource record type is unknown."""
+
+
+def from_text(text):
+    """Convert text into a DNS rdata type value.
+
+    The input text can be a defined DNS RR type mnemonic or
+    instance of the DNS generic type syntax.
+
+    For example, "NS" and "TYPE2" will both result in a value of 2.
+
+    Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown.
+
+    Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
+
+    Returns an ``int``.
+    """
+
+    text = text.upper().replace('-', '_')
+    try:
+        return RdataType.from_text(text)
+    except UnknownRdatatype:
+        registered_type = _registered_by_text.get(text)
+        if registered_type:
+            return registered_type
+        raise
+
+
+def to_text(value):
+    """Convert a DNS rdata type value to text.
+
+    If the value has a known mnemonic, it will be used, otherwise the
+    DNS generic type syntax will be used.
+
+    Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535.
+
+    Returns a ``str``.
+    """
+
+    text = RdataType.to_text(value)
+    if text.startswith("TYPE"):
+        registered_text = _registered_by_value.get(value)
+        if registered_text:
+            text = registered_text
+    return text.replace('_', '-')
+
+
+def is_metatype(rdtype):
+    """True if the specified type is a metatype.
+
+    *rdtype* is an ``int``.
+
+    The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA,
+    MAILB, ANY, and OPT.
+
+    Returns a ``bool``.
+    """
+
+    return (256 > rdtype >= 128) or rdtype in _metatypes
+
+
+def is_singleton(rdtype):
+    """Is the specified type a singleton type?
+
+    Singleton types can only have a single rdata in an rdataset, or a single
+    RR in an RRset.
+
+    The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and
+    SOA.
+
+    *rdtype* is an ``int``.
+
+    Returns a ``bool``.
+    """
+
+    if rdtype in _singletons:
+        return True
+    return False
+
+# pylint: disable=redefined-outer-name
+def register_type(rdtype, rdtype_text, is_singleton=False):
+    """Dynamically register an rdatatype.
+
+    *rdtype*, an ``int``, the rdatatype to register.
+
+    *rdtype_text*, a ``str``, the textual form of the rdatatype.
+
+    *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e.
+    RRsets of the type can have only one member.)
+    """
+
+    _registered_by_text[rdtype_text] = rdtype
+    _registered_by_value[rdtype] = rdtype_text
+    if is_singleton:
+        _singletons.add(rdtype)
+
+### BEGIN generated RdataType constants
+
+TYPE0 = RdataType.TYPE0
+NONE = RdataType.NONE
+A = RdataType.A
+NS = RdataType.NS
+MD = RdataType.MD
+MF = RdataType.MF
+CNAME = RdataType.CNAME
+SOA = RdataType.SOA
+MB = RdataType.MB
+MG = RdataType.MG
+MR = RdataType.MR
+NULL = RdataType.NULL
+WKS = RdataType.WKS
+PTR = RdataType.PTR
+HINFO = RdataType.HINFO
+MINFO = RdataType.MINFO
+MX = RdataType.MX
+TXT = RdataType.TXT
+RP = RdataType.RP
+AFSDB = RdataType.AFSDB
+X25 = RdataType.X25
+ISDN = RdataType.ISDN
+RT = RdataType.RT
+NSAP = RdataType.NSAP
+NSAP_PTR = RdataType.NSAP_PTR
+SIG = RdataType.SIG
+KEY = RdataType.KEY
+PX = RdataType.PX
+GPOS = RdataType.GPOS
+AAAA = RdataType.AAAA
+LOC = RdataType.LOC
+NXT = RdataType.NXT
+SRV = RdataType.SRV
+NAPTR = RdataType.NAPTR
+KX = RdataType.KX
+CERT = RdataType.CERT
+A6 = RdataType.A6
+DNAME = RdataType.DNAME
+OPT = RdataType.OPT
+APL = RdataType.APL
+DS = RdataType.DS
+SSHFP = RdataType.SSHFP
+IPSECKEY = RdataType.IPSECKEY
+RRSIG = RdataType.RRSIG
+NSEC = RdataType.NSEC
+DNSKEY = RdataType.DNSKEY
+DHCID = RdataType.DHCID
+NSEC3 = RdataType.NSEC3
+NSEC3PARAM = RdataType.NSEC3PARAM
+TLSA = RdataType.TLSA
+SMIMEA = RdataType.SMIMEA
+HIP = RdataType.HIP
+NINFO = RdataType.NINFO
+CDS = RdataType.CDS
+CDNSKEY = RdataType.CDNSKEY
+OPENPGPKEY = RdataType.OPENPGPKEY
+CSYNC = RdataType.CSYNC
+ZONEMD = RdataType.ZONEMD
+SVCB = RdataType.SVCB
+HTTPS = RdataType.HTTPS
+SPF = RdataType.SPF
+UNSPEC = RdataType.UNSPEC
+EUI48 = RdataType.EUI48
+EUI64 = RdataType.EUI64
+TKEY = RdataType.TKEY
+TSIG = RdataType.TSIG
+IXFR = RdataType.IXFR
+AXFR = RdataType.AXFR
+MAILB = RdataType.MAILB
+MAILA = RdataType.MAILA
+ANY = RdataType.ANY
+URI = RdataType.URI
+CAA = RdataType.CAA
+AVC = RdataType.AVC
+AMTRELAY = RdataType.AMTRELAY
+TA = RdataType.TA
+DLV = RdataType.DLV
+
+### END generated RdataType constants

=== added directory 'dns/rdtypes'
=== added directory 'dns/rdtypes/ANY'
=== added file 'dns/rdtypes/ANY/AFSDB.py'
--- old/dns/rdtypes/ANY/AFSDB.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/AFSDB.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,46 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.mxbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+
+    """AFSDB record"""
+
+    # Use the property mechanism to make "subtype" an alias for the
+    # "preference" attribute, and "hostname" an alias for the "exchange"
+    # attribute.
+    #
+    # This lets us inherit the UncompressedMX implementation but lets
+    # the caller use appropriate attribute names for the rdata type.
+    #
+    # We probably lose some performance vs. a cut-and-paste
+    # implementation, but this way we don't copy code, and that's
+    # good.
+
+    @property
+    def subtype(self):
+        "the AFSDB subtype"
+        return self.preference
+
+    @property
+    def hostname(self):
+        "the AFSDB hostname"
+        return self.exchange

=== added file 'dns/rdtypes/ANY/AMTRELAY.py'
--- old/dns/rdtypes/ANY/AMTRELAY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/AMTRELAY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,86 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdtypes.util
+
+
+class Relay(dns.rdtypes.util.Gateway):
+    name = 'AMTRELAY relay'
+
+    @property
+    def relay(self):
+        return self.gateway
+
+
+@dns.immutable.immutable
+class AMTRELAY(dns.rdata.Rdata):
+
+    """AMTRELAY record"""
+
+    # see: RFC 8777
+
+    __slots__ = ['precedence', 'discovery_optional', 'relay_type', 'relay']
+
+    def __init__(self, rdclass, rdtype, precedence, discovery_optional,
+                 relay_type, relay):
+        super().__init__(rdclass, rdtype)
+        relay = Relay(relay_type, relay)
+        self.precedence = self._as_uint8(precedence)
+        self.discovery_optional = self._as_bool(discovery_optional)
+        self.relay_type = relay.type
+        self.relay = relay.relay
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        relay = Relay(self.relay_type, self.relay).to_text(origin, relativize)
+        return '%d %d %d %s' % (self.precedence, self.discovery_optional,
+                                self.relay_type, relay)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        precedence = tok.get_uint8()
+        discovery_optional = tok.get_uint8()
+        if discovery_optional > 1:
+            raise dns.exception.SyntaxError('expecting 0 or 1')
+        discovery_optional = bool(discovery_optional)
+        relay_type = tok.get_uint8()
+        if relay_type > 0x7f:
+            raise dns.exception.SyntaxError('expecting an integer <= 127')
+        relay = Relay.from_text(relay_type, tok, origin, relativize,
+                                relativize_to)
+        return cls(rdclass, rdtype, precedence, discovery_optional, relay_type,
+                   relay.relay)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        relay_type = self.relay_type | (self.discovery_optional << 7)
+        header = struct.pack("!BB", self.precedence, relay_type)
+        file.write(header)
+        Relay(self.relay_type, self.relay).to_wire(file, compress, origin,
+                                                   canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (precedence, relay_type) = parser.get_struct('!BB')
+        discovery_optional = bool(relay_type >> 7)
+        relay_type &= 0x7f
+        relay = Relay.from_wire_parser(relay_type, parser, origin)
+        return cls(rdclass, rdtype, precedence, discovery_optional, relay_type,
+                   relay.relay)

=== added file 'dns/rdtypes/ANY/AVC.py'
--- old/dns/rdtypes/ANY/AVC.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/AVC.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,27 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.txtbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class AVC(dns.rdtypes.txtbase.TXTBase):
+
+    """AVC record"""
+
+    # See: IANA dns parameters for AVC

=== added file 'dns/rdtypes/ANY/CAA.py'
--- old/dns/rdtypes/ANY/CAA.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CAA.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,69 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class CAA(dns.rdata.Rdata):
+
+    """CAA (Certification Authority Authorization) record"""
+
+    # see: RFC 6844
+
+    __slots__ = ['flags', 'tag', 'value']
+
+    def __init__(self, rdclass, rdtype, flags, tag, value):
+        super().__init__(rdclass, rdtype)
+        self.flags = self._as_uint8(flags)
+        self.tag = self._as_bytes(tag, True, 255)
+        if not tag.isalnum():
+            raise ValueError("tag is not alphanumeric")
+        self.value = self._as_bytes(value)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '%u %s "%s"' % (self.flags,
+                               dns.rdata._escapify(self.tag),
+                               dns.rdata._escapify(self.value))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        flags = tok.get_uint8()
+        tag = tok.get_string().encode()
+        value = tok.get_string().encode()
+        return cls(rdclass, rdtype, flags, tag, value)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(struct.pack('!B', self.flags))
+        l = len(self.tag)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.tag)
+        file.write(self.value)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        flags = parser.get_uint8()
+        tag = parser.get_counted_bytes()
+        value = parser.get_remaining()
+        return cls(rdclass, rdtype, flags, tag, value)

=== added file 'dns/rdtypes/ANY/CDNSKEY.py'
--- old/dns/rdtypes/ANY/CDNSKEY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CDNSKEY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,28 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.dnskeybase
+import dns.immutable
+
+# pylint: disable=unused-import
+from dns.rdtypes.dnskeybase import SEP, REVOKE, ZONE  # noqa: F401
+# pylint: enable=unused-import
+
+@dns.immutable.immutable
+class CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
+
+    """CDNSKEY record"""

=== added file 'dns/rdtypes/ANY/CDS.py'
--- old/dns/rdtypes/ANY/CDS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CDS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.dsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class CDS(dns.rdtypes.dsbase.DSBase):
+
+    """CDS record"""

=== added file 'dns/rdtypes/ANY/CERT.py'
--- old/dns/rdtypes/ANY/CERT.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CERT.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,103 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import base64
+
+import dns.exception
+import dns.immutable
+import dns.dnssec
+import dns.rdata
+import dns.tokenizer
+
+_ctype_by_value = {
+    1: 'PKIX',
+    2: 'SPKI',
+    3: 'PGP',
+    253: 'URI',
+    254: 'OID',
+}
+
+_ctype_by_name = {
+    'PKIX': 1,
+    'SPKI': 2,
+    'PGP': 3,
+    'URI': 253,
+    'OID': 254,
+}
+
+
+def _ctype_from_text(what):
+    v = _ctype_by_name.get(what)
+    if v is not None:
+        return v
+    return int(what)
+
+
+def _ctype_to_text(what):
+    v = _ctype_by_value.get(what)
+    if v is not None:
+        return v
+    return str(what)
+
+
+@dns.immutable.immutable
+class CERT(dns.rdata.Rdata):
+
+    """CERT record"""
+
+    # see RFC 2538
+
+    __slots__ = ['certificate_type', 'key_tag', 'algorithm', 'certificate']
+
+    def __init__(self, rdclass, rdtype, certificate_type, key_tag, algorithm,
+                 certificate):
+        super().__init__(rdclass, rdtype)
+        self.certificate_type = self._as_uint16(certificate_type)
+        self.key_tag = self._as_uint16(key_tag)
+        self.algorithm = self._as_uint8(algorithm)
+        self.certificate = self._as_bytes(certificate)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        certificate_type = _ctype_to_text(self.certificate_type)
+        return "%s %d %s %s" % (certificate_type, self.key_tag,
+                                dns.dnssec.algorithm_to_text(self.algorithm),
+                                dns.rdata._base64ify(self.certificate, **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        certificate_type = _ctype_from_text(tok.get_string())
+        key_tag = tok.get_uint16()
+        algorithm = dns.dnssec.algorithm_from_text(tok.get_string())
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        certificate = base64.b64decode(b64)
+        return cls(rdclass, rdtype, certificate_type, key_tag,
+                   algorithm, certificate)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        prefix = struct.pack("!HHB", self.certificate_type, self.key_tag,
+                             self.algorithm)
+        file.write(prefix)
+        file.write(self.certificate)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (certificate_type, key_tag, algorithm) = parser.get_struct("!HHB")
+        certificate = parser.get_remaining()
+        return cls(rdclass, rdtype, certificate_type, key_tag, algorithm,
+                   certificate)

=== added file 'dns/rdtypes/ANY/CNAME.py'
--- old/dns/rdtypes/ANY/CNAME.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CNAME.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,29 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.nsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class CNAME(dns.rdtypes.nsbase.NSBase):
+
+    """CNAME record
+
+    Note: although CNAME is officially a singleton type, dnspython allows
+    non-singleton CNAME rdatasets because such sets have been commonly
+    used by BIND and other nameservers for load balancing."""

=== added file 'dns/rdtypes/ANY/CSYNC.py'
--- old/dns/rdtypes/ANY/CSYNC.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/CSYNC.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,68 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011, 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.name
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+    type_name = 'CSYNC'
+
+
+@dns.immutable.immutable
+class CSYNC(dns.rdata.Rdata):
+
+    """CSYNC record"""
+
+    __slots__ = ['serial', 'flags', 'windows']
+
+    def __init__(self, rdclass, rdtype, serial, flags, windows):
+        super().__init__(rdclass, rdtype)
+        self.serial = self._as_uint32(serial)
+        self.flags = self._as_uint16(flags)
+        if not isinstance(windows, Bitmap):
+            windows = Bitmap(windows)
+        self.windows = tuple(windows.windows)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        text = Bitmap(self.windows).to_text()
+        return '%d %d%s' % (self.serial, self.flags, text)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        serial = tok.get_uint32()
+        flags = tok.get_uint16()
+        bitmap = Bitmap.from_text(tok)
+        return cls(rdclass, rdtype, serial, flags, bitmap)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(struct.pack('!IH', self.serial, self.flags))
+        Bitmap(self.windows).to_wire(file)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (serial, flags) = parser.get_struct("!IH")
+        bitmap = Bitmap.from_wire_parser(parser)
+        return cls(rdclass, rdtype, serial, flags, bitmap)

=== added file 'dns/rdtypes/ANY/DLV.py'
--- old/dns/rdtypes/ANY/DLV.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/DLV.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.dsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class DLV(dns.rdtypes.dsbase.DSBase):
+
+    """DLV record"""

=== added file 'dns/rdtypes/ANY/DNAME.py'
--- old/dns/rdtypes/ANY/DNAME.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/DNAME.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,28 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.nsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class DNAME(dns.rdtypes.nsbase.UncompressedNS):
+
+    """DNAME record"""
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.target.to_wire(file, None, origin, canonicalize)

=== added file 'dns/rdtypes/ANY/DNSKEY.py'
--- old/dns/rdtypes/ANY/DNSKEY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/DNSKEY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,28 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.dnskeybase
+import dns.immutable
+
+# pylint: disable=unused-import
+from dns.rdtypes.dnskeybase import SEP, REVOKE, ZONE  # noqa: F401
+# pylint: enable=unused-import
+
+@dns.immutable.immutable
+class DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase):
+
+    """DNSKEY record"""

=== added file 'dns/rdtypes/ANY/DS.py'
--- old/dns/rdtypes/ANY/DS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/DS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.dsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class DS(dns.rdtypes.dsbase.DSBase):
+
+    """DS record"""

=== added file 'dns/rdtypes/ANY/EUI48.py'
--- old/dns/rdtypes/ANY/EUI48.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/EUI48.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,31 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.euibase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class EUI48(dns.rdtypes.euibase.EUIBase):
+
+    """EUI48 record"""
+
+    # see: rfc7043.txt
+
+    byte_len = 6  # 0123456789ab (in hex)
+    text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab

=== added file 'dns/rdtypes/ANY/EUI64.py'
--- old/dns/rdtypes/ANY/EUI64.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/EUI64.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,31 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.euibase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class EUI64(dns.rdtypes.euibase.EUIBase):
+
+    """EUI64 record"""
+
+    # see: rfc7043.txt
+
+    byte_len = 8  # 0123456789abcdef (in hex)
+    text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab-cd-ef

=== added file 'dns/rdtypes/ANY/GPOS.py'
--- old/dns/rdtypes/ANY/GPOS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/GPOS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,128 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+def _validate_float_string(what):
+    if len(what) == 0:
+        raise dns.exception.FormError
+    if what[0] == b'-'[0] or what[0] == b'+'[0]:
+        what = what[1:]
+    if what.isdigit():
+        return
+    try:
+        (left, right) = what.split(b'.')
+    except ValueError:
+        raise dns.exception.FormError
+    if left == b'' and right == b'':
+        raise dns.exception.FormError
+    if not left == b'' and not left.decode().isdigit():
+        raise dns.exception.FormError
+    if not right == b'' and not right.decode().isdigit():
+        raise dns.exception.FormError
+
+
+@dns.immutable.immutable
+class GPOS(dns.rdata.Rdata):
+
+    """GPOS record"""
+
+    # see: RFC 1712
+
+    __slots__ = ['latitude', 'longitude', 'altitude']
+
+    def __init__(self, rdclass, rdtype, latitude, longitude, altitude):
+        super().__init__(rdclass, rdtype)
+        if isinstance(latitude, float) or \
+           isinstance(latitude, int):
+            latitude = str(latitude)
+        if isinstance(longitude, float) or \
+           isinstance(longitude, int):
+            longitude = str(longitude)
+        if isinstance(altitude, float) or \
+           isinstance(altitude, int):
+            altitude = str(altitude)
+        latitude = self._as_bytes(latitude, True, 255)
+        longitude = self._as_bytes(longitude, True, 255)
+        altitude = self._as_bytes(altitude, True, 255)
+        _validate_float_string(latitude)
+        _validate_float_string(longitude)
+        _validate_float_string(altitude)
+        self.latitude = latitude
+        self.longitude = longitude
+        self.altitude = altitude
+        flat = self.float_latitude
+        if flat < -90.0 or flat > 90.0:
+            raise dns.exception.FormError('bad latitude')
+        flong = self.float_longitude
+        if flong < -180.0 or flong > 180.0:
+            raise dns.exception.FormError('bad longitude')
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '{} {} {}'.format(self.latitude.decode(),
+                                 self.longitude.decode(),
+                                 self.altitude.decode())
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        latitude = tok.get_string()
+        longitude = tok.get_string()
+        altitude = tok.get_string()
+        return cls(rdclass, rdtype, latitude, longitude, altitude)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.latitude)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.latitude)
+        l = len(self.longitude)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.longitude)
+        l = len(self.altitude)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.altitude)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        latitude = parser.get_counted_bytes()
+        longitude = parser.get_counted_bytes()
+        altitude = parser.get_counted_bytes()
+        return cls(rdclass, rdtype, latitude, longitude, altitude)
+
+    @property
+    def float_latitude(self):
+        "latitude as a floating point value"
+        return float(self.latitude)
+
+    @property
+    def float_longitude(self):
+        "longitude as a floating point value"
+        return float(self.longitude)
+
+    @property
+    def float_altitude(self):
+        "altitude as a floating point value"
+        return float(self.altitude)

=== added file 'dns/rdtypes/ANY/HINFO.py'
--- old/dns/rdtypes/ANY/HINFO.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/HINFO.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,65 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class HINFO(dns.rdata.Rdata):
+
+    """HINFO record"""
+
+    # see: RFC 1035
+
+    __slots__ = ['cpu', 'os']
+
+    def __init__(self, rdclass, rdtype, cpu, os):
+        super().__init__(rdclass, rdtype)
+        self.cpu = self._as_bytes(cpu, True, 255)
+        self.os = self._as_bytes(os, True, 255)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '"{}" "{}"'.format(dns.rdata._escapify(self.cpu),
+                                  dns.rdata._escapify(self.os))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        cpu = tok.get_string(max_length=255)
+        os = tok.get_string(max_length=255)
+        return cls(rdclass, rdtype, cpu, os)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.cpu)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.cpu)
+        l = len(self.os)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.os)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        cpu = parser.get_counted_bytes()
+        os = parser.get_counted_bytes()
+        return cls(rdclass, rdtype, cpu, os)

=== added file 'dns/rdtypes/ANY/HIP.py'
--- old/dns/rdtypes/ANY/HIP.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/HIP.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,85 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2010, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import base64
+import binascii
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class HIP(dns.rdata.Rdata):
+
+    """HIP record"""
+
+    # see: RFC 5205
+
+    __slots__ = ['hit', 'algorithm', 'key', 'servers']
+
+    def __init__(self, rdclass, rdtype, hit, algorithm, key, servers):
+        super().__init__(rdclass, rdtype)
+        self.hit = self._as_bytes(hit, True, 255)
+        self.algorithm = self._as_uint8(algorithm)
+        self.key = self._as_bytes(key, True)
+        self.servers = self._as_tuple(servers, self._as_name)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        hit = binascii.hexlify(self.hit).decode()
+        key = base64.b64encode(self.key).replace(b'\n', b'').decode()
+        text = ''
+        servers = []
+        for server in self.servers:
+            servers.append(server.choose_relativity(origin, relativize))
+        if len(servers) > 0:
+            text += (' ' + ' '.join((x.to_unicode() for x in servers)))
+        return '%u %s %s%s' % (self.algorithm, hit, key, text)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_uint8()
+        hit = binascii.unhexlify(tok.get_string().encode())
+        key = base64.b64decode(tok.get_string().encode())
+        servers = []
+        for token in tok.get_remaining():
+            server = tok.as_name(token, origin, relativize, relativize_to)
+            servers.append(server)
+        return cls(rdclass, rdtype, hit, algorithm, key, servers)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        lh = len(self.hit)
+        lk = len(self.key)
+        file.write(struct.pack("!BBH", lh, self.algorithm, lk))
+        file.write(self.hit)
+        file.write(self.key)
+        for server in self.servers:
+            server.to_wire(file, None, origin, False)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (lh, algorithm, lk) = parser.get_struct('!BBH')
+        hit = parser.get_bytes(lh)
+        key = parser.get_bytes(lk)
+        servers = []
+        while parser.remaining() > 0:
+            server = parser.get_name(origin)
+            servers.append(server)
+        return cls(rdclass, rdtype, hit, algorithm, key, servers)

=== added file 'dns/rdtypes/ANY/ISDN.py'
--- old/dns/rdtypes/ANY/ISDN.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/ISDN.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,76 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class ISDN(dns.rdata.Rdata):
+
+    """ISDN record"""
+
+    # see: RFC 1183
+
+    __slots__ = ['address', 'subaddress']
+
+    def __init__(self, rdclass, rdtype, address, subaddress):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_bytes(address, True, 255)
+        self.subaddress = self._as_bytes(subaddress, True, 255)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        if self.subaddress:
+            return '"{}" "{}"'.format(dns.rdata._escapify(self.address),
+                                      dns.rdata._escapify(self.subaddress))
+        else:
+            return '"%s"' % dns.rdata._escapify(self.address)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_string()
+        tokens = tok.get_remaining(max_tokens=1)
+        if len(tokens) >= 1:
+            subaddress = tokens[0].unescape().value
+        else:
+            subaddress = ''
+        return cls(rdclass, rdtype, address, subaddress)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.address)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.address)
+        l = len(self.subaddress)
+        if l > 0:
+            assert l < 256
+            file.write(struct.pack('!B', l))
+            file.write(self.subaddress)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_counted_bytes()
+        if parser.remaining() > 0:
+            subaddress = parser.get_counted_bytes()
+        else:
+            subaddress = b''
+        return cls(rdclass, rdtype, address, subaddress)

=== added file 'dns/rdtypes/ANY/LOC.py'
--- old/dns/rdtypes/ANY/LOC.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/LOC.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,326 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+
+_pows = tuple(10**i for i in range(0, 11))
+
+# default values are in centimeters
+_default_size = 100.0
+_default_hprec = 1000000.0
+_default_vprec = 1000.0
+
+# for use by from_wire()
+_MAX_LATITUDE = 0x80000000 + 90 * 3600000
+_MIN_LATITUDE = 0x80000000 - 90 * 3600000
+_MAX_LONGITUDE = 0x80000000 + 180 * 3600000
+_MIN_LONGITUDE = 0x80000000 - 180 * 3600000
+
+
+def _exponent_of(what, desc):
+    if what == 0:
+        return 0
+    exp = None
+    for (i, pow) in enumerate(_pows):
+        if what < pow:
+            exp = i - 1
+            break
+    if exp is None or exp < 0:
+        raise dns.exception.SyntaxError("%s value out of bounds" % desc)
+    return exp
+
+
+def _float_to_tuple(what):
+    if what < 0:
+        sign = -1
+        what *= -1
+    else:
+        sign = 1
+    what = round(what * 3600000)  # pylint: disable=round-builtin
+    degrees = int(what // 3600000)
+    what -= degrees * 3600000
+    minutes = int(what // 60000)
+    what -= minutes * 60000
+    seconds = int(what // 1000)
+    what -= int(seconds * 1000)
+    what = int(what)
+    return (degrees, minutes, seconds, what, sign)
+
+
+def _tuple_to_float(what):
+    value = float(what[0])
+    value += float(what[1]) / 60.0
+    value += float(what[2]) / 3600.0
+    value += float(what[3]) / 3600000.0
+    return float(what[4]) * value
+
+
+def _encode_size(what, desc):
+    what = int(what)
+    exponent = _exponent_of(what, desc) & 0xF
+    base = what // pow(10, exponent) & 0xF
+    return base * 16 + exponent
+
+
+def _decode_size(what, desc):
+    exponent = what & 0x0F
+    if exponent > 9:
+        raise dns.exception.FormError("bad %s exponent" % desc)
+    base = (what & 0xF0) >> 4
+    if base > 9:
+        raise dns.exception.FormError("bad %s base" % desc)
+    return base * pow(10, exponent)
+
+
+def _check_coordinate_list(value, low, high):
+    if value[0] < low or value[0] > high:
+        raise ValueError(f'not in range [{low}, {high}]')
+    if value[1] < 0 or value[1] > 59:
+        raise ValueError('bad minutes value')
+    if value[2] < 0 or value[2] > 59:
+        raise ValueError('bad seconds value')
+    if value[3] < 0 or value[3] > 999:
+        raise ValueError('bad milliseconds value')
+    if value[4] != 1 and value[4] != -1:
+        raise ValueError('bad hemisphere value')
+
+
+@dns.immutable.immutable
+class LOC(dns.rdata.Rdata):
+
+    """LOC record"""
+
+    # see: RFC 1876
+
+    __slots__ = ['latitude', 'longitude', 'altitude', 'size',
+                 'horizontal_precision', 'vertical_precision']
+
+    def __init__(self, rdclass, rdtype, latitude, longitude, altitude,
+                 size=_default_size, hprec=_default_hprec,
+                 vprec=_default_vprec):
+        """Initialize a LOC record instance.
+
+        The parameters I{latitude} and I{longitude} may be either a 4-tuple
+        of integers specifying (degrees, minutes, seconds, milliseconds),
+        or they may be floating point values specifying the number of
+        degrees. The other parameters are floats. Size, horizontal precision,
+        and vertical precision are specified in centimeters."""
+
+        super().__init__(rdclass, rdtype)
+        if isinstance(latitude, int):
+            latitude = float(latitude)
+        if isinstance(latitude, float):
+            latitude = _float_to_tuple(latitude)
+        _check_coordinate_list(latitude, -90, 90)
+        self.latitude = tuple(latitude)
+        if isinstance(longitude, int):
+            longitude = float(longitude)
+        if isinstance(longitude, float):
+            longitude = _float_to_tuple(longitude)
+        _check_coordinate_list(longitude, -180, 180)
+        self.longitude = tuple(longitude)
+        self.altitude = float(altitude)
+        self.size = float(size)
+        self.horizontal_precision = float(hprec)
+        self.vertical_precision = float(vprec)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        if self.latitude[4] > 0:
+            lat_hemisphere = 'N'
+        else:
+            lat_hemisphere = 'S'
+        if self.longitude[4] > 0:
+            long_hemisphere = 'E'
+        else:
+            long_hemisphere = 'W'
+        text = "%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm" % (
+            self.latitude[0], self.latitude[1],
+            self.latitude[2], self.latitude[3], lat_hemisphere,
+            self.longitude[0], self.longitude[1], self.longitude[2],
+            self.longitude[3], long_hemisphere,
+            self.altitude / 100.0
+        )
+
+        # do not print default values
+        if self.size != _default_size or \
+            self.horizontal_precision != _default_hprec or \
+                self.vertical_precision != _default_vprec:
+            text += " {:0.2f}m {:0.2f}m {:0.2f}m".format(
+                self.size / 100.0, self.horizontal_precision / 100.0,
+                self.vertical_precision / 100.0
+            )
+        return text
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        latitude = [0, 0, 0, 0, 1]
+        longitude = [0, 0, 0, 0, 1]
+        size = _default_size
+        hprec = _default_hprec
+        vprec = _default_vprec
+
+        latitude[0] = tok.get_int()
+        t = tok.get_string()
+        if t.isdigit():
+            latitude[1] = int(t)
+            t = tok.get_string()
+            if '.' in t:
+                (seconds, milliseconds) = t.split('.')
+                if not seconds.isdigit():
+                    raise dns.exception.SyntaxError(
+                        'bad latitude seconds value')
+                latitude[2] = int(seconds)
+                l = len(milliseconds)
+                if l == 0 or l > 3 or not milliseconds.isdigit():
+                    raise dns.exception.SyntaxError(
+                        'bad latitude milliseconds value')
+                if l == 1:
+                    m = 100
+                elif l == 2:
+                    m = 10
+                else:
+                    m = 1
+                latitude[3] = m * int(milliseconds)
+                t = tok.get_string()
+            elif t.isdigit():
+                latitude[2] = int(t)
+                t = tok.get_string()
+        if t == 'S':
+            latitude[4] = -1
+        elif t != 'N':
+            raise dns.exception.SyntaxError('bad latitude hemisphere value')
+
+        longitude[0] = tok.get_int()
+        t = tok.get_string()
+        if t.isdigit():
+            longitude[1] = int(t)
+            t = tok.get_string()
+            if '.' in t:
+                (seconds, milliseconds) = t.split('.')
+                if not seconds.isdigit():
+                    raise dns.exception.SyntaxError(
+                        'bad longitude seconds value')
+                longitude[2] = int(seconds)
+                l = len(milliseconds)
+                if l == 0 or l > 3 or not milliseconds.isdigit():
+                    raise dns.exception.SyntaxError(
+                        'bad longitude milliseconds value')
+                if l == 1:
+                    m = 100
+                elif l == 2:
+                    m = 10
+                else:
+                    m = 1
+                longitude[3] = m * int(milliseconds)
+                t = tok.get_string()
+            elif t.isdigit():
+                longitude[2] = int(t)
+                t = tok.get_string()
+        if t == 'W':
+            longitude[4] = -1
+        elif t != 'E':
+            raise dns.exception.SyntaxError('bad longitude hemisphere value')
+
+        t = tok.get_string()
+        if t[-1] == 'm':
+            t = t[0: -1]
+        altitude = float(t) * 100.0        # m -> cm
+
+        tokens = tok.get_remaining(max_tokens=3)
+        if len(tokens) >= 1:
+            value = tokens[0].unescape().value
+            if value[-1] == 'm':
+                value = value[0: -1]
+            size = float(value) * 100.0        # m -> cm
+            if len(tokens) >= 2:
+                value = tokens[1].unescape().value
+                if value[-1] == 'm':
+                    value = value[0: -1]
+                hprec = float(value) * 100.0        # m -> cm
+                if len(tokens) >= 3:
+                    value = tokens[2].unescape().value
+                    if value[-1] == 'm':
+                        value = value[0: -1]
+                    vprec = float(value) * 100.0        # m -> cm
+
+        # Try encoding these now so we raise if they are bad
+        _encode_size(size, "size")
+        _encode_size(hprec, "horizontal precision")
+        _encode_size(vprec, "vertical precision")
+
+        return cls(rdclass, rdtype, latitude, longitude, altitude,
+                   size, hprec, vprec)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        milliseconds = (self.latitude[0] * 3600000 +
+                        self.latitude[1] * 60000 +
+                        self.latitude[2] * 1000 +
+                        self.latitude[3]) * self.latitude[4]
+        latitude = 0x80000000 + milliseconds
+        milliseconds = (self.longitude[0] * 3600000 +
+                        self.longitude[1] * 60000 +
+                        self.longitude[2] * 1000 +
+                        self.longitude[3]) * self.longitude[4]
+        longitude = 0x80000000 + milliseconds
+        altitude = int(self.altitude) + 10000000
+        size = _encode_size(self.size, "size")
+        hprec = _encode_size(self.horizontal_precision, "horizontal precision")
+        vprec = _encode_size(self.vertical_precision, "vertical precision")
+        wire = struct.pack("!BBBBIII", 0, size, hprec, vprec, latitude,
+                           longitude, altitude)
+        file.write(wire)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (version, size, hprec, vprec, latitude, longitude, altitude) = \
+            parser.get_struct("!BBBBIII")
+        if version != 0:
+            raise dns.exception.FormError("LOC version not zero")
+        if latitude < _MIN_LATITUDE or latitude > _MAX_LATITUDE:
+            raise dns.exception.FormError("bad latitude")
+        if latitude > 0x80000000:
+            latitude = (latitude - 0x80000000) / 3600000
+        else:
+            latitude = -1 * (0x80000000 - latitude) / 3600000
+        if longitude < _MIN_LONGITUDE or longitude > _MAX_LONGITUDE:
+            raise dns.exception.FormError("bad longitude")
+        if longitude > 0x80000000:
+            longitude = (longitude - 0x80000000) / 3600000
+        else:
+            longitude = -1 * (0x80000000 - longitude) / 3600000
+        altitude = float(altitude) - 10000000.0
+        size = _decode_size(size, "size")
+        hprec = _decode_size(hprec, "horizontal precision")
+        vprec = _decode_size(vprec, "vertical precision")
+        return cls(rdclass, rdtype, latitude, longitude, altitude,
+                   size, hprec, vprec)
+
+    @property
+    def float_latitude(self):
+        "latitude as a floating point value"
+        return _tuple_to_float(self.latitude)
+
+    @property
+    def float_longitude(self):
+        "longitude as a floating point value"
+        return _tuple_to_float(self.longitude)

=== added file 'dns/rdtypes/ANY/MX.py'
--- old/dns/rdtypes/ANY/MX.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/MX.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.mxbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class MX(dns.rdtypes.mxbase.MXBase):
+
+    """MX record"""

=== added file 'dns/rdtypes/ANY/NINFO.py'
--- old/dns/rdtypes/ANY/NINFO.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/NINFO.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,27 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.txtbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class NINFO(dns.rdtypes.txtbase.TXTBase):
+
+    """NINFO record"""
+
+    # see: draft-reid-dnsext-zs-01

=== added file 'dns/rdtypes/ANY/NS.py'
--- old/dns/rdtypes/ANY/NS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/NS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.nsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class NS(dns.rdtypes.nsbase.NSBase):
+
+    """NS record"""

=== added file 'dns/rdtypes/ANY/NSEC.py'
--- old/dns/rdtypes/ANY/NSEC.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/NSEC.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,67 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.name
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+    type_name = 'NSEC'
+
+
+@dns.immutable.immutable
+class NSEC(dns.rdata.Rdata):
+
+    """NSEC record"""
+
+    __slots__ = ['next', 'windows']
+
+    def __init__(self, rdclass, rdtype, next, windows):
+        super().__init__(rdclass, rdtype)
+        self.next = self._as_name(next)
+        if not isinstance(windows, Bitmap):
+            windows = Bitmap(windows)
+        self.windows = tuple(windows.windows)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        next = self.next.choose_relativity(origin, relativize)
+        text = Bitmap(self.windows).to_text()
+        return '{}{}'.format(next, text)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        next = tok.get_name(origin, relativize, relativize_to)
+        windows = Bitmap.from_text(tok)
+        return cls(rdclass, rdtype, next, windows)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        # Note that NSEC downcasing, originally mandated by RFC 4034
+        # section 6.2 was removed by RFC 6840 section 5.1.
+        self.next.to_wire(file, None, origin, False)
+        Bitmap(self.windows).to_wire(file)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        next = parser.get_name(origin)
+        bitmap = Bitmap.from_wire_parser(parser)
+        return cls(rdclass, rdtype, next, bitmap)

=== added file 'dns/rdtypes/ANY/NSEC3.py'
--- old/dns/rdtypes/ANY/NSEC3.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/NSEC3.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,111 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import binascii
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+
+b32_hex_to_normal = bytes.maketrans(b'0123456789ABCDEFGHIJKLMNOPQRSTUV',
+                                    b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
+b32_normal_to_hex = bytes.maketrans(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
+                                    b'0123456789ABCDEFGHIJKLMNOPQRSTUV')
+
+# hash algorithm constants
+SHA1 = 1
+
+# flag constants
+OPTOUT = 1
+
+
+@dns.immutable.immutable
+class Bitmap(dns.rdtypes.util.Bitmap):
+    type_name = 'NSEC3'
+
+
+@dns.immutable.immutable
+class NSEC3(dns.rdata.Rdata):
+
+    """NSEC3 record"""
+
+    __slots__ = ['algorithm', 'flags', 'iterations', 'salt', 'next', 'windows']
+
+    def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt,
+                 next, windows):
+        super().__init__(rdclass, rdtype)
+        self.algorithm = self._as_uint8(algorithm)
+        self.flags = self._as_uint8(flags)
+        self.iterations = self._as_uint16(iterations)
+        self.salt = self._as_bytes(salt, True, 255)
+        self.next = self._as_bytes(next, True, 255)
+        if not isinstance(windows, Bitmap):
+            windows = Bitmap(windows)
+        self.windows = tuple(windows.windows)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        next = base64.b32encode(self.next).translate(
+            b32_normal_to_hex).lower().decode()
+        if self.salt == b'':
+            salt = '-'
+        else:
+            salt = binascii.hexlify(self.salt).decode()
+        text = Bitmap(self.windows).to_text()
+        return '%u %u %u %s %s%s' % (self.algorithm, self.flags,
+                                     self.iterations, salt, next, text)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_uint8()
+        flags = tok.get_uint8()
+        iterations = tok.get_uint16()
+        salt = tok.get_string()
+        if salt == '-':
+            salt = b''
+        else:
+            salt = binascii.unhexlify(salt.encode('ascii'))
+        next = tok.get_string().encode(
+            'ascii').upper().translate(b32_hex_to_normal)
+        next = base64.b32decode(next)
+        bitmap = Bitmap.from_text(tok)
+        return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,
+                   bitmap)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.salt)
+        file.write(struct.pack("!BBHB", self.algorithm, self.flags,
+                               self.iterations, l))
+        file.write(self.salt)
+        l = len(self.next)
+        file.write(struct.pack("!B", l))
+        file.write(self.next)
+        Bitmap(self.windows).to_wire(file)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (algorithm, flags, iterations) = parser.get_struct('!BBH')
+        salt = parser.get_counted_bytes()
+        next = parser.get_counted_bytes()
+        bitmap = Bitmap.from_wire_parser(parser)
+        return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next,
+                   bitmap)

=== added file 'dns/rdtypes/ANY/NSEC3PARAM.py'
--- old/dns/rdtypes/ANY/NSEC3PARAM.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/NSEC3PARAM.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,71 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import binascii
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+
+
+@dns.immutable.immutable
+class NSEC3PARAM(dns.rdata.Rdata):
+
+    """NSEC3PARAM record"""
+
+    __slots__ = ['algorithm', 'flags', 'iterations', 'salt']
+
+    def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt):
+        super().__init__(rdclass, rdtype)
+        self.algorithm = self._as_uint8(algorithm)
+        self.flags = self._as_uint8(flags)
+        self.iterations = self._as_uint16(iterations)
+        self.salt = self._as_bytes(salt, True, 255)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        if self.salt == b'':
+            salt = '-'
+        else:
+            salt = binascii.hexlify(self.salt).decode()
+        return '%u %u %u %s' % (self.algorithm, self.flags, self.iterations,
+                                salt)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_uint8()
+        flags = tok.get_uint8()
+        iterations = tok.get_uint16()
+        salt = tok.get_string()
+        if salt == '-':
+            salt = ''
+        else:
+            salt = binascii.unhexlify(salt.encode())
+        return cls(rdclass, rdtype, algorithm, flags, iterations, salt)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.salt)
+        file.write(struct.pack("!BBHB", self.algorithm, self.flags,
+                               self.iterations, l))
+        file.write(self.salt)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (algorithm, flags, iterations) = parser.get_struct('!BBH')
+        salt = parser.get_counted_bytes()
+        return cls(rdclass, rdtype, algorithm, flags, iterations, salt)

=== added file 'dns/rdtypes/ANY/OPENPGPKEY.py'
--- old/dns/rdtypes/ANY/OPENPGPKEY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/OPENPGPKEY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,52 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2016 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+@dns.immutable.immutable
+class OPENPGPKEY(dns.rdata.Rdata):
+
+    """OPENPGPKEY record"""
+
+    # see: RFC 7929
+
+    def __init__(self, rdclass, rdtype, key):
+        super().__init__(rdclass, rdtype)
+        self.key = self._as_bytes(key)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return dns.rdata._base64ify(self.key, chunksize=None, **kw)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        key = base64.b64decode(b64)
+        return cls(rdclass, rdtype, key)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(self.key)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        key = parser.get_remaining()
+        return cls(rdclass, rdtype, key)

=== added file 'dns/rdtypes/ANY/OPT.py'
--- old/dns/rdtypes/ANY/OPT.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/OPT.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,76 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.edns
+import dns.immutable
+import dns.exception
+import dns.rdata
+
+
+# We don't implement from_text, and that's ok.
+# pylint: disable=abstract-method
+
+@dns.immutable.immutable
+class OPT(dns.rdata.Rdata):
+
+    """OPT record"""
+
+    __slots__ = ['options']
+
+    def __init__(self, rdclass, rdtype, options):
+        """Initialize an OPT rdata.
+
+        *rdclass*, an ``int`` is the rdataclass of the Rdata,
+        which is also the payload size.
+
+        *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+        *options*, a tuple of ``bytes``
+        """
+
+        super().__init__(rdclass, rdtype)
+        def as_option(option):
+            if not isinstance(option, dns.edns.Option):
+                raise ValueError('option is not a dns.edns.option')
+            return option
+        self.options = self._as_tuple(options, as_option)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        for opt in self.options:
+            owire = opt.to_wire()
+            file.write(struct.pack("!HH", opt.otype, len(owire)))
+            file.write(owire)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return ' '.join(opt.to_text() for opt in self.options)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        options = []
+        while parser.remaining() > 0:
+            (otype, olen) = parser.get_struct('!HH')
+            with parser.restrict_to(olen):
+                opt = dns.edns.option_from_wire_parser(otype, parser)
+            options.append(opt)
+        return cls(rdclass, rdtype, options)
+
+    @property
+    def payload(self):
+        "payload size"
+        return self.rdclass

=== added file 'dns/rdtypes/ANY/PTR.py'
--- old/dns/rdtypes/ANY/PTR.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/PTR.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.nsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class PTR(dns.rdtypes.nsbase.NSBase):
+
+    """PTR record"""

=== added file 'dns/rdtypes/ANY/RP.py'
--- old/dns/rdtypes/ANY/RP.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/RP.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,58 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.name
+
+
+@dns.immutable.immutable
+class RP(dns.rdata.Rdata):
+
+    """RP record"""
+
+    # see: RFC 1183
+
+    __slots__ = ['mbox', 'txt']
+
+    def __init__(self, rdclass, rdtype, mbox, txt):
+        super().__init__(rdclass, rdtype)
+        self.mbox = self._as_name(mbox)
+        self.txt = self._as_name(txt)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        mbox = self.mbox.choose_relativity(origin, relativize)
+        txt = self.txt.choose_relativity(origin, relativize)
+        return "{} {}".format(str(mbox), str(txt))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        mbox = tok.get_name(origin, relativize, relativize_to)
+        txt = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, mbox, txt)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.mbox.to_wire(file, None, origin, canonicalize)
+        self.txt.to_wire(file, None, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        mbox = parser.get_name(origin)
+        txt = parser.get_name(origin)
+        return cls(rdclass, rdtype, mbox, txt)

=== added file 'dns/rdtypes/ANY/RRSIG.py'
--- old/dns/rdtypes/ANY/RRSIG.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/RRSIG.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,124 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import calendar
+import struct
+import time
+
+import dns.dnssec
+import dns.immutable
+import dns.exception
+import dns.rdata
+import dns.rdatatype
+
+
+class BadSigTime(dns.exception.DNSException):
+
+    """Time in DNS SIG or RRSIG resource record cannot be parsed."""
+
+
+def sigtime_to_posixtime(what):
+    if len(what) <= 10 and what.isdigit():
+        return int(what)
+    if len(what) != 14:
+        raise BadSigTime
+    year = int(what[0:4])
+    month = int(what[4:6])
+    day = int(what[6:8])
+    hour = int(what[8:10])
+    minute = int(what[10:12])
+    second = int(what[12:14])
+    return calendar.timegm((year, month, day, hour, minute, second,
+                            0, 0, 0))
+
+
+def posixtime_to_sigtime(what):
+    return time.strftime('%Y%m%d%H%M%S', time.gmtime(what))
+
+
+@dns.immutable.immutable
+class RRSIG(dns.rdata.Rdata):
+
+    """RRSIG record"""
+
+    __slots__ = ['type_covered', 'algorithm', 'labels', 'original_ttl',
+                 'expiration', 'inception', 'key_tag', 'signer',
+                 'signature']
+
+    def __init__(self, rdclass, rdtype, type_covered, algorithm, labels,
+                 original_ttl, expiration, inception, key_tag, signer,
+                 signature):
+        super().__init__(rdclass, rdtype)
+        self.type_covered = self._as_rdatatype(type_covered)
+        self.algorithm = dns.dnssec.Algorithm.make(algorithm)
+        self.labels = self._as_uint8(labels)
+        self.original_ttl = self._as_ttl(original_ttl)
+        self.expiration = self._as_uint32(expiration)
+        self.inception = self._as_uint32(inception)
+        self.key_tag = self._as_uint16(key_tag)
+        self.signer = self._as_name(signer)
+        self.signature = self._as_bytes(signature)
+
+    def covers(self):
+        return self.type_covered
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '%s %d %d %d %s %s %d %s %s' % (
+            dns.rdatatype.to_text(self.type_covered),
+            self.algorithm,
+            self.labels,
+            self.original_ttl,
+            posixtime_to_sigtime(self.expiration),
+            posixtime_to_sigtime(self.inception),
+            self.key_tag,
+            self.signer.choose_relativity(origin, relativize),
+            dns.rdata._base64ify(self.signature, **kw)
+        )
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        type_covered = dns.rdatatype.from_text(tok.get_string())
+        algorithm = dns.dnssec.algorithm_from_text(tok.get_string())
+        labels = tok.get_int()
+        original_ttl = tok.get_ttl()
+        expiration = sigtime_to_posixtime(tok.get_string())
+        inception = sigtime_to_posixtime(tok.get_string())
+        key_tag = tok.get_int()
+        signer = tok.get_name(origin, relativize, relativize_to)
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        signature = base64.b64decode(b64)
+        return cls(rdclass, rdtype, type_covered, algorithm, labels,
+                   original_ttl, expiration, inception, key_tag, signer,
+                   signature)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack('!HBBIIIH', self.type_covered,
+                             self.algorithm, self.labels,
+                             self.original_ttl, self.expiration,
+                             self.inception, self.key_tag)
+        file.write(header)
+        self.signer.to_wire(file, None, origin, canonicalize)
+        file.write(self.signature)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct('!HBBIIIH')
+        signer = parser.get_name(origin)
+        signature = parser.get_remaining()
+        return cls(rdclass, rdtype, *header, signer, signature)

=== added file 'dns/rdtypes/ANY/RT.py'
--- old/dns/rdtypes/ANY/RT.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/RT.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.mxbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class RT(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+
+    """RT record"""

=== added file 'dns/rdtypes/ANY/SMIMEA.py'
--- old/dns/rdtypes/ANY/SMIMEA.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/SMIMEA.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,9 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.tlsabase
+
+
+@dns.immutable.immutable
+class SMIMEA(dns.rdtypes.tlsabase.TLSABase):
+    """SMIMEA record"""

=== added file 'dns/rdtypes/ANY/SOA.py'
--- old/dns/rdtypes/ANY/SOA.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/SOA.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,78 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.name
+
+
+@dns.immutable.immutable
+class SOA(dns.rdata.Rdata):
+
+    """SOA record"""
+
+    # see: RFC 1035
+
+    __slots__ = ['mname', 'rname', 'serial', 'refresh', 'retry', 'expire',
+                 'minimum']
+
+    def __init__(self, rdclass, rdtype, mname, rname, serial, refresh, retry,
+                 expire, minimum):
+        super().__init__(rdclass, rdtype)
+        self.mname = self._as_name(mname)
+        self.rname = self._as_name(rname)
+        self.serial = self._as_uint32(serial)
+        self.refresh = self._as_ttl(refresh)
+        self.retry = self._as_ttl(retry)
+        self.expire = self._as_ttl(expire)
+        self.minimum = self._as_ttl(minimum)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        mname = self.mname.choose_relativity(origin, relativize)
+        rname = self.rname.choose_relativity(origin, relativize)
+        return '%s %s %d %d %d %d %d' % (
+            mname, rname, self.serial, self.refresh, self.retry,
+            self.expire, self.minimum)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        mname = tok.get_name(origin, relativize, relativize_to)
+        rname = tok.get_name(origin, relativize, relativize_to)
+        serial = tok.get_uint32()
+        refresh = tok.get_ttl()
+        retry = tok.get_ttl()
+        expire = tok.get_ttl()
+        minimum = tok.get_ttl()
+        return cls(rdclass, rdtype, mname, rname, serial, refresh, retry,
+                   expire, minimum)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.mname.to_wire(file, compress, origin, canonicalize)
+        self.rname.to_wire(file, compress, origin, canonicalize)
+        five_ints = struct.pack('!IIIII', self.serial, self.refresh,
+                                self.retry, self.expire, self.minimum)
+        file.write(five_ints)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        mname = parser.get_name(origin)
+        rname = parser.get_name(origin)
+        return cls(rdclass, rdtype, mname, rname, *parser.get_struct('!IIIII'))

=== added file 'dns/rdtypes/ANY/SPF.py'
--- old/dns/rdtypes/ANY/SPF.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/SPF.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,27 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.txtbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class SPF(dns.rdtypes.txtbase.TXTBase):
+
+    """SPF record"""
+
+    # see: RFC 4408

=== added file 'dns/rdtypes/ANY/SSHFP.py'
--- old/dns/rdtypes/ANY/SSHFP.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/SSHFP.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,69 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import binascii
+
+import dns.rdata
+import dns.immutable
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class SSHFP(dns.rdata.Rdata):
+
+    """SSHFP record"""
+
+    # See RFC 4255
+
+    __slots__ = ['algorithm', 'fp_type', 'fingerprint']
+
+    def __init__(self, rdclass, rdtype, algorithm, fp_type,
+                 fingerprint):
+        super().__init__(rdclass, rdtype)
+        self.algorithm = self._as_uint8(algorithm)
+        self.fp_type = self._as_uint8(fp_type)
+        self.fingerprint = self._as_bytes(fingerprint, True)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        kw = kw.copy()
+        chunksize = kw.pop('chunksize', 128)
+        return '%d %d %s' % (self.algorithm,
+                             self.fp_type,
+                             dns.rdata._hexify(self.fingerprint,
+                                               chunksize=chunksize,
+                                               **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_uint8()
+        fp_type = tok.get_uint8()
+        fingerprint = tok.concatenate_remaining_identifiers().encode()
+        fingerprint = binascii.unhexlify(fingerprint)
+        return cls(rdclass, rdtype, algorithm, fp_type, fingerprint)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!BB", self.algorithm, self.fp_type)
+        file.write(header)
+        file.write(self.fingerprint)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct("BB")
+        fingerprint = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], fingerprint)

=== added file 'dns/rdtypes/ANY/TKEY.py'
--- old/dns/rdtypes/ANY/TKEY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/TKEY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,118 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.dnssec
+import dns.immutable
+import dns.exception
+import dns.rdata
+
+
+@dns.immutable.immutable
+class TKEY(dns.rdata.Rdata):
+
+    """TKEY Record"""
+
+    __slots__ = ['algorithm', 'inception', 'expiration', 'mode', 'error',
+                 'key', 'other']
+
+    def __init__(self, rdclass, rdtype, algorithm, inception, expiration,
+                 mode, error, key, other=b''):
+        super().__init__(rdclass, rdtype)
+        self.algorithm = self._as_name(algorithm)
+        self.inception = self._as_uint32(inception)
+        self.expiration = self._as_uint32(expiration)
+        self.mode = self._as_uint16(mode)
+        self.error = self._as_uint16(error)
+        self.key = self._as_bytes(key)
+        self.other = self._as_bytes(other)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        _algorithm = self.algorithm.choose_relativity(origin, relativize)
+        text = '%s %u %u %u %u %s' % (str(_algorithm), self.inception,
+                                      self.expiration, self.mode, self.error,
+                                      dns.rdata._base64ify(self.key, 0))
+        if len(self.other) > 0:
+            text += ' %s' % (dns.rdata._base64ify(self.other, 0))
+
+        return text
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_name(relativize=False)
+        inception = tok.get_uint32()
+        expiration = tok.get_uint32()
+        mode = tok.get_uint16()
+        error = tok.get_uint16()
+        key_b64 = tok.get_string().encode()
+        key = base64.b64decode(key_b64)
+        other_b64 = tok.concatenate_remaining_identifiers().encode()
+        other = base64.b64decode(other_b64)
+
+        return cls(rdclass, rdtype, algorithm, inception, expiration, mode,
+                   error, key, other)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.algorithm.to_wire(file, compress, origin)
+        file.write(struct.pack("!IIHH", self.inception, self.expiration,
+                               self.mode, self.error))
+        file.write(struct.pack("!H", len(self.key)))
+        file.write(self.key)
+        file.write(struct.pack("!H", len(self.other)))
+        if len(self.other) > 0:
+            file.write(self.other)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        algorithm = parser.get_name(origin)
+        inception, expiration, mode, error = parser.get_struct("!IIHH")
+        key = parser.get_counted_bytes(2)
+        other = parser.get_counted_bytes(2)
+
+        return cls(rdclass, rdtype, algorithm, inception, expiration, mode,
+                   error, key, other)
+
+    # Constants for the mode field - from RFC 2930:
+    # 2.5 The Mode Field
+    #
+    #    The mode field specifies the general scheme for key agreement or
+    #    the purpose of the TKEY DNS message.  Servers and resolvers
+    #    supporting this specification MUST implement the Diffie-Hellman key
+    #    agreement mode and the key deletion mode for queries.  All other
+    #    modes are OPTIONAL.  A server supporting TKEY that receives a TKEY
+    #    request with a mode it does not support returns the BADMODE error.
+    #    The following values of the Mode octet are defined, available, or
+    #    reserved:
+    #
+    #          Value    Description
+    #          -----    -----------
+    #           0        - reserved, see section 7
+    #           1       server assignment
+    #           2       Diffie-Hellman exchange
+    #           3       GSS-API negotiation
+    #           4       resolver assignment
+    #           5       key deletion
+    #          6-65534   - available, see section 7
+    #          65535     - reserved, see section 7
+    SERVER_ASSIGNMENT = 1
+    DIFFIE_HELLMAN_EXCHANGE = 2
+    GSSAPI_NEGOTIATION = 3
+    RESOLVER_ASSIGNMENT = 4
+    KEY_DELETION = 5

=== added file 'dns/rdtypes/ANY/TLSA.py'
--- old/dns/rdtypes/ANY/TLSA.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/TLSA.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,10 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.immutable
+import dns.rdtypes.tlsabase
+
+
+@dns.immutable.immutable
+class TLSA(dns.rdtypes.tlsabase.TLSABase):
+
+    """TLSA record"""

=== added file 'dns/rdtypes/ANY/TSIG.py'
--- old/dns/rdtypes/ANY/TSIG.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/TSIG.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,120 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rcode
+import dns.rdata
+
+
+@dns.immutable.immutable
+class TSIG(dns.rdata.Rdata):
+
+    """TSIG record"""
+
+    __slots__ = ['algorithm', 'time_signed', 'fudge', 'mac',
+                 'original_id', 'error', 'other']
+
+    def __init__(self, rdclass, rdtype, algorithm, time_signed, fudge, mac,
+                 original_id, error, other):
+        """Initialize a TSIG rdata.
+
+        *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+        *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+        *algorithm*, a ``dns.name.Name``.
+
+        *time_signed*, an ``int``.
+
+        *fudge*, an ``int`.
+
+        *mac*, a ``bytes``
+
+        *original_id*, an ``int``
+
+        *error*, an ``int``
+
+        *other*, a ``bytes``
+        """
+
+        super().__init__(rdclass, rdtype)
+        self.algorithm = self._as_name(algorithm)
+        self.time_signed = self._as_uint48(time_signed)
+        self.fudge = self._as_uint16(fudge)
+        self.mac = self._as_bytes(mac)
+        self.original_id = self._as_uint16(original_id)
+        self.error = dns.rcode.Rcode.make(error)
+        self.other = self._as_bytes(other)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        algorithm = self.algorithm.choose_relativity(origin, relativize)
+        error = dns.rcode.to_text(self.error, True)
+        text = f"{algorithm} {self.time_signed} {self.fudge} " + \
+               f"{len(self.mac)} {dns.rdata._base64ify(self.mac, 0)} " + \
+               f"{self.original_id} {error} {len(self.other)}"
+        if self.other:
+            text += f" {dns.rdata._base64ify(self.other, 0)}"
+        return text
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        algorithm = tok.get_name(relativize=False)
+        time_signed = tok.get_uint48()
+        fudge = tok.get_uint16()
+        mac_len = tok.get_uint16()
+        mac = base64.b64decode(tok.get_string())
+        if len(mac) != mac_len:
+            raise SyntaxError('invalid MAC')
+        original_id = tok.get_uint16()
+        error = dns.rcode.from_text(tok.get_string())
+        other_len = tok.get_uint16()
+        if other_len > 0:
+            other = base64.b64decode(tok.get_string())
+            if len(other) != other_len:
+                raise SyntaxError('invalid other data')
+        else:
+            other = b''
+        return cls(rdclass, rdtype, algorithm, time_signed, fudge, mac,
+                   original_id, error, other)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.algorithm.to_wire(file, None, origin, False)
+        file.write(struct.pack('!HIHH',
+                               (self.time_signed >> 32) & 0xffff,
+                               self.time_signed & 0xffffffff,
+                               self.fudge,
+                               len(self.mac)))
+        file.write(self.mac)
+        file.write(struct.pack('!HHH', self.original_id, self.error,
+                               len(self.other)))
+        file.write(self.other)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        algorithm = parser.get_name()
+        time_signed = parser.get_uint48()
+        fudge = parser.get_uint16()
+        mac = parser.get_counted_bytes(2)
+        (original_id, error) = parser.get_struct('!HH')
+        other = parser.get_counted_bytes(2)
+        return cls(rdclass, rdtype, algorithm, time_signed, fudge, mac,
+                   original_id, error, other)

=== added file 'dns/rdtypes/ANY/TXT.py'
--- old/dns/rdtypes/ANY/TXT.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/TXT.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.txtbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class TXT(dns.rdtypes.txtbase.TXTBase):
+
+    """TXT record"""

=== added file 'dns/rdtypes/ANY/URI.py'
--- old/dns/rdtypes/ANY/URI.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/URI.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,80 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdtypes.util
+import dns.name
+
+
+@dns.immutable.immutable
+class URI(dns.rdata.Rdata):
+
+    """URI record"""
+
+    # see RFC 7553
+
+    __slots__ = ['priority', 'weight', 'target']
+
+    def __init__(self, rdclass, rdtype, priority, weight, target):
+        super().__init__(rdclass, rdtype)
+        self.priority = self._as_uint16(priority)
+        self.weight = self._as_uint16(weight)
+        self.target = self._as_bytes(target, True)
+        if len(self.target) == 0:
+            raise dns.exception.SyntaxError("URI target cannot be empty")
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '%d %d "%s"' % (self.priority, self.weight,
+                               self.target.decode())
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        priority = tok.get_uint16()
+        weight = tok.get_uint16()
+        target = tok.get().unescape()
+        if not (target.is_quoted_string() or target.is_identifier()):
+            raise dns.exception.SyntaxError("URI target must be a string")
+        return cls(rdclass, rdtype, priority, weight, target.value)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        two_ints = struct.pack("!HH", self.priority, self.weight)
+        file.write(two_ints)
+        file.write(self.target)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (priority, weight) = parser.get_struct('!HH')
+        target = parser.get_remaining()
+        if len(target) == 0:
+            raise dns.exception.FormError('URI target may not be empty')
+        return cls(rdclass, rdtype, priority, weight, target)
+
+    def _processing_priority(self):
+        return self.priority
+
+    def _processing_weight(self):
+        return self.weight
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.weighted_processing_order(iterable)

=== added file 'dns/rdtypes/ANY/X25.py'
--- old/dns/rdtypes/ANY/X25.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/X25.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,57 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class X25(dns.rdata.Rdata):
+
+    """X25 record"""
+
+    # see RFC 1183
+
+    __slots__ = ['address']
+
+    def __init__(self, rdclass, rdtype, address):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_bytes(address, True, 255)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '"%s"' % dns.rdata._escapify(self.address)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_string()
+        return cls(rdclass, rdtype, address)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        l = len(self.address)
+        assert l < 256
+        file.write(struct.pack('!B', l))
+        file.write(self.address)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_counted_bytes()
+        return cls(rdclass, rdtype, address)

=== added file 'dns/rdtypes/ANY/ZONEMD.py'
--- old/dns/rdtypes/ANY/ZONEMD.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/ZONEMD.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,66 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import hashlib
+import struct
+import binascii
+
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.zone
+
+
+@dns.immutable.immutable
+class ZONEMD(dns.rdata.Rdata):
+
+    """ZONEMD record"""
+
+    # See RFC 8976
+
+    __slots__ = ['serial', 'scheme', 'hash_algorithm', 'digest']
+
+    def __init__(self, rdclass, rdtype, serial, scheme, hash_algorithm, digest):
+        super().__init__(rdclass, rdtype)
+        self.serial = self._as_uint32(serial)
+        self.scheme = dns.zone.DigestScheme.make(scheme)
+        self.hash_algorithm = dns.zone.DigestHashAlgorithm.make(hash_algorithm)
+        self.digest = self._as_bytes(digest)
+
+        if self.scheme == 0:  # reserved, RFC 8976 Sec. 5.2
+            raise ValueError('scheme 0 is reserved')
+        if self.hash_algorithm == 0:  # reserved, RFC 8976 Sec. 5.3
+            raise ValueError('hash_algorithm 0 is reserved')
+
+        hasher = dns.zone._digest_hashers.get(self.hash_algorithm)
+        if hasher and hasher().digest_size != len(self.digest):
+            raise ValueError('digest length inconsistent with hash algorithm')
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        kw = kw.copy()
+        chunksize = kw.pop('chunksize', 128)
+        return '%d %d %d %s' % (self.serial, self.scheme, self.hash_algorithm,
+                                dns.rdata._hexify(self.digest,
+                                                  chunksize=chunksize,
+                                                  **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        serial = tok.get_uint32()
+        scheme = tok.get_uint8()
+        hash_algorithm = tok.get_uint8()
+        digest = tok.concatenate_remaining_identifiers().encode()
+        digest = binascii.unhexlify(digest)
+        return cls(rdclass, rdtype, serial, scheme, hash_algorithm, digest)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!IBB", self.serial, self.scheme,
+                             self.hash_algorithm)
+        file.write(header)
+        file.write(self.digest)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct("!IBB")
+        digest = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], header[2], digest)

=== added file 'dns/rdtypes/ANY/__init__.py'
--- old/dns/rdtypes/ANY/__init__.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/ANY/__init__.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,64 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class ANY (generic) rdata type classes."""
+
+__all__ = [
+    'AFSDB',
+    'AMTRELAY',
+    'AVC',
+    'CAA',
+    'CDNSKEY',
+    'CDS',
+    'CERT',
+    'CNAME',
+    'CSYNC',
+    'DLV',
+    'DNAME',
+    'DNSKEY',
+    'DS',
+    'EUI48',
+    'EUI64',
+    'GPOS',
+    'HINFO',
+    'HIP',
+    'ISDN',
+    'LOC',
+    'MX',
+    'NINFO',
+    'NS',
+    'NSEC',
+    'NSEC3',
+    'NSEC3PARAM',
+    'OPENPGPKEY',
+    'OPT',
+    'PTR',
+    'RP',
+    'RRSIG',
+    'RT',
+    'SMIMEA',
+    'SOA',
+    'SPF',
+    'SSHFP',
+    'TKEY',
+    'TLSA',
+    'TSIG',
+    'TXT',
+    'URI',
+    'X25',
+    'ZONEMD',
+]

=== added directory 'dns/rdtypes/CH'
=== added file 'dns/rdtypes/CH/A.py'
--- old/dns/rdtypes/CH/A.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/CH/A.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,58 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.rdtypes.mxbase
+import dns.immutable
+
+@dns.immutable.immutable
+class A(dns.rdata.Rdata):
+
+    """A record for Chaosnet"""
+
+    # domain: the domain of the address
+    # address: the 16-bit address
+
+    __slots__ = ['domain', 'address']
+
+    def __init__(self, rdclass, rdtype, domain, address):
+        super().__init__(rdclass, rdtype)
+        self.domain = self._as_name(domain)
+        self.address = self._as_uint16(address)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        domain = self.domain.choose_relativity(origin, relativize)
+        return '%s %o' % (domain, self.address)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        domain = tok.get_name(origin, relativize, relativize_to)
+        address = tok.get_uint16(base=8)
+        return cls(rdclass, rdtype, domain, address)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.domain.to_wire(file, compress, origin, canonicalize)
+        pref = struct.pack("!H", self.address)
+        file.write(pref)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        domain = parser.get_name(origin)
+        address = parser.get_uint16()
+        return cls(rdclass, rdtype, domain, address)

=== added file 'dns/rdtypes/CH/__init__.py'
--- old/dns/rdtypes/CH/__init__.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/CH/__init__.py	2018-12-23 00:54:24 +0000
@@ -0,0 +1,22 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class CH rdata type classes."""
+
+__all__ = [
+    'A',
+]

=== added directory 'dns/rdtypes/IN'
=== added file 'dns/rdtypes/IN/A.py'
--- old/dns/rdtypes/IN/A.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/A.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,51 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class A(dns.rdata.Rdata):
+
+    """A record."""
+
+    __slots__ = ['address']
+
+    def __init__(self, rdclass, rdtype, address):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_ipv4_address(address)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return self.address
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_identifier()
+        return cls(rdclass, rdtype, address)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(dns.ipv4.inet_aton(self.address))
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_remaining()
+        return cls(rdclass, rdtype, address)

=== added file 'dns/rdtypes/IN/AAAA.py'
--- old/dns/rdtypes/IN/AAAA.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/AAAA.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,51 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.exception
+import dns.immutable
+import dns.ipv6
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class AAAA(dns.rdata.Rdata):
+
+    """AAAA record."""
+
+    __slots__ = ['address']
+
+    def __init__(self, rdclass, rdtype, address):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_ipv6_address(address)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return self.address
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_identifier()
+        return cls(rdclass, rdtype, address)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(dns.ipv6.inet_aton(self.address))
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_remaining()
+        return cls(rdclass, rdtype, address)

=== added file 'dns/rdtypes/IN/APL.py'
--- old/dns/rdtypes/IN/APL.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/APL.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,151 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+import codecs
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.rdata
+import dns.tokenizer
+
+@dns.immutable.immutable
+class APLItem:
+
+    """An APL list item."""
+
+    __slots__ = ['family', 'negation', 'address', 'prefix']
+
+    def __init__(self, family, negation, address, prefix):
+        self.family = dns.rdata.Rdata._as_uint16(family)
+        self.negation = dns.rdata.Rdata._as_bool(negation)
+        if self.family == 1:
+            self.address = dns.rdata.Rdata._as_ipv4_address(address)
+            self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 32)
+        elif self.family == 2:
+            self.address = dns.rdata.Rdata._as_ipv6_address(address)
+            self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 128)
+        else:
+            self.address = dns.rdata.Rdata._as_bytes(address)
+            self.prefix = dns.rdata.Rdata._as_uint8(prefix)
+
+    def __str__(self):
+        if self.negation:
+            return "!%d:%s/%s" % (self.family, self.address, self.prefix)
+        else:
+            return "%d:%s/%s" % (self.family, self.address, self.prefix)
+
+    def to_wire(self, file):
+        if self.family == 1:
+            address = dns.ipv4.inet_aton(self.address)
+        elif self.family == 2:
+            address = dns.ipv6.inet_aton(self.address)
+        else:
+            address = binascii.unhexlify(self.address)
+        #
+        # Truncate least significant zero bytes.
+        #
+        last = 0
+        for i in range(len(address) - 1, -1, -1):
+            if address[i] != 0:
+                last = i + 1
+                break
+        address = address[0: last]
+        l = len(address)
+        assert l < 128
+        if self.negation:
+            l |= 0x80
+        header = struct.pack('!HBB', self.family, self.prefix, l)
+        file.write(header)
+        file.write(address)
+
+
+@dns.immutable.immutable
+class APL(dns.rdata.Rdata):
+
+    """APL record."""
+
+    # see: RFC 3123
+
+    __slots__ = ['items']
+
+    def __init__(self, rdclass, rdtype, items):
+        super().__init__(rdclass, rdtype)
+        for item in items:
+            if not isinstance(item, APLItem):
+                raise ValueError('item not an APLItem')
+        self.items = tuple(items)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return ' '.join(map(str, self.items))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        items = []
+        for token in tok.get_remaining():
+            item = token.unescape().value
+            if item[0] == '!':
+                negation = True
+                item = item[1:]
+            else:
+                negation = False
+            (family, rest) = item.split(':', 1)
+            family = int(family)
+            (address, prefix) = rest.split('/', 1)
+            prefix = int(prefix)
+            item = APLItem(family, negation, address, prefix)
+            items.append(item)
+
+        return cls(rdclass, rdtype, items)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        for item in self.items:
+            item.to_wire(file)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+
+        items = []
+        while parser.remaining() > 0:
+            header = parser.get_struct('!HBB')
+            afdlen = header[2]
+            if afdlen > 127:
+                negation = True
+                afdlen -= 128
+            else:
+                negation = False
+            address = parser.get_bytes(afdlen)
+            l = len(address)
+            if header[0] == 1:
+                if l < 4:
+                    address += b'\x00' * (4 - l)
+            elif header[0] == 2:
+                if l < 16:
+                    address += b'\x00' * (16 - l)
+            else:
+                #
+                # This isn't really right according to the RFC, but it
+                # seems better than throwing an exception
+                #
+                address = codecs.encode(address, 'hex_codec')
+            item = APLItem(header[0], negation, address, header[1])
+            items.append(item)
+        return cls(rdclass, rdtype, items)

=== added file 'dns/rdtypes/IN/DHCID.py'
--- old/dns/rdtypes/IN/DHCID.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/DHCID.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,53 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+
+import dns.exception
+import dns.immutable
+
+
+@dns.immutable.immutable
+class DHCID(dns.rdata.Rdata):
+
+    """DHCID record"""
+
+    # see: RFC 4701
+
+    __slots__ = ['data']
+
+    def __init__(self, rdclass, rdtype, data):
+        super().__init__(rdclass, rdtype)
+        self.data = self._as_bytes(data)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return dns.rdata._base64ify(self.data, **kw)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        data = base64.b64decode(b64)
+        return cls(rdclass, rdtype, data)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(self.data)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        data = parser.get_remaining()
+        return cls(rdclass, rdtype, data)

=== added file 'dns/rdtypes/IN/HTTPS.py'
--- old/dns/rdtypes/IN/HTTPS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/HTTPS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,8 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.rdtypes.svcbbase
+import dns.immutable
+
+@dns.immutable.immutable
+class HTTPS(dns.rdtypes.svcbbase.SVCBBase):
+    """HTTPS record"""

=== added file 'dns/rdtypes/IN/IPSECKEY.py'
--- old/dns/rdtypes/IN/IPSECKEY.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/IPSECKEY.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,83 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import base64
+
+import dns.exception
+import dns.immutable
+import dns.rdtypes.util
+
+
+class Gateway(dns.rdtypes.util.Gateway):
+    name = 'IPSECKEY gateway'
+
+@dns.immutable.immutable
+class IPSECKEY(dns.rdata.Rdata):
+
+    """IPSECKEY record"""
+
+    # see: RFC 4025
+
+    __slots__ = ['precedence', 'gateway_type', 'algorithm', 'gateway', 'key']
+
+    def __init__(self, rdclass, rdtype, precedence, gateway_type, algorithm,
+                 gateway, key):
+        super().__init__(rdclass, rdtype)
+        gateway = Gateway(gateway_type, gateway)
+        self.precedence = self._as_uint8(precedence)
+        self.gateway_type = gateway.type
+        self.algorithm = self._as_uint8(algorithm)
+        self.gateway = gateway.gateway
+        self.key = self._as_bytes(key)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        gateway = Gateway(self.gateway_type, self.gateway).to_text(origin,
+                                                                   relativize)
+        return '%d %d %d %s %s' % (self.precedence, self.gateway_type,
+                                   self.algorithm, gateway,
+                                   dns.rdata._base64ify(self.key, **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        precedence = tok.get_uint8()
+        gateway_type = tok.get_uint8()
+        algorithm = tok.get_uint8()
+        gateway = Gateway.from_text(gateway_type, tok, origin, relativize,
+                                    relativize_to)
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        key = base64.b64decode(b64)
+        return cls(rdclass, rdtype, precedence, gateway_type, algorithm,
+                   gateway.gateway, key)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!BBB", self.precedence, self.gateway_type,
+                             self.algorithm)
+        file.write(header)
+        Gateway(self.gateway_type, self.gateway).to_wire(file, compress,
+                                                         origin, canonicalize)
+        file.write(self.key)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct('!BBB')
+        gateway_type = header[1]
+        gateway = Gateway.from_wire_parser(gateway_type, parser, origin)
+        key = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], gateway_type, header[2],
+                   gateway.gateway, key)

=== added file 'dns/rdtypes/IN/KX.py'
--- old/dns/rdtypes/IN/KX.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/KX.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.mxbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class KX(dns.rdtypes.mxbase.UncompressedDowncasingMX):
+
+    """KX record"""

=== added file 'dns/rdtypes/IN/NAPTR.py'
--- old/dns/rdtypes/IN/NAPTR.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/NAPTR.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,99 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+
+
+def _write_string(file, s):
+    l = len(s)
+    assert l < 256
+    file.write(struct.pack('!B', l))
+    file.write(s)
+
+
+@dns.immutable.immutable
+class NAPTR(dns.rdata.Rdata):
+
+    """NAPTR record"""
+
+    # see: RFC 3403
+
+    __slots__ = ['order', 'preference', 'flags', 'service', 'regexp',
+                 'replacement']
+
+    def __init__(self, rdclass, rdtype, order, preference, flags, service,
+                 regexp, replacement):
+        super().__init__(rdclass, rdtype)
+        self.flags = self._as_bytes(flags, True, 255)
+        self.service = self._as_bytes(service, True, 255)
+        self.regexp = self._as_bytes(regexp, True, 255)
+        self.order = self._as_uint16(order)
+        self.preference = self._as_uint16(preference)
+        self.replacement = self._as_name(replacement)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        replacement = self.replacement.choose_relativity(origin, relativize)
+        return '%d %d "%s" "%s" "%s" %s' % \
+               (self.order, self.preference,
+                dns.rdata._escapify(self.flags),
+                dns.rdata._escapify(self.service),
+                dns.rdata._escapify(self.regexp),
+                replacement)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        order = tok.get_uint16()
+        preference = tok.get_uint16()
+        flags = tok.get_string()
+        service = tok.get_string()
+        regexp = tok.get_string()
+        replacement = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, order, preference, flags, service,
+                   regexp, replacement)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        two_ints = struct.pack("!HH", self.order, self.preference)
+        file.write(two_ints)
+        _write_string(file, self.flags)
+        _write_string(file, self.service)
+        _write_string(file, self.regexp)
+        self.replacement.to_wire(file, compress, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (order, preference) = parser.get_struct('!HH')
+        strings = []
+        for _ in range(3):
+            s = parser.get_counted_bytes()
+            strings.append(s)
+        replacement = parser.get_name(origin)
+        return cls(rdclass, rdtype, order, preference, strings[0], strings[1],
+                   strings[2], replacement)
+
+    def _processing_priority(self):
+        return (self.order, self.preference)
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.priority_processing_order(iterable)

=== added file 'dns/rdtypes/IN/NSAP.py'
--- old/dns/rdtypes/IN/NSAP.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/NSAP.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,60 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class NSAP(dns.rdata.Rdata):
+
+    """NSAP record."""
+
+    # see: RFC 1706
+
+    __slots__ = ['address']
+
+    def __init__(self, rdclass, rdtype, address):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_bytes(address)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return "0x%s" % binascii.hexlify(self.address).decode()
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_string()
+        if address[0:2] != '0x':
+            raise dns.exception.SyntaxError('string does not start with 0x')
+        address = address[2:].replace('.', '')
+        if len(address) % 2 != 0:
+            raise dns.exception.SyntaxError('hexstring has odd length')
+        address = binascii.unhexlify(address.encode())
+        return cls(rdclass, rdtype, address)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(self.address)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_remaining()
+        return cls(rdclass, rdtype, address)

=== added file 'dns/rdtypes/IN/NSAP_PTR.py'
--- old/dns/rdtypes/IN/NSAP_PTR.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/NSAP_PTR.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,25 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import dns.rdtypes.nsbase
+import dns.immutable
+
+
+@dns.immutable.immutable
+class NSAP_PTR(dns.rdtypes.nsbase.UncompressedNS):
+
+    """NSAP-PTR record"""

=== added file 'dns/rdtypes/IN/PX.py'
--- old/dns/rdtypes/IN/PX.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/PX.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,73 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdtypes.util
+import dns.name
+
+
+@dns.immutable.immutable
+class PX(dns.rdata.Rdata):
+
+    """PX record."""
+
+    # see: RFC 2163
+
+    __slots__ = ['preference', 'map822', 'mapx400']
+
+    def __init__(self, rdclass, rdtype, preference, map822, mapx400):
+        super().__init__(rdclass, rdtype)
+        self.preference = self._as_uint16(preference)
+        self.map822 = self._as_name(map822)
+        self.mapx400 = self._as_name(mapx400)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        map822 = self.map822.choose_relativity(origin, relativize)
+        mapx400 = self.mapx400.choose_relativity(origin, relativize)
+        return '%d %s %s' % (self.preference, map822, mapx400)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        preference = tok.get_uint16()
+        map822 = tok.get_name(origin, relativize, relativize_to)
+        mapx400 = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, preference, map822, mapx400)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        pref = struct.pack("!H", self.preference)
+        file.write(pref)
+        self.map822.to_wire(file, None, origin, canonicalize)
+        self.mapx400.to_wire(file, None, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        preference = parser.get_uint16()
+        map822 = parser.get_name(origin)
+        mapx400 = parser.get_name(origin)
+        return cls(rdclass, rdtype, preference, map822, mapx400)
+
+    def _processing_priority(self):
+        return self.preference
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.priority_processing_order(iterable)

=== added file 'dns/rdtypes/IN/SRV.py'
--- old/dns/rdtypes/IN/SRV.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/SRV.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,76 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdtypes.util
+import dns.name
+
+
+@dns.immutable.immutable
+class SRV(dns.rdata.Rdata):
+
+    """SRV record"""
+
+    # see: RFC 2782
+
+    __slots__ = ['priority', 'weight', 'port', 'target']
+
+    def __init__(self, rdclass, rdtype, priority, weight, port, target):
+        super().__init__(rdclass, rdtype)
+        self.priority = self._as_uint16(priority)
+        self.weight = self._as_uint16(weight)
+        self.port = self._as_uint16(port)
+        self.target = self._as_name(target)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        target = self.target.choose_relativity(origin, relativize)
+        return '%d %d %d %s' % (self.priority, self.weight, self.port,
+                                target)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        priority = tok.get_uint16()
+        weight = tok.get_uint16()
+        port = tok.get_uint16()
+        target = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, priority, weight, port, target)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        three_ints = struct.pack("!HHH", self.priority, self.weight, self.port)
+        file.write(three_ints)
+        self.target.to_wire(file, compress, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (priority, weight, port) = parser.get_struct('!HHH')
+        target = parser.get_name(origin)
+        return cls(rdclass, rdtype, priority, weight, port, target)
+
+    def _processing_priority(self):
+        return self.priority
+
+    def _processing_weight(self):
+        return self.weight
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.weighted_processing_order(iterable)

=== added file 'dns/rdtypes/IN/SVCB.py'
--- old/dns/rdtypes/IN/SVCB.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/SVCB.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,8 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import dns.rdtypes.svcbbase
+import dns.immutable
+
+@dns.immutable.immutable
+class SVCB(dns.rdtypes.svcbbase.SVCBBase):
+    """SVCB record"""

=== added file 'dns/rdtypes/IN/WKS.py'
--- old/dns/rdtypes/IN/WKS.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/WKS.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,96 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import socket
+import struct
+
+import dns.ipv4
+import dns.immutable
+import dns.rdata
+
+_proto_tcp = socket.getprotobyname('tcp')
+_proto_udp = socket.getprotobyname('udp')
+
+
+@dns.immutable.immutable
+class WKS(dns.rdata.Rdata):
+
+    """WKS record"""
+
+    # see: RFC 1035
+
+    __slots__ = ['address', 'protocol', 'bitmap']
+
+    def __init__(self, rdclass, rdtype, address, protocol, bitmap):
+        super().__init__(rdclass, rdtype)
+        self.address = self._as_ipv4_address(address)
+        self.protocol = self._as_uint8(protocol)
+        self.bitmap = self._as_bytes(bitmap)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        bits = []
+        for i in range(0, len(self.bitmap)):
+            byte = self.bitmap[i]
+            for j in range(0, 8):
+                if byte & (0x80 >> j):
+                    bits.append(str(i * 8 + j))
+        text = ' '.join(bits)
+        return '%s %d %s' % (self.address, self.protocol, text)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        address = tok.get_string()
+        protocol = tok.get_string()
+        if protocol.isdigit():
+            protocol = int(protocol)
+        else:
+            protocol = socket.getprotobyname(protocol)
+        bitmap = bytearray()
+        for token in tok.get_remaining():
+            value = token.unescape().value
+            if value.isdigit():
+                serv = int(value)
+            else:
+                if protocol != _proto_udp and protocol != _proto_tcp:
+                    raise NotImplementedError("protocol must be TCP or UDP")
+                if protocol == _proto_udp:
+                    protocol_text = "udp"
+                else:
+                    protocol_text = "tcp"
+                serv = socket.getservbyname(value, protocol_text)
+            i = serv // 8
+            l = len(bitmap)
+            if l < i + 1:
+                for _ in range(l, i + 1):
+                    bitmap.append(0)
+            bitmap[i] = bitmap[i] | (0x80 >> (serv % 8))
+        bitmap = dns.rdata._truncate_bitmap(bitmap)
+        return cls(rdclass, rdtype, address, protocol, bitmap)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(dns.ipv4.inet_aton(self.address))
+        protocol = struct.pack('!B', self.protocol)
+        file.write(protocol)
+        file.write(self.bitmap)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        address = parser.get_bytes(4)
+        protocol = parser.get_uint8()
+        bitmap = parser.get_remaining()
+        return cls(rdclass, rdtype, address, protocol, bitmap)

=== added file 'dns/rdtypes/IN/__init__.py'
--- old/dns/rdtypes/IN/__init__.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/IN/__init__.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,35 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Class IN rdata type classes."""
+
+__all__ = [
+    'A',
+    'AAAA',
+    'APL',
+    'DHCID',
+    'HTTPS',
+    'IPSECKEY',
+    'KX',
+    'NAPTR',
+    'NSAP',
+    'NSAP_PTR',
+    'PX',
+    'SRV',
+    'SVCB',
+    'WKS',
+]

=== added file 'dns/rdtypes/__init__.py'
--- old/dns/rdtypes/__init__.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/__init__.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,33 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS rdata type classes"""
+
+__all__ = [
+    'ANY',
+    'IN',
+    'CH',
+    'dnskeybase',
+    'dsbase',
+    'euibase',
+    'mxbase',
+    'nsbase',
+    'svcbbase',
+    'tlsabase',
+    'txtbase',
+    'util'
+]

=== added file 'dns/rdtypes/dnskeybase.py'
--- old/dns/rdtypes/dnskeybase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/dnskeybase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,82 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import base64
+import enum
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.dnssec
+import dns.rdata
+
+# wildcard import
+__all__ = ["SEP", "REVOKE", "ZONE"]   # noqa: F822
+
+class Flag(enum.IntFlag):
+    SEP = 0x0001
+    REVOKE = 0x0080
+    ZONE = 0x0100
+
+
+@dns.immutable.immutable
+class DNSKEYBase(dns.rdata.Rdata):
+
+    """Base class for rdata that is like a DNSKEY record"""
+
+    __slots__ = ['flags', 'protocol', 'algorithm', 'key']
+
+    def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key):
+        super().__init__(rdclass, rdtype)
+        self.flags = self._as_uint16(flags)
+        self.protocol = self._as_uint8(protocol)
+        self.algorithm = dns.dnssec.Algorithm.make(algorithm)
+        self.key = self._as_bytes(key)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return '%d %d %d %s' % (self.flags, self.protocol, self.algorithm,
+                                dns.rdata._base64ify(self.key, **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        flags = tok.get_uint16()
+        protocol = tok.get_uint8()
+        algorithm = tok.get_string()
+        b64 = tok.concatenate_remaining_identifiers().encode()
+        key = base64.b64decode(b64)
+        return cls(rdclass, rdtype, flags, protocol, algorithm, key)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!HBB", self.flags, self.protocol, self.algorithm)
+        file.write(header)
+        file.write(self.key)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct('!HBB')
+        key = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], header[2],
+                   key)
+
+### BEGIN generated Flag constants
+
+SEP = Flag.SEP
+REVOKE = Flag.REVOKE
+ZONE = Flag.ZONE
+
+### END generated Flag constants

=== added file 'dns/rdtypes/dnskeybase.pyi'
--- old/dns/rdtypes/dnskeybase.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/dnskeybase.pyi	2020-08-06 23:26:37 +0000
@@ -0,0 +1,38 @@
+from typing import Set, Any
+
+SEP : int
+REVOKE : int
+ZONE : int
+
+def flags_to_text_set(flags : int) -> Set[str]:
+    ...
+
+def flags_from_text_set(texts_set) -> int:
+    ...
+
+from .. import rdata
+
+class DNSKEYBase(rdata.Rdata):
+    def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key):
+        self.flags : int
+        self.protocol : int
+        self.key : str
+        self.algorithm : int
+
+    def to_text(self, origin : Any = None, relativize=True, **kw : Any):
+        ...
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        ...
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        ...
+
+    @classmethod
+    def from_parser(cls, rdclass, rdtype, parser, origin=None):
+        ...
+
+    def flags_to_text_set(self) -> Set[str]:
+        ...

=== added file 'dns/rdtypes/dsbase.py'
--- old/dns/rdtypes/dsbase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/dsbase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,90 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2010, 2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import binascii
+
+import dns.dnssec
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+
+
+# Digest types registry: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml
+_digest_length_by_type = {
+    1: 20,  # SHA-1, RFC 3658 Sec. 2.4
+    2: 32,  # SHA-256, RFC 4509 Sec. 2.2
+    3: 32,  # GOST R 34.11-94, RFC 5933 Sec. 4 in conjunction with RFC 4490 Sec. 2.1
+    4: 48,  # SHA-384, RFC 6605 Sec. 2
+}
+
+
+@dns.immutable.immutable
+class DSBase(dns.rdata.Rdata):
+
+    """Base class for rdata that is like a DS record"""
+
+    __slots__ = ['key_tag', 'algorithm', 'digest_type', 'digest']
+
+    def __init__(self, rdclass, rdtype, key_tag, algorithm, digest_type,
+                 digest):
+        super().__init__(rdclass, rdtype)
+        self.key_tag = self._as_uint16(key_tag)
+        self.algorithm = dns.dnssec.Algorithm.make(algorithm)
+        self.digest_type = self._as_uint8(digest_type)
+        self.digest = self._as_bytes(digest)
+
+        try:
+            if self.digest_type == 0:  # reserved, RFC 3658 Sec. 2.4
+                raise ValueError('digest type 0 is reserved')
+            expected_length = _digest_length_by_type[self.digest_type]
+        except KeyError:
+            raise ValueError('unknown digest type')
+        if len(self.digest) != expected_length:
+            raise ValueError('digest length inconsistent with digest type')
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        kw = kw.copy()
+        chunksize = kw.pop('chunksize', 128)
+        return '%d %d %d %s' % (self.key_tag, self.algorithm,
+                                self.digest_type,
+                                dns.rdata._hexify(self.digest,
+                                                  chunksize=chunksize,
+                                                  **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        key_tag = tok.get_uint16()
+        algorithm = tok.get_string()
+        digest_type = tok.get_uint8()
+        digest = tok.concatenate_remaining_identifiers().encode()
+        digest = binascii.unhexlify(digest)
+        return cls(rdclass, rdtype, key_tag, algorithm, digest_type,
+                   digest)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!HBB", self.key_tag, self.algorithm,
+                             self.digest_type)
+        file.write(header)
+        file.write(self.digest)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct("!HBB")
+        digest = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], header[2], digest)

=== added file 'dns/rdtypes/euibase.py'
--- old/dns/rdtypes/euibase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/euibase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,69 @@
+# Copyright (C) 2015 Red Hat, Inc.
+# Author: Petr Spacek <pspacek@redhat.com>
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import binascii
+
+import dns.rdata
+import dns.immutable
+
+
+@dns.immutable.immutable
+class EUIBase(dns.rdata.Rdata):
+
+    """EUIxx record"""
+
+    # see: rfc7043.txt
+
+    __slots__ = ['eui']
+    # define these in subclasses
+    # byte_len = 6  # 0123456789ab (in hex)
+    # text_len = byte_len * 3 - 1  # 01-23-45-67-89-ab
+
+    def __init__(self, rdclass, rdtype, eui):
+        super().__init__(rdclass, rdtype)
+        self.eui = self._as_bytes(eui)
+        if len(self.eui) != self.byte_len:
+            raise dns.exception.FormError('EUI%s rdata has to have %s bytes'
+                                          % (self.byte_len * 8, self.byte_len))
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        return dns.rdata._hexify(self.eui, chunksize=2, **kw).replace(' ', '-')
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        text = tok.get_string()
+        if len(text) != cls.text_len:
+            raise dns.exception.SyntaxError(
+                'Input text must have %s characters' % cls.text_len)
+        for i in range(2, cls.byte_len * 3 - 1, 3):
+            if text[i] != '-':
+                raise dns.exception.SyntaxError('Dash expected at position %s'
+                                                % i)
+        text = text.replace('-', '')
+        try:
+            data = binascii.unhexlify(text.encode())
+        except (ValueError, TypeError) as ex:
+            raise dns.exception.SyntaxError('Hex decoding error: %s' % str(ex))
+        return cls(rdclass, rdtype, data)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(self.eui)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        eui = parser.get_bytes(cls.byte_len)
+        return cls(rdclass, rdtype, eui)

=== added file 'dns/rdtypes/mxbase.py'
--- old/dns/rdtypes/mxbase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/mxbase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,89 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""MX-like base classes."""
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.name
+import dns.rdtypes.util
+
+
+@dns.immutable.immutable
+class MXBase(dns.rdata.Rdata):
+
+    """Base class for rdata that is like an MX record."""
+
+    __slots__ = ['preference', 'exchange']
+
+    def __init__(self, rdclass, rdtype, preference, exchange):
+        super().__init__(rdclass, rdtype)
+        self.preference = self._as_uint16(preference)
+        self.exchange = self._as_name(exchange)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        exchange = self.exchange.choose_relativity(origin, relativize)
+        return '%d %s' % (self.preference, exchange)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        preference = tok.get_uint16()
+        exchange = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, preference, exchange)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        pref = struct.pack("!H", self.preference)
+        file.write(pref)
+        self.exchange.to_wire(file, compress, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        preference = parser.get_uint16()
+        exchange = parser.get_name(origin)
+        return cls(rdclass, rdtype, preference, exchange)
+
+    def _processing_priority(self):
+        return self.preference
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.priority_processing_order(iterable)
+
+
+@dns.immutable.immutable
+class UncompressedMX(MXBase):
+
+    """Base class for rdata that is like an MX record, but whose name
+    is not compressed when converted to DNS wire format, and whose
+    digestable form is not downcased."""
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        super()._to_wire(file, None, origin, False)
+
+
+@dns.immutable.immutable
+class UncompressedDowncasingMX(MXBase):
+
+    """Base class for rdata that is like an MX record, but whose name
+    is not compressed when convert to DNS wire format."""
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        super()._to_wire(file, None, origin, canonicalize)

=== added file 'dns/rdtypes/nsbase.py'
--- old/dns/rdtypes/nsbase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/nsbase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,64 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""NS-like base classes."""
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.name
+
+
+@dns.immutable.immutable
+class NSBase(dns.rdata.Rdata):
+
+    """Base class for rdata that is like an NS record."""
+
+    __slots__ = ['target']
+
+    def __init__(self, rdclass, rdtype, target):
+        super().__init__(rdclass, rdtype)
+        self.target = self._as_name(target)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        target = self.target.choose_relativity(origin, relativize)
+        return str(target)
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        target = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, target)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.target.to_wire(file, compress, origin, canonicalize)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        target = parser.get_name(origin)
+        return cls(rdclass, rdtype, target)
+
+
+@dns.immutable.immutable
+class UncompressedNS(NSBase):
+
+    """Base class for rdata that is like an NS record, but whose name
+    is not compressed when convert to DNS wire format, and whose
+    digestable form is not downcased."""
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        self.target.to_wire(file, None, origin, False)

=== added file 'dns/rdtypes/svcbbase.py'
--- old/dns/rdtypes/svcbbase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/svcbbase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,544 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import base64
+import enum
+import io
+import struct
+
+import dns.enum
+import dns.exception
+import dns.immutable
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdata
+import dns.rdtypes.util
+import dns.tokenizer
+import dns.wire
+
+# Until there is an RFC, this module is experimental and may be changed in
+# incompatible ways.
+
+
+class UnknownParamKey(dns.exception.DNSException):
+    """Unknown SVCB ParamKey"""
+
+
+class ParamKey(dns.enum.IntEnum):
+    """SVCB ParamKey"""
+
+    MANDATORY = 0
+    ALPN = 1
+    NO_DEFAULT_ALPN = 2
+    PORT = 3
+    IPV4HINT = 4
+    ECHCONFIG = 5
+    IPV6HINT = 6
+
+    @classmethod
+    def _maximum(cls):
+        return 65535
+
+    @classmethod
+    def _short_name(cls):
+        return "SVCBParamKey"
+
+    @classmethod
+    def _prefix(cls):
+        return "KEY"
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownParamKey
+
+
+class Emptiness(enum.IntEnum):
+    NEVER = 0
+    ALWAYS = 1
+    ALLOWED = 2
+
+
+def _validate_key(key):
+    force_generic = False
+    if isinstance(key, bytes):
+        # We decode to latin-1 so we get 0-255 as valid and do NOT interpret
+        # UTF-8 sequences
+        key = key.decode('latin-1')
+    if isinstance(key, str):
+        if key.lower().startswith('key'):
+            force_generic = True
+            if key[3:].startswith('0') and len(key) != 4:
+                # key has leading zeros
+                raise ValueError('leading zeros in key')
+        key = key.replace('-', '_')
+    return (ParamKey.make(key), force_generic)
+
+def key_to_text(key):
+    return ParamKey.to_text(key).replace('_', '-').lower()
+
+# Like rdata escapify, but escapes ',' too.
+
+_escaped = b'",\\'
+
+def _escapify(qstring):
+    text = ''
+    for c in qstring:
+        if c in _escaped:
+            text += '\\' + chr(c)
+        elif c >= 0x20 and c < 0x7F:
+            text += chr(c)
+        else:
+            text += '\\%03d' % c
+    return text
+
+def _unescape(value, list_mode=False):
+    if value == '':
+        return value
+    items = []
+    unescaped = b''
+    l = len(value)
+    i = 0
+    while i < l:
+        c = value[i]
+        i += 1
+        if c == ',' and list_mode:
+            if len(unescaped) == 0:
+                raise ValueError('list item cannot be empty')
+            items.append(unescaped)
+            unescaped = b''
+            continue
+        if c == '\\':
+            if i >= l:  # pragma: no cover   (can't happen via tokenizer get())
+                raise dns.exception.UnexpectedEnd
+            c = value[i]
+            i += 1
+            if c.isdigit():
+                if i >= l:
+                    raise dns.exception.UnexpectedEnd
+                c2 = value[i]
+                i += 1
+                if i >= l:
+                    raise dns.exception.UnexpectedEnd
+                c3 = value[i]
+                i += 1
+                if not (c2.isdigit() and c3.isdigit()):
+                    raise dns.exception.SyntaxError
+                codepoint = int(c) * 100 + int(c2) * 10 + int(c3)
+                if codepoint > 255:
+                    raise dns.exception.SyntaxError
+                c = chr(codepoint)
+        unescaped += c.encode()
+    if len(unescaped) > 0:
+        items.append(unescaped)
+    else:
+        # This can't happen outside of list_mode because that would
+        # require the value parameter to the function to be empty, but
+        # we special case that at the beginning.
+        assert list_mode
+        raise ValueError('trailing comma')
+    if list_mode:
+        return items
+    else:
+        return items[0]
+
+
+@dns.immutable.immutable
+class Param:
+    """Abstract base class for SVCB parameters"""
+
+    @classmethod
+    def emptiness(cls):
+        return Emptiness.NEVER
+
+
+@dns.immutable.immutable
+class GenericParam(Param):
+    """Generic SVCB parameter
+    """
+    def __init__(self, value):
+        self.value = dns.rdata.Rdata._as_bytes(value, True)
+
+    @classmethod
+    def emptiness(cls):
+        return Emptiness.ALLOWED
+
+    @classmethod
+    def from_value(cls, value):
+        if value is None or len(value) == 0:
+            return None
+        else:
+            return cls(_unescape(value))
+
+    def to_text(self):
+        return '"' + _escapify(self.value) + '"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        value = parser.get_bytes(parser.remaining())
+        if len(value) == 0:
+            return None
+        else:
+            return cls(value)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        file.write(self.value)
+
+
+@dns.immutable.immutable
+class MandatoryParam(Param):
+    def __init__(self, keys):
+        # check for duplicates
+        keys = sorted([_validate_key(key)[0] for key in keys])
+        prior_k = None
+        for k in keys:
+            if k == prior_k:
+                raise ValueError(f'duplicate key {k}')
+            prior_k = k
+            if k == ParamKey.MANDATORY:
+                raise ValueError('listed the mandatory key as mandatory')
+        self.keys = tuple(keys)
+
+    @classmethod
+    def from_value(cls, value):
+        keys = [k.encode() for k in value.split(',')]
+        return cls(keys)
+
+    def to_text(self):
+        return '"' + ','.join([key_to_text(key) for key in self.keys]) + '"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        keys = []
+        last_key = -1
+        while parser.remaining() > 0:
+            key = parser.get_uint16()
+            if key < last_key:
+                raise dns.exception.FormError('manadatory keys not ascending')
+            last_key = key
+            keys.append(key)
+        return cls(keys)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        for key in self.keys:
+            file.write(struct.pack('!H', key))
+
+
+@dns.immutable.immutable
+class ALPNParam(Param):
+    def __init__(self, ids):
+        self.ids = dns.rdata.Rdata._as_tuple(
+            ids, lambda x: dns.rdata.Rdata._as_bytes(x, True, 255, False))
+
+    @classmethod
+    def from_value(cls, value):
+        return cls(_unescape(value, True))
+
+    def to_text(self):
+        return '"' + ','.join([_escapify(id) for id in self.ids]) + '"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        ids = []
+        while parser.remaining() > 0:
+            id = parser.get_counted_bytes()
+            ids.append(id)
+        return cls(ids)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        for id in self.ids:
+            file.write(struct.pack('!B', len(id)))
+            file.write(id)
+
+
+@dns.immutable.immutable
+class NoDefaultALPNParam(Param):
+    # We don't ever expect to instantiate this class, but we need
+    # a from_value() and a from_wire_parser(), so we just return None
+    # from the class methods when things are OK.
+
+    @classmethod
+    def emptiness(cls):
+        return Emptiness.ALWAYS
+
+    @classmethod
+    def from_value(cls, value):
+        if value is None or value == '':
+            return None
+        else:
+            raise ValueError('no-default-alpn with non-empty value')
+
+    def to_text(self):
+        raise NotImplementedError  # pragma: no cover
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        if parser.remaining() != 0:
+            raise dns.exception.FormError
+        return None
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        raise NotImplementedError  # pragma: no cover
+
+
+@dns.immutable.immutable
+class PortParam(Param):
+    def __init__(self, port):
+        self.port = dns.rdata.Rdata._as_uint16(port)
+
+    @classmethod
+    def from_value(cls, value):
+        value = int(value)
+        return cls(value)
+
+    def to_text(self):
+        return f'"{self.port}"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        port = parser.get_uint16()
+        return cls(port)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        file.write(struct.pack('!H', self.port))
+
+
+@dns.immutable.immutable
+class IPv4HintParam(Param):
+    def __init__(self, addresses):
+        self.addresses = dns.rdata.Rdata._as_tuple(
+            addresses, dns.rdata.Rdata._as_ipv4_address)
+
+    @classmethod
+    def from_value(cls, value):
+        addresses = value.split(',')
+        return cls(addresses)
+
+    def to_text(self):
+        return '"' + ','.join(self.addresses) + '"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        addresses = []
+        while parser.remaining() > 0:
+            ip = parser.get_bytes(4)
+            addresses.append(dns.ipv4.inet_ntoa(ip))
+        return cls(addresses)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        for address in self.addresses:
+            file.write(dns.ipv4.inet_aton(address))
+
+
+@dns.immutable.immutable
+class IPv6HintParam(Param):
+    def __init__(self, addresses):
+        self.addresses = dns.rdata.Rdata._as_tuple(
+            addresses, dns.rdata.Rdata._as_ipv6_address)
+
+    @classmethod
+    def from_value(cls, value):
+        addresses = value.split(',')
+        return cls(addresses)
+
+    def to_text(self):
+        return '"' + ','.join(self.addresses) + '"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        addresses = []
+        while parser.remaining() > 0:
+            ip = parser.get_bytes(16)
+            addresses.append(dns.ipv6.inet_ntoa(ip))
+        return cls(addresses)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        for address in self.addresses:
+            file.write(dns.ipv6.inet_aton(address))
+
+
+@dns.immutable.immutable
+class ECHConfigParam(Param):
+    def __init__(self, echconfig):
+        self.echconfig = dns.rdata.Rdata._as_bytes(echconfig, True)
+
+    @classmethod
+    def from_value(cls, value):
+        if '\\' in value:
+            raise ValueError('escape in ECHConfig value')
+        value = base64.b64decode(value.encode())
+        return cls(value)
+
+    def to_text(self):
+        b64 = base64.b64encode(self.echconfig).decode('ascii')
+        return f'"{b64}"'
+
+    @classmethod
+    def from_wire_parser(cls, parser, origin=None):  # pylint: disable=W0613
+        value = parser.get_bytes(parser.remaining())
+        return cls(value)
+
+    def to_wire(self, file, origin=None):  # pylint: disable=W0613
+        file.write(self.echconfig)
+
+
+_class_for_key = {
+    ParamKey.MANDATORY: MandatoryParam,
+    ParamKey.ALPN: ALPNParam,
+    ParamKey.NO_DEFAULT_ALPN: NoDefaultALPNParam,
+    ParamKey.PORT: PortParam,
+    ParamKey.IPV4HINT: IPv4HintParam,
+    ParamKey.ECHCONFIG: ECHConfigParam,
+    ParamKey.IPV6HINT: IPv6HintParam,
+}
+
+
+def _validate_and_define(params, key, value):
+    (key, force_generic) = _validate_key(_unescape(key))
+    if key in params:
+        raise SyntaxError(f'duplicate key "{key}"')
+    cls = _class_for_key.get(key, GenericParam)
+    emptiness = cls.emptiness()
+    if value is None:
+        if emptiness == Emptiness.NEVER:
+            raise SyntaxError('value cannot be empty')
+        value = cls.from_value(value)
+    else:
+        if force_generic:
+            value = cls.from_wire_parser(dns.wire.Parser(_unescape(value)))
+        else:
+            value = cls.from_value(value)
+    params[key] = value
+
+
+@dns.immutable.immutable
+class SVCBBase(dns.rdata.Rdata):
+
+    """Base class for SVCB-like records"""
+
+    # see: draft-ietf-dnsop-svcb-https-01
+
+    __slots__ = ['priority', 'target', 'params']
+
+    def __init__(self, rdclass, rdtype, priority, target, params):
+        super().__init__(rdclass, rdtype)
+        self.priority = self._as_uint16(priority)
+        self.target = self._as_name(target)
+        for k, v in params.items():
+            k = ParamKey.make(k)
+            if not isinstance(v, Param) and v is not None:
+                raise ValueError("not a Param")
+        self.params = dns.immutable.Dict(params)
+        # Make sure any paramater listed as mandatory is present in the
+        # record.
+        mandatory = params.get(ParamKey.MANDATORY)
+        if mandatory:
+            for key in mandatory.keys:
+                # Note we have to say "not in" as we have None as a value
+                # so a get() and a not None test would be wrong.
+                if key not in params:
+                    raise ValueError(f'key {key} declared mandatory but not'
+                                     'present')
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        target = self.target.choose_relativity(origin, relativize)
+        params = []
+        for key in sorted(self.params.keys()):
+            value = self.params[key]
+            if value is None:
+                params.append(key_to_text(key))
+            else:
+                kv = key_to_text(key) + '=' + value.to_text()
+                params.append(kv)
+        if len(params) > 0:
+            space = ' '
+        else:
+            space = ''
+        return '%d %s%s%s' % (self.priority, target, space, ' '.join(params))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        priority = tok.get_uint16()
+        target = tok.get_name(origin, relativize, relativize_to)
+        if priority == 0:
+            token = tok.get()
+            if not token.is_eol_or_eof():
+                raise SyntaxError('parameters in AliasMode')
+            tok.unget(token)
+        params = {}
+        while True:
+            token = tok.get()
+            if token.is_eol_or_eof():
+                tok.unget(token)
+                break
+            if token.ttype != dns.tokenizer.IDENTIFIER:
+                raise SyntaxError('parameter is not an identifier')
+            equals = token.value.find('=')
+            if equals == len(token.value) - 1:
+                # 'key=', so next token should be a quoted string without
+                # any intervening whitespace.
+                key = token.value[:-1]
+                token = tok.get(want_leading=True)
+                if token.ttype != dns.tokenizer.QUOTED_STRING:
+                    raise SyntaxError('whitespace after =')
+                value = token.value
+            elif equals > 0:
+                # key=value
+                key = token.value[:equals]
+                value = token.value[equals + 1:]
+            elif equals == 0:
+                # =key
+                raise SyntaxError('parameter cannot start with "="')
+            else:
+                # key
+                key = token.value
+                value = None
+            _validate_and_define(params, key, value)
+        return cls(rdclass, rdtype, priority, target, params)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        file.write(struct.pack("!H", self.priority))
+        self.target.to_wire(file, None, origin, False)
+        for key in sorted(self.params):
+            file.write(struct.pack("!H", key))
+            value = self.params[key]
+            # placeholder for length (or actual length of empty values)
+            file.write(struct.pack("!H", 0))
+            if value is None:
+                continue
+            else:
+                start = file.tell()
+                value.to_wire(file, origin)
+                end = file.tell()
+                assert end - start < 65536
+                file.seek(start - 2)
+                stuff = struct.pack("!H", end - start)
+                file.write(stuff)
+                file.seek(0, io.SEEK_END)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        priority = parser.get_uint16()
+        target = parser.get_name(origin)
+        if priority == 0 and parser.remaining() != 0:
+            raise dns.exception.FormError('parameters in AliasMode')
+        params = {}
+        prior_key = -1
+        while parser.remaining() > 0:
+            key = parser.get_uint16()
+            if key < prior_key:
+                raise dns.exception.FormError('keys not in order')
+            prior_key = key
+            vlen = parser.get_uint16()
+            pcls = _class_for_key.get(key, GenericParam)
+            with parser.restrict_to(vlen):
+                value = pcls.from_wire_parser(parser, origin)
+            params[key] = value
+        return cls(rdclass, rdtype, priority, target, params)
+
+    def _processing_priority(self):
+        return self.priority
+
+    @classmethod
+    def _processing_order(cls, iterable):
+        return dns.rdtypes.util.priority_processing_order(iterable)

=== added file 'dns/rdtypes/tlsabase.py'
--- old/dns/rdtypes/tlsabase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/tlsabase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,72 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import struct
+import binascii
+
+import dns.rdata
+import dns.immutable
+import dns.rdatatype
+
+
+@dns.immutable.immutable
+class TLSABase(dns.rdata.Rdata):
+
+    """Base class for TLSA and SMIMEA records"""
+
+    # see: RFC 6698
+
+    __slots__ = ['usage', 'selector', 'mtype', 'cert']
+
+    def __init__(self, rdclass, rdtype, usage, selector,
+                 mtype, cert):
+        super().__init__(rdclass, rdtype)
+        self.usage = self._as_uint8(usage)
+        self.selector = self._as_uint8(selector)
+        self.mtype = self._as_uint8(mtype)
+        self.cert = self._as_bytes(cert)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        kw = kw.copy()
+        chunksize = kw.pop('chunksize', 128)
+        return '%d %d %d %s' % (self.usage,
+                                self.selector,
+                                self.mtype,
+                                dns.rdata._hexify(self.cert,
+                                                  chunksize=chunksize,
+                                                  **kw))
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        usage = tok.get_uint8()
+        selector = tok.get_uint8()
+        mtype = tok.get_uint8()
+        cert = tok.concatenate_remaining_identifiers().encode()
+        cert = binascii.unhexlify(cert)
+        return cls(rdclass, rdtype, usage, selector, mtype, cert)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        header = struct.pack("!BBB", self.usage, self.selector, self.mtype)
+        file.write(header)
+        file.write(self.cert)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        header = parser.get_struct("BBB")
+        cert = parser.get_remaining()
+        return cls(rdclass, rdtype, header[0], header[1], header[2], cert)

=== added file 'dns/rdtypes/txtbase.py'
--- old/dns/rdtypes/txtbase.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/txtbase.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,87 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""TXT-like base class."""
+
+import struct
+
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.tokenizer
+
+
+@dns.immutable.immutable
+class TXTBase(dns.rdata.Rdata):
+
+    """Base class for rdata that is like a TXT record (see RFC 1035)."""
+
+    __slots__ = ['strings']
+
+    def __init__(self, rdclass, rdtype, strings):
+        """Initialize a TXT-like rdata.
+
+        *rdclass*, an ``int`` is the rdataclass of the Rdata.
+
+        *rdtype*, an ``int`` is the rdatatype of the Rdata.
+
+        *strings*, a tuple of ``bytes``
+        """
+        super().__init__(rdclass, rdtype)
+        self.strings = self._as_tuple(strings,
+                                      lambda x: self._as_bytes(x, True, 255))
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        txt = ''
+        prefix = ''
+        for s in self.strings:
+            txt += '{}"{}"'.format(prefix, dns.rdata._escapify(s))
+            prefix = ' '
+        return txt
+
+    @classmethod
+    def from_text(cls, rdclass, rdtype, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        strings = []
+        for token in tok.get_remaining():
+            token = token.unescape_to_bytes()
+            # The 'if' below is always true in the current code, but we
+            # are leaving this check in in case things change some day.
+            if not (token.is_quoted_string() or
+                    token.is_identifier()):  # pragma: no cover
+                raise dns.exception.SyntaxError("expected a string")
+            if len(token.value) > 255:
+                raise dns.exception.SyntaxError("string too long")
+            strings.append(token.value)
+        if len(strings) == 0:
+            raise dns.exception.UnexpectedEnd
+        return cls(rdclass, rdtype, strings)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        for s in self.strings:
+            l = len(s)
+            assert l < 256
+            file.write(struct.pack('!B', l))
+            file.write(s)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        strings = []
+        while parser.remaining() > 0:
+            s = parser.get_counted_bytes()
+            strings.append(s)
+        return cls(rdclass, rdtype, strings)

=== added file 'dns/rdtypes/txtbase.pyi'
--- old/dns/rdtypes/txtbase.pyi	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/txtbase.pyi	2018-12-23 00:54:24 +0000
@@ -0,0 +1,6 @@
+from .. import rdata
+
+class TXTBase(rdata.Rdata):
+    ...
+class TXT(TXTBase):
+    ...

=== added file 'dns/rdtypes/util.py'
--- old/dns/rdtypes/util.py	1970-01-01 00:00:00 +0000
+++ new/dns/rdtypes/util.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,231 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import collections
+import random
+import struct
+
+import dns.exception
+import dns.ipv4
+import dns.ipv6
+import dns.name
+import dns.rdata
+
+
+class Gateway:
+    """A helper class for the IPSECKEY gateway and AMTRELAY relay fields"""
+    name = ""
+
+    def __init__(self, type, gateway=None):
+        self.type = dns.rdata.Rdata._as_uint8(type)
+        self.gateway = gateway
+        self._check()
+
+    @classmethod
+    def _invalid_type(cls, gateway_type):
+        return f"invalid {cls.name} type: {gateway_type}"
+
+    def _check(self):
+        if self.type == 0:
+            if self.gateway not in (".", None):
+                raise SyntaxError(f"invalid {self.name} for type 0")
+            self.gateway = None
+        elif self.type == 1:
+            # check that it's OK
+            dns.ipv4.inet_aton(self.gateway)
+        elif self.type == 2:
+            # check that it's OK
+            dns.ipv6.inet_aton(self.gateway)
+        elif self.type == 3:
+            if not isinstance(self.gateway, dns.name.Name):
+                raise SyntaxError(f"invalid {self.name}; not a name")
+        else:
+            raise SyntaxError(self._invalid_type(self.type))
+
+    def to_text(self, origin=None, relativize=True):
+        if self.type == 0:
+            return "."
+        elif self.type in (1, 2):
+            return self.gateway
+        elif self.type == 3:
+            return str(self.gateway.choose_relativity(origin, relativize))
+        else:
+            raise ValueError(self._invalid_type(self.type))  # pragma: no cover
+
+    @classmethod
+    def from_text(cls, gateway_type, tok, origin=None, relativize=True,
+                  relativize_to=None):
+        if gateway_type in (0, 1, 2):
+            gateway = tok.get_string()
+        elif gateway_type == 3:
+            gateway = tok.get_name(origin, relativize, relativize_to)
+        else:
+            raise dns.exception.SyntaxError(
+                cls._invalid_type(gateway_type))  # pragma: no cover
+        return cls(gateway_type, gateway)
+
+    # pylint: disable=unused-argument
+    def to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        if self.type == 0:
+            pass
+        elif self.type == 1:
+            file.write(dns.ipv4.inet_aton(self.gateway))
+        elif self.type == 2:
+            file.write(dns.ipv6.inet_aton(self.gateway))
+        elif self.type == 3:
+            self.gateway.to_wire(file, None, origin, False)
+        else:
+            raise ValueError(self._invalid_type(self.type))  # pragma: no cover
+    # pylint: enable=unused-argument
+
+    @classmethod
+    def from_wire_parser(cls, gateway_type, parser, origin=None):
+        if gateway_type == 0:
+            gateway = None
+        elif gateway_type == 1:
+            gateway = dns.ipv4.inet_ntoa(parser.get_bytes(4))
+        elif gateway_type == 2:
+            gateway = dns.ipv6.inet_ntoa(parser.get_bytes(16))
+        elif gateway_type == 3:
+            gateway = parser.get_name(origin)
+        else:
+            raise dns.exception.FormError(cls._invalid_type(gateway_type))
+        return cls(gateway_type, gateway)
+
+
+class Bitmap:
+    """A helper class for the NSEC/NSEC3/CSYNC type bitmaps"""
+    type_name = ""
+
+    def __init__(self, windows=None):
+        last_window = -1
+        self.windows = windows
+        for (window, bitmap) in self.windows:
+            if not isinstance(window, int):
+                raise ValueError(f"bad {self.type_name} window type")
+            if window <= last_window:
+                raise ValueError(f"bad {self.type_name} window order")
+            if window > 256:
+                raise ValueError(f"bad {self.type_name} window number")
+            last_window = window
+            if not isinstance(bitmap, bytes):
+                raise ValueError(f"bad {self.type_name} octets type")
+            if len(bitmap) == 0 or len(bitmap) > 32:
+                raise ValueError(f"bad {self.type_name} octets")
+
+    def to_text(self):
+        text = ""
+        for (window, bitmap) in self.windows:
+            bits = []
+            for (i, byte) in enumerate(bitmap):
+                for j in range(0, 8):
+                    if byte & (0x80 >> j):
+                        rdtype = window * 256 + i * 8 + j
+                        bits.append(dns.rdatatype.to_text(rdtype))
+            text += (' ' + ' '.join(bits))
+        return text
+
+    @classmethod
+    def from_text(cls, tok):
+        rdtypes = []
+        for token in tok.get_remaining():
+            rdtype = dns.rdatatype.from_text(token.unescape().value)
+            if rdtype == 0:
+                raise dns.exception.SyntaxError(f"{cls.type_name} with bit 0")
+            rdtypes.append(rdtype)
+        rdtypes.sort()
+        window = 0
+        octets = 0
+        prior_rdtype = 0
+        bitmap = bytearray(b'\0' * 32)
+        windows = []
+        for rdtype in rdtypes:
+            if rdtype == prior_rdtype:
+                continue
+            prior_rdtype = rdtype
+            new_window = rdtype // 256
+            if new_window != window:
+                if octets != 0:
+                    windows.append((window, bytes(bitmap[0:octets])))
+                bitmap = bytearray(b'\0' * 32)
+                window = new_window
+            offset = rdtype % 256
+            byte = offset // 8
+            bit = offset % 8
+            octets = byte + 1
+            bitmap[byte] = bitmap[byte] | (0x80 >> bit)
+        if octets != 0:
+            windows.append((window, bytes(bitmap[0:octets])))
+        return cls(windows)
+
+    def to_wire(self, file):
+        for (window, bitmap) in self.windows:
+            file.write(struct.pack('!BB', window, len(bitmap)))
+            file.write(bitmap)
+
+    @classmethod
+    def from_wire_parser(cls, parser):
+        windows = []
+        while parser.remaining() > 0:
+            window = parser.get_uint8()
+            bitmap = parser.get_counted_bytes()
+            windows.append((window, bitmap))
+        return cls(windows)
+
+
+def _priority_table(items):
+    by_priority = collections.defaultdict(list)
+    for rdata in items:
+        by_priority[rdata._processing_priority()].append(rdata)
+    return by_priority
+
+def priority_processing_order(iterable):
+    items = list(iterable)
+    if len(items) == 1:
+        return items
+    by_priority = _priority_table(items)
+    ordered = []
+    for k in sorted(by_priority.keys()):
+        rdatas = by_priority[k]
+        random.shuffle(rdatas)
+        ordered.extend(rdatas)
+    return ordered
+
+_no_weight = 0.1
+
+def weighted_processing_order(iterable):
+    items = list(iterable)
+    if len(items) == 1:
+        return items
+    by_priority = _priority_table(items)
+    ordered = []
+    for k in sorted(by_priority.keys()):
+        rdatas = by_priority[k]
+        total = sum(rdata._processing_weight() or _no_weight
+                    for rdata in rdatas)
+        while len(rdatas) > 1:
+            r = random.uniform(0, total)
+            for (n, rdata) in enumerate(rdatas):
+                weight = rdata._processing_weight() or _no_weight
+                if weight > r:
+                    break
+                r -= weight
+            total -= weight
+            ordered.append(rdata)
+            del rdatas[n]
+        ordered.append(rdatas[0])
+    return ordered

=== added file 'dns/renderer.py'
--- old/dns/renderer.py	1970-01-01 00:00:00 +0000
+++ new/dns/renderer.py	2020-08-06 23:26:37 +0000
@@ -0,0 +1,250 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2001-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""Help for building DNS wire format messages"""
+
+import contextlib
+import io
+import struct
+import random
+import time
+
+import dns.exception
+import dns.tsig
+
+
+QUESTION = 0
+ANSWER = 1
+AUTHORITY = 2
+ADDITIONAL = 3
+
+
+class Renderer:
+    """Helper class for building DNS wire-format messages.
+
+    Most applications can use the higher-level L{dns.message.Message}
+    class and its to_wire() method to generate wire-format messages.
+    This class is for those applications which need finer control
+    over the generation of messages.
+
+    Typical use::
+
+        r = dns.renderer.Renderer(id=1, flags=0x80, max_size=512)
+        r.add_question(qname, qtype, qclass)
+        r.add_rrset(dns.renderer.ANSWER, rrset_1)
+        r.add_rrset(dns.renderer.ANSWER, rrset_2)
+        r.add_rrset(dns.renderer.AUTHORITY, ns_rrset)
+        r.add_edns(0, 0, 4096)
+        r.add_rrset(dns.renderer.ADDTIONAL, ad_rrset_1)
+        r.add_rrset(dns.renderer.ADDTIONAL, ad_rrset_2)
+        r.write_header()
+        r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac)
+        wire = r.get_wire()
+
+    output, an io.BytesIO, where rendering is written
+
+    id: the message id
+
+    flags: the message flags
+
+    max_size: the maximum size of the message
+
+    origin: the origin to use when rendering relative names
+
+    compress: the compression table
+
+    section: an int, the section currently being rendered
+
+    counts: list of the number of RRs in each section
+
+    mac: the MAC of the rendered message (if TSIG was used)
+    """
+
+    def __init__(self, id=None, flags=0, max_size=65535, origin=None):
+        """Initialize a new renderer."""
+
+        self.output = io.BytesIO()
+        if id is None:
+            self.id = random.randint(0, 65535)
+        else:
+            self.id = id
+        self.flags = flags
+        self.max_size = max_size
+        self.origin = origin
+        self.compress = {}
+        self.section = QUESTION
+        self.counts = [0, 0, 0, 0]
+        self.output.write(b'\x00' * 12)
+        self.mac = ''
+
+    def _rollback(self, where):
+        """Truncate the output buffer at offset *where*, and remove any
+        compression table entries that pointed beyond the truncation
+        point.
+        """
+
+        self.output.seek(where)
+        self.output.truncate()
+        keys_to_delete = []
+        for k, v in self.compress.items():
+            if v >= where:
+                keys_to_delete.append(k)
+        for k in keys_to_delete:
+            del self.compress[k]
+
+    def _set_section(self, section):
+        """Set the renderer's current section.
+
+        Sections must be rendered order: QUESTION, ANSWER, AUTHORITY,
+        ADDITIONAL.  Sections may be empty.
+
+        Raises dns.exception.FormError if an attempt was made to set
+        a section value less than the current section.
+        """
+
+        if self.section != section:
+            if self.section > section:
+                raise dns.exception.FormError
+            self.section = section
+
+    @contextlib.contextmanager
+    def _track_size(self):
+        start = self.output.tell()
+        yield start
+        if self.output.tell() > self.max_size:
+            self._rollback(start)
+            raise dns.exception.TooBig
+
+    def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN):
+        """Add a question to the message."""
+
+        self._set_section(QUESTION)
+        with self._track_size():
+            qname.to_wire(self.output, self.compress, self.origin)
+            self.output.write(struct.pack("!HH", rdtype, rdclass))
+        self.counts[QUESTION] += 1
+
+    def add_rrset(self, section, rrset, **kw):
+        """Add the rrset to the specified section.
+
+        Any keyword arguments are passed on to the rdataset's to_wire()
+        routine.
+        """
+
+        self._set_section(section)
+        with self._track_size():
+            n = rrset.to_wire(self.output, self.compress, self.origin, **kw)
+        self.counts[section] += n
+
+    def add_rdataset(self, section, name, rdataset, **kw):
+        """Add the rdataset to the specified section, using the specified
+        name as the owner name.
+
+        Any keyword arguments are passed on to the rdataset's to_wire()
+        routine.
+        """
+
+        self._set_section(section)
+        with self._track_size():
+            n = rdataset.to_wire(name, self.output, self.compress, self.origin,
+                                 **kw)
+        self.counts[section] += n
+
+    def add_edns(self, edns, ednsflags, payload, options=None):
+        """Add an EDNS OPT record to the message."""
+
+        # make sure the EDNS version in ednsflags agrees with edns
+        ednsflags &= 0xFF00FFFF
+        ednsflags |= (edns << 16)
+        opt = dns.message.Message._make_opt(ednsflags, payload, options)
+        self.add_rrset(ADDITIONAL, opt)
+
+    def add_tsig(self, keyname, secret, fudge, id, tsig_error, other_data,
+                 request_mac, algorithm=dns.tsig.default_algorithm):
+        """Add a TSIG signature to the message."""
+
+        s = self.output.getvalue()
+
+        if isinstance(secret, dns.tsig.Key):
+            key = secret
+        else:
+            key = dns.tsig.Key(keyname, secret, algorithm)
+        tsig = dns.message.Message._make_tsig(keyname, algorithm, 0, fudge,
+                                              b'', id, tsig_error, other_data)
+        (tsig, _) = dns.tsig.sign(s, key, tsig[0], int(time.time()),
+                                  request_mac)
+        self._write_tsig(tsig, keyname)
+
+    def add_multi_tsig(self, ctx, keyname, secret, fudge, id, tsig_error,
+                       other_data, request_mac,
+                       algorithm=dns.tsig.default_algorithm):
+        """Add a TSIG signature to the message. Unlike add_tsig(), this can be
+        used for a series of consecutive DNS envelopes, e.g. for a zone
+        transfer over TCP [RFC2845, 4.4].
+
+        For the first message in the sequence, give ctx=None. For each
+        subsequent message, give the ctx that was returned from the
+        add_multi_tsig() call for the previous message."""
+
+        s = self.output.getvalue()
+
+        if isinstance(secret, dns.tsig.Key):
+            key = secret
+        else:
+            key = dns.tsig.Key(keyname, secret, algorithm)
+        tsig = dns.message.Message._make_tsig(keyname, algorithm, 0, fudge,
+                                              b'', id, tsig_error, other_data)
+        (tsig, ctx) = dns.tsig.sign(s, key, tsig[0], int(time.time()),
+                                    request_mac, ctx, True)
+        self._write_tsig(tsig, keyname)
+        return ctx
+
+    def _write_tsig(self, tsig, keyname):
+        self._set_section(ADDITIONAL)
+        with self._track_size():
+            keyname.to_wire(self.output, self.compress, self.origin)
+            self.output.write(struct.pack('!HHIH', dns.rdatatype.TSIG,
+                                          dns.rdataclass.ANY, 0, 0))
+            rdata_start = self.output.tell()
+            tsig.to_wire(self.output)
+
+        after = self.output.tell()
+        self.output.seek(rdata_start - 2)
+        self.output.write(struct.pack('!H', after - rdata_start))
+        self.counts[ADDITIONAL] += 1
+        self.output.seek(10)
+        self.output.write(struct.pack('!H', self.counts[ADDITIONAL]))
+        self.output.seek(0, io.SEEK_END)
+
+    def write_header(self):
+        """Write the DNS message header.
+
+        Writing the DNS message header is done after all sections
+        have been rendered, but before the optional TSIG signature
+        is added.
+        """
+
+        self.output.seek(0)
+        self.output.write(struct.pack('!HHHHHH', self.id, self.flags,
+                                      self.counts[0], self.counts[1],
+                                      self.counts[2], self.counts[3]))
+        self.output.seek(0, io.SEEK_END)
+
+    def get_wire(self):
+        """Return the wire format message."""
+
+        return self.output.getvalue()

=== added file 'dns/resolver.py'
--- old/dns/resolver.py	1970-01-01 00:00:00 +0000
+++ new/dns/resolver.py	2021-04-07 01:51:59 +0000
@@ -0,0 +1,1649 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+# Copyright (C) 2003-2017 Nominum, Inc.
+#
+# Permission to use, copy, modify, and distribute this software and its
+# documentation for any purpose with or without fee is hereby granted,
+# provided that the above copyright notice and this permission notice
+# appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""DNS stub resolver."""
+from urllib.parse import urlparse
+import contextlib
+import socket
+import sys
+import time
+import random
+import warnings
+try:
+    import threading as _threading
+except ImportError:  # pragma: no cover
+    import dummy_threading as _threading    # type: ignore
+
+import dns.exception
+import dns.flags
+import dns.inet
+import dns.ipv4
+import dns.ipv6
+import dns.message
+import dns.name
+import dns.query
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.reversename
+import dns.tsig
+
+if sys.platform == 'win32':
+    # pylint: disable=import-error
+    import winreg
+
+class NXDOMAIN(dns.exception.DNSException):
+    """The DNS query name does not exist."""
+    supp_kwargs = {'qnames', 'responses'}
+    fmt = None  # we have our own __str__ implementation
+
+    # pylint: disable=arguments-differ
+
+    def _check_kwargs(self, qnames,
+                      responses=None):
+        if not isinstance(qnames, (list, tuple, set)):
+            raise AttributeError("qnames must be a list, tuple or set")
+        if len(qnames) == 0:
+            raise AttributeError("qnames must contain at least one element")
+        if responses is None:
+            responses = {}
+        elif not isinstance(responses, dict):
+            raise AttributeError("responses must be a dict(qname=response)")
+        kwargs = dict(qnames=qnames, responses=responses)
+        return kwargs
+
+    def __str__(self):
+        if 'qnames' not in self.kwargs:
+            return super().__str__()
+        qnames = self.kwargs['qnames']
+        if len(qnames) > 1:
+            msg = 'None of DNS query names exist'
+        else:
+            msg = 'The DNS query name does not exist'
+        qnames = ', '.join(map(str, qnames))
+        return "{}: {}".format(msg, qnames)
+
+    @property
+    def canonical_name(self):
+        """Return the unresolved canonical name."""
+        if 'qnames' not in self.kwargs:
+            raise TypeError("parametrized exception required")
+        for qname in self.kwargs['qnames']:
+            response = self.kwargs['responses'][qname]
+            try:
+                cname = response.canonical_name()
+                if cname != qname:
+                    return cname
+            except Exception:
+                # We can just eat this exception as it means there was
+                # something wrong with the response.
+                pass
+        return self.kwargs['qnames'][0]
+
+    def __add__(self, e_nx):
+        """Augment by results from another NXDOMAIN exception."""
+        qnames0 = list(self.kwargs.get('qnames', []))
+        responses0 = dict(self.kwargs.get('responses', {}))
+        responses1 = e_nx.kwargs.get('responses', {})
+        for qname1 in e_nx.kwargs.get('qnames', []):
+            if qname1 not in qnames0:
+                qnames0.append(qname1)
+            if qname1 in responses1:
+                responses0[qname1] = responses1[qname1]
+        return NXDOMAIN(qnames=qnames0, responses=responses0)
+
+    def qnames(self):
+        """All of the names that were tried.
+
+        Returns a list of ``dns.name.Name``.
+        """
+        return self.kwargs['qnames']
+
+    def responses(self):
+        """A map from queried names to their NXDOMAIN responses.
+
+        Returns a dict mapping a ``dns.name.Name`` to a
+        ``dns.message.Message``.
+        """
+        return self.kwargs['responses']
+
+    def response(self, qname):
+        """The response for query *qname*.
+
+        Returns a ``dns.message.Message``.
+        """
+        return self.kwargs['responses'][qname]
+
+
+class YXDOMAIN(dns.exception.DNSException):
+    """The DNS query name is too long after DNAME substitution."""
+
+# The definition of the Timeout exception has moved from here to the
+# dns.exception module.  We keep dns.resolver.Timeout defined for
+# backwards compatibility.
+
+Timeout = dns.exception.Timeout
+
+
+class NoAnswer(dns.exception.DNSException):
+    """The DNS response does not contain an answer to the question."""
+    fmt = 'The DNS response does not contain an answer ' + \
+          'to the question: {query}'
+    supp_kwargs = {'response'}
+
+    def _fmt_kwargs(self, **kwargs):
+        return super()._fmt_kwargs(query=kwargs['response'].question)
+
+
+class NoNameservers(dns.exception.DNSException):
+    """All nameservers failed to answer the query.
+
+    errors: list of servers and respective errors
+    The type of errors is
+    [(server IP address, any object convertible to string)].
+    Non-empty errors list will add explanatory message ()
+    """
+
+    msg = "All nameservers failed to answer the query."
+    fmt = "%s {query}: {errors}" % msg[:-1]
+    supp_kwargs = {'request', 'errors'}
+
+    def _fmt_kwargs(self, **kwargs):
+        srv_msgs = []
+        for err in kwargs['errors']:
+            # pylint: disable=bad-continuation
+            srv_msgs.append('Server {} {} port {} answered {}'.format(err[0],
+                            'TCP' if err[1] else 'UDP', err[2], err[3]))
+        return super()._fmt_kwargs(query=kwargs['request'].question,
+                                   errors='; '.join(srv_msgs))
+
+
+class NotAbsolute(dns.exception.DNSException):
+    """An absolute domain name is required but a relative name was provided."""
+
+
+class NoRootSOA(dns.exception.DNSException):
+    """There is no SOA RR at the DNS root name. This should never happen!"""
+
+
+class NoMetaqueries(dns.exception.DNSException):
+    """DNS metaqueries are not allowed."""
+
+class NoResolverConfiguration(dns.exception.DNSException):
+    """Resolver configuration could not be read or specified no nameservers."""
+
+class Answer:
+    """DNS stub resolver answer.
+
+    Instances of this class bundle up the result of a successful DNS
+    resolution.
+
+    For convenience, the answer object implements much of the sequence
+    protocol, forwarding to its ``rrset`` attribute.  E.g.
+    ``for a in answer`` is equivalent to ``for a in answer.rrset``.
+    ``answer[i]`` is equivalent to ``answer.rrset[i]``, and
+    ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``.
+
+    Note that CNAMEs or DNAMEs in the response may mean that answer
+    RRset's name might not be the query name.
+    """
+
+    def __init__(self, qname, rdtype, rdclass, response, nameserver=None,
+                 port=None):
+        self.qname = qname
+        self.rdtype = rdtype
+        self.rdclass = rdclass
+        self.response = response
+        self.nameserver = nameserver
+        self.port = port
+        self.chaining_result = response.resolve_chaining()
+        # Copy some attributes out of chaining_result for backwards
+        # compatibility and convenience.
+        self.canonical_name = self.chaining_result.canonical_name
+        self.rrset = self.chaining_result.answer
+        self.expiration = time.time() + self.chaining_result.minimum_ttl
+
+    def __getattr__(self, attr):  # pragma: no cover
+        if attr == 'name':
+            return self.rrset.name
+        elif attr == 'ttl':
+            return self.rrset.ttl
+        elif attr == 'covers':
+            return self.rrset.covers
+        elif attr == 'rdclass':
+            return self.rrset.rdclass
+        elif attr == 'rdtype':
+            return self.rrset.rdtype
+        else:
+            raise AttributeError(attr)
+
+    def __len__(self):
+        return self.rrset and len(self.rrset) or 0
+
+    def __iter__(self):
+        return self.rrset and iter(self.rrset) or iter(tuple())
+
+    def __getitem__(self, i):
+        if self.rrset is None:
+            raise IndexError
+        return self.rrset[i]
+
+    def __delitem__(self, i):
+        if self.rrset is None:
+            raise IndexError
+        del self.rrset[i]
+
+
+class CacheStatistics:
+    """Cache Statistics
+    """
+
+    def __init__(self, hits=0, misses=0):
+        self.hits = hits
+        self.misses = misses
+
+    def reset(self):
+        self.hits = 0
+        self.misses = 0
+
+    def clone(self):
+        return CacheStatistics(self.hits, self.misses)
+
+
+class CacheBase:
+    def __init__(self):
+        self.lock = _threading.Lock()
+        self.statistics = CacheStatistics()
+
+    def reset_statistics(self):
+        """Reset all statistics to zero."""
+        with self.lock:
+            self.statistics.reset()
+
+    def hits(self):
+        """How many hits has the cache had?"""
+        with self.lock:
+            return self.statistics.hits
+
+    def misses(self):
+        """How many misses has the cache had?"""
+        with self.lock:
+            return self.statistics.misses
+
+    def get_statistics_snapshot(self):
+        """Return a consistent snapshot of all the statistics.
+
+        If running with multiple threads, it's better to take a
+        snapshot than to call statistics methods such as hits() and
+        misses() individually.
+        """
+        with self.lock:
+            return self.statistics.clone()
+
+
+class Cache(CacheBase):
+    """Simple thread-safe DNS answer cache."""
+
+    def __init__(self, cleaning_interval=300.0):
+        """*cleaning_interval*, a ``float`` is the number of seconds between
+        periodic cleanings.
+        """
+
+        super().__init__()
+        self.data = {}
+        self.cleaning_interval = cleaning_interval
+        self.next_cleaning = time.time() + self.cleaning_interval
+
+    def _maybe_clean(self):
+        """Clean the cache if it's time to do so."""
+
+        now = time.time()
+        if self.next_cleaning <= now:
+            keys_to_delete = []
+            for (k, v) in self.data.items():
+                if v.expiration <= now:
+                    keys_to_delete.append(k)
+            for k in keys_to_delete:
+                del self.data[k]
+            now = time.time()
+            self.next_cleaning = now + self.cleaning_interval
+
+    def get(self, key):
+        """Get the answer associated with *key*.
+
+        Returns None if no answer is cached for the key.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+
+        Returns a ``dns.resolver.Answer`` or ``None``.
+        """
+
+        with self.lock:
+            self._maybe_clean()
+            v = self.data.get(key)
+            if v is None or v.expiration <= time.time():
+                self.statistics.misses += 1
+                return None
+            self.statistics.hits += 1
+            return v
+
+    def put(self, key, value):
+        """Associate key and value in the cache.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+
+        *value*, a ``dns.resolver.Answer``, the answer.
+        """
+
+        with self.lock:
+            self._maybe_clean()
+            self.data[key] = value
+
+    def flush(self, key=None):
+        """Flush the cache.
+
+        If *key* is not ``None``, only that item is flushed.  Otherwise
+        the entire cache is flushed.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+        """
+
+        with self.lock:
+            if key is not None:
+                if key in self.data:
+                    del self.data[key]
+            else:
+                self.data = {}
+                self.next_cleaning = time.time() + self.cleaning_interval
+
+
+class LRUCacheNode:
+    """LRUCache node."""
+
+    def __init__(self, key, value):
+        self.key = key
+        self.value = value
+        self.hits = 0
+        self.prev = self
+        self.next = self
+
+    def link_after(self, node):
+        self.prev = node
+        self.next = node.next
+        node.next.prev = self
+        node.next = self
+
+    def unlink(self):
+        self.next.prev = self.prev
+        self.prev.next = self.next
+
+
+class LRUCache(CacheBase):
+    """Thread-safe, bounded, least-recently-used DNS answer cache.
+
+    This cache is better than the simple cache (above) if you're
+    running a web crawler or other process that does a lot of
+    resolutions.  The LRUCache has a maximum number of nodes, and when
+    it is full, the least-recently used node is removed to make space
+    for a new one.
+    """
+
+    def __init__(self, max_size=100000):
+        """*max_size*, an ``int``, is the maximum number of nodes to cache;
+        it must be greater than 0.
+        """
+
+        super().__init__()
+        self.data = {}
+        self.set_max_size(max_size)
+        self.sentinel = LRUCacheNode(None, None)
+        self.sentinel.prev = self.sentinel
+        self.sentinel.next = self.sentinel
+
+    def set_max_size(self, max_size):
+        if max_size < 1:
+            max_size = 1
+        self.max_size = max_size
+
+    def get(self, key):
+        """Get the answer associated with *key*.
+
+        Returns None if no answer is cached for the key.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+
+        Returns a ``dns.resolver.Answer`` or ``None``.
+        """
+
+        with self.lock:
+            node = self.data.get(key)
+            if node is None:
+                self.statistics.misses += 1
+                return None
+            # Unlink because we're either going to move the node to the front
+            # of the LRU list or we're going to free it.
+            node.unlink()
+            if node.value.expiration <= time.time():
+                del self.data[node.key]
+                self.statistics.misses += 1
+                return None
+            node.link_after(self.sentinel)
+            self.statistics.hits += 1
+            node.hits += 1
+            return node.value
+
+    def get_hits_for_key(self, key):
+        """Return the number of cache hits associated with the specified key."""
+        with self.lock:
+            node = self.data.get(key)
+            if node is None or node.value.expiration <= time.time():
+                return 0
+            else:
+                return node.hits
+
+    def put(self, key, value):
+        """Associate key and value in the cache.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+
+        *value*, a ``dns.resolver.Answer``, the answer.
+        """
+
+        with self.lock:
+            node = self.data.get(key)
+            if node is not None:
+                node.unlink()
+                del self.data[node.key]
+            while len(self.data) >= self.max_size:
+                node = self.sentinel.prev
+                node.unlink()
+                del self.data[node.key]
+            node = LRUCacheNode(key, value)
+            node.link_after(self.sentinel)
+            self.data[key] = node
+
+    def flush(self, key=None):
+        """Flush the cache.
+
+        If *key* is not ``None``, only that item is flushed.  Otherwise
+        the entire cache is flushed.
+
+        *key*, a ``(dns.name.Name, int, int)`` tuple whose values are the
+        query name, rdtype, and rdclass respectively.
+        """
+
+        with self.lock:
+            if key is not None:
+                node = self.data.get(key)
+                if node is not None:
+                    node.unlink()
+                    del self.data[node.key]
+            else:
+                node = self.sentinel.next
+                while node != self.sentinel:
+                    next = node.next
+                    node.unlink()
+                    node = next
+                self.data = {}
+
+class _Resolution:
+    """Helper class for dns.resolver.Resolver.resolve().
+
+    All of the "business logic" of resolution is encapsulated in this
+    class, allowing us to have multiple resolve() implementations
+    using different I/O schemes without copying all of the
+    complicated logic.
+
+    This class is a "friend" to dns.resolver.Resolver and manipulates
+    resolver data structures directly.
+    """
+
+    def __init__(self, resolver, qname, rdtype, rdclass, tcp,
+                 raise_on_no_answer, search):
+        if isinstance(qname, str):
+            qname = dns.name.from_text(qname, None)
+        rdtype = dns.rdatatype.RdataType.make(rdtype)
+        if dns.rdatatype.is_metatype(rdtype):
+            raise NoMetaqueries
+        rdclass = dns.rdataclass.RdataClass.make(rdclass)
+        if dns.rdataclass.is_metaclass(rdclass):
+            raise NoMetaqueries
+        self.resolver = resolver
+        self.qnames_to_try = resolver._get_qnames_to_try(qname, search)
+        self.qnames = self.qnames_to_try[:]
+        self.rdtype = rdtype
+        self.rdclass = rdclass
+        self.tcp = tcp
+        self.raise_on_no_answer = raise_on_no_answer
+        self.nxdomain_responses = {}
+        #
+        # Initialize other things to help analysis tools
+        self.qname = dns.name.empty
+        self.nameservers = []
+        self.current_nameservers = []
+        self.errors = []
+        self.nameserver = None
+        self.port = 0
+        self.tcp_attempt = False
+        self.retry_with_tcp = False
+        self.request = None
+        self.backoff = 0
+
+    def next_request(self):
+        """Get the next request to send, and check the cache.
+
+        Returns a (request, answer) tuple.  At most one of request or
+        answer will not be None.
+        """
+
+        # We return a tuple instead of Union[Message,Answer] as it lets
+        # the caller avoid isinstance().
+
+        while len(self.qnames) > 0:
+            self.qname = self.qnames.pop(0)
+
+            # Do we know the answer?
+            if self.resolver.cache:
+                answer = self.resolver.cache.get((self.qname, self.rdtype,
+                                                  self.rdclass))
+                if answer is not None:
+                    if answer.rrset is None and self.raise_on_no_answer:
+                        raise NoAnswer(response=answer.response)
+                    else:
+                        return (None, answer)
+                answer = self.resolver.cache.get((self.qname,
+                                                  dns.rdatatype.ANY,
+                                                  self.rdclass))
+                if answer is not None and \
+                   answer.response.rcode() == dns.rcode.NXDOMAIN:
+                    # cached NXDOMAIN; record it and continue to next
+                    # name.
+                    self.nxdomain_responses[self.qname] = answer.response
+                    continue
+
+            # Build the request
+            request = dns.message.make_query(self.qname, self.rdtype,
+                                             self.rdclass)
+            if self.resolver.keyname is not None:
+                request.use_tsig(self.resolver.keyring, self.resolver.keyname,
+                                 algorithm=self.resolver.keyalgorithm)
+            request.use_edns(self.resolver.edns, self.resolver.ednsflags,
+                             self.resolver.payload)
+            if self.resolver.flags is not None:
+                request.flags = self.resolver.flags
+
+            self.nameservers = self.resolver.nameservers[:]
+            if self.resolver.rotate:
+                random.shuffle(self.nameservers)
+            self.current_nameservers = self.nameservers[:]
+            self.errors = []
+            self.nameserver = None
+            self.tcp_attempt = False
+            self.retry_with_tcp = False
+            self.request = request
+            self.backoff = 0.10
+
+            return (request, None)
+
+        #
+        # We've tried everything and only gotten NXDOMAINs.  (We know
+        # it's only NXDOMAINs as anything else would have returned
+        # before now.)
+        #
+        raise NXDOMAIN(qnames=self.qnames_to_try,
+                       responses=self.nxdomain_responses)
+
+    def next_nameserver(self):
+        if self.retry_with_tcp:
+            assert self.nameserver is not None
+            self.tcp_attempt = True
+            self.retry_with_tcp = False
+            return (self.nameserver, self.port, True, 0)
+
+        backoff = 0
+        if not self.current_nameservers:
+            if len(self.nameservers) == 0:
+                # Out of things to try!
+                raise NoNameservers(request=self.request, errors=self.errors)
+            self.current_nameservers = self.nameservers[:]
+            backoff = self.backoff
+            self.backoff = min(self.backoff * 2, 2)
+
+        self.nameserver = self.current_nameservers.pop(0)
+        self.port = self.resolver.nameserver_ports.get(self.nameserver,
+                                                       self.resolver.port)
+        self.tcp_attempt = self.tcp
+        return (self.nameserver, self.port, self.tcp_attempt, backoff)
+
+    def query_result(self, response, ex):
+        #
+        # returns an (answer: Answer, end_loop: bool) tuple.
+        #
+        if ex:
+            # Exception during I/O or from_wire()
+            assert response is None
+            self.errors.append((self.nameserver, self.tcp_attempt, self.port,
+                                ex, response))
+            if isinstance(ex, dns.exception.FormError) or \
+               isinstance(ex, EOFError) or \
+               isinstance(ex, OSError) or \
+               isinstance(ex, NotImplementedError):
+                # This nameserver is no good, take it out of the mix.
+                self.nameservers.remove(self.nameserver)
+            elif isinstance(ex, dns.message.Truncated):
+                if self.tcp_attempt:
+                    # Truncation with TCP is no good!
+                    self.nameservers.remove(self.nameserver)
+                else:
+                    self.retry_with_tcp = True
+            return (None, False)
+        # We got an answer!
+        assert response is not None
+        rcode = response.rcode()
+        if rcode == dns.rcode.NOERROR:
+            try:
+                answer = Answer(self.qname, self.rdtype, self.rdclass, response,
+                                self.nameserver, self.port)
+            except Exception:
+                # The nameserver is no good, take it out of the mix.
+                self.nameservers.remove(self.nameserver)
+                return (None, False)
+            if self.resolver.cache:
+                self.resolver.cache.put((self.qname, self.rdtype,
+                                         self.rdclass), answer)
+            if answer.rrset is None and self.raise_on_no_answer:
+                raise NoAnswer(response=answer.response)
+            return (answer, True)
+        elif rcode == dns.rcode.NXDOMAIN:
+            # Further validate the response by making an Answer, even
+            # if we aren't going to cache it.
+            try:
+                answer = Answer(self.qname, dns.rdatatype.ANY,
+                                dns.rdataclass.IN, response)
+            except Exception:
+                # The nameserver is no good, take it out of the mix.
+                self.nameservers.remove(self.nameserver)
+                return (None, False)
+            self.nxdomain_responses[self.qname] = response
+            if self.resolver.cache:
+                self.resolver.cache.put((self.qname,
+                                         dns.rdatatype.ANY,
+                                         self.rdclass), answer)
+            # Make next_nameserver() return None, so caller breaks its
+            # inner loop and calls next_request().
+            return (None, True)
+        elif rcode == dns.rcode.YXDOMAIN:
+            yex = YXDOMAIN()
+            self.errors.append((self.nameserver, self.tcp_attempt,
+                                self.port, yex, response))
+            raise yex
+        else:
+            #
+            # We got a response, but we're not happy with the
+            # rcode in it.
+            #
+            if rcode != dns.rcode.SERVFAIL or not self.resolver.retry_servfail:
+                self.nameservers.remove(self.nameserver)
+            self.errors.append((self.nameserver, self.tcp_attempt, self.port,
+                                dns.rcode.to_text(rcode), response))
+            return (None, False)
+
+class BaseResolver:
+    """DNS stub resolver."""
+
+    # We initialize in reset()
+    #
+    # pylint: disable=attribute-defined-outside-init
+
+    def __init__(self, filename='/etc/resolv.conf', configure=True):
+        """*filename*, a ``str`` or file object, specifying a file
+        in standard /etc/resolv.conf format.  This parameter is meaningful
+        only when *configure* is true and the platform is POSIX.
+
+        *configure*, a ``bool``.  If True (the default), the resolver
+        instance is configured in the normal fashion for the operating
+        system the resolver is running on.  (I.e. by reading a
+        /etc/resolv.conf file on POSIX systems and from the registry
+        on Windows systems.)
+        """
+
+        self.reset()
+        if configure:
+            if sys.platform == 'win32':
+                self.read_registry()
+            elif filename:
+                self.read_resolv_conf(filename)
+
+    def reset(self):
+        """Reset all resolver configuration to the defaults."""
+
+        self.domain = \
+            dns.name.Name(dns.name.from_text(socket.gethostname())[1:])
+        if len(self.domain) == 0:
+            self.domain = dns.name.root
+        self.nameservers = []
+        self.nameserver_ports = {}
+        self.port = 53
+        self.search = []
+        self.use_search_by_default = False
+        self.timeout = 2.0
+        self.lifetime = 5.0
+        self.keyring = None
+        self.keyname = None
+        self.keyalgorithm = dns.tsig.default_algorithm
+        self.edns = -1
+        self.ednsflags = 0
+        self.payload = 0
+        self.cache = None
+        self.flags = None
+        self.retry_servfail = False
+        self.rotate = False
+        self.ndots = None
+
+    def read_resolv_conf(self, f):
+        """Process *f* as a file in the /etc/resolv.conf format.  If f is
+        a ``str``, it is used as the name of the file to open; otherwise it
+        is treated as the file itself.
+
+        Interprets the following items:
+
+        - nameserver - name server IP address
+
+        - domain - local domain name
+
+        - search - search list for host-name lookup
+
+        - options - supported options are rotate, timeout, edns0, and ndots
+
+        """
+
+        with contextlib.ExitStack() as stack:
+            if isinstance(f, str):
+                try:
+                    f = stack.enter_context(open(f))
+                except OSError:
+                    # /etc/resolv.conf doesn't exist, can't be read, etc.
+                    raise NoResolverConfiguration
+
+            for l in f:
+                if len(l) == 0 or l[0] == '#' or l[0] == ';':
+                    continue
+                tokens = l.split()
+
+                # Any line containing less than 2 tokens is malformed
+                if len(tokens) < 2:
+                    continue
+
+                if tokens[0] == 'nameserver':
+                    self.nameservers.append(tokens[1])
+                elif tokens[0] == 'domain':
+                    self.domain = dns.name.from_text(tokens[1])
+                    # domain and search are exclusive
+                    self.search = []
+                elif tokens[0] == 'search':
+                    # the last search wins
+                    self.search = []
+                    for suffix in tokens[1:]:
+                        self.search.append(dns.name.from_text(suffix))
+                    # We don't set domain as it is not used if
+                    # len(self.search) > 0
+                elif tokens[0] == 'options':
+                    for opt in tokens[1:]:
+                        if opt == 'rotate':
+                            self.rotate = True
+                        elif opt == 'edns0':
+                            self.use_edns()
+                        elif 'timeout' in opt:
+                            try:
+                                self.timeout = int(opt.split(':')[1])
+                            except (ValueError, IndexError):
+                                pass
+                        elif 'ndots' in opt:
+                            try:
+                                self.ndots = int(opt.split(':')[1])
+                            except (ValueError, IndexError):
+                                pass
+        if len(self.nameservers) == 0:
+            raise NoResolverConfiguration
+
+    def _determine_split_char(self, entry):
+        #
+        # The windows registry irritatingly changes the list element
+        # delimiter in between ' ' and ',' (and vice-versa) in various
+        # versions of windows.
+        #
+        if entry.find(' ') >= 0:  # pragma: no cover
+            split_char = ' '
+        elif entry.find(',') >= 0:  # pragma: no cover
+            split_char = ','
+        else:
+            # probably a singleton; treat as a space-separated list.
+            split_char = ' '
+        return split_char
+
+    def _config_win32_nameservers(self, nameservers):
+        # we call str() on nameservers to convert it from unicode to ascii
+        nameservers = str(nameservers)
+        split_char = self._determine_split_char(nameservers)
+        ns_list = nameservers.split(split_char)
+        for ns in ns_list:
+            if ns not in self.nameservers:
+                self.nameservers.append(ns)
+
+    def _config_win32_domain(self, domain):  # pragma: no cover
+        # we call str() on domain to convert it from unicode to ascii
+        self.domain = dns.name.from_text(str(domain))
+
+    def _config_win32_search(self, search):  # pragma: no cover
+        # we call str() on search to convert it from unicode to ascii
+        search = str(search)
+        split_char = self._determine_split_char(search)
+        search_list = search.split(split_char)
+        for s in search_list:
+            if s not in self.search:
+                self.search.append(dns.name.from_text(s))
+
+    def _config_win32_fromkey(self, key, always_try_domain):
+        # pylint: disable=undefined-variable
+        # (disabled for WindowsError)
+        try:
+            servers, _ = winreg.QueryValueEx(key, 'NameServer')
+        except WindowsError:  # pragma: no cover
+            servers = None
+        if servers:
+            self._config_win32_nameservers(servers)
+        if servers or always_try_domain:
+            try:
+                dom, _ = winreg.QueryValueEx(key, 'Domain')
+                if dom:
+                    self._config_win32_domain(dom)  # pragma: no cover
+            except WindowsError:  # pragma: no cover
+                pass
+        else:
+            try:
+                servers, _ = winreg.QueryValueEx(key, 'DhcpNameServer')
+            except WindowsError:  # pragma: no cover
+                servers = None
+            if servers:
+                self._config_win32_nameservers(servers)
+                try:
+                    dom, _ = winreg.QueryValueEx(key, 'DhcpDomain')
+                    if dom:
+                        self._config_win32_domain(dom)
+                except WindowsError:  # pragma: no cover
+                    pass
+        try:
+            search, _ = winreg.QueryValueEx(key, 'SearchList')
+        except WindowsError:  # pragma: no cover
+            search = None
+        if search:  # pragma: no cover
+            self._config_win32_search(search)
+
+    def read_registry(self):
+        """Extract resolver configuration from the Windows registry."""
+
+        lm = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+        try:
+            tcp_params = winreg.OpenKey(lm,
+                                        r'SYSTEM\CurrentControlSet'
+                                        r'\Services\Tcpip\Parameters')
+            try:
+                self._config_win32_fromkey(tcp_params, True)
+            finally:
+                tcp_params.Close()
+            interfaces = winreg.OpenKey(lm,
+                                        r'SYSTEM\CurrentControlSet'
+                                        r'\Services\Tcpip\Parameters'
+                                        r'\Interfaces')
+            try:
+                i = 0
+                while True:
+                    try:
+                        guid = winreg.EnumKey(interfaces, i)
+                        i += 1
+                        # XXXRTH why do we get this key and then not use it?
+                        key = winreg.OpenKey(interfaces, guid)
+                        if not self._win32_is_nic_enabled(lm, guid, key):
+                            continue
+                        try:
+                            self._config_win32_fromkey(key, False)
+                        finally:
+                            key.Close()
+                    except EnvironmentError:  # pragma: no cover
+                        break
+            finally:
+                interfaces.Close()
+        finally:
+            lm.Close()
+
+    def _win32_is_nic_enabled(self, lm, guid, _):
+        # Look in the Windows Registry to determine whether the network
+        # interface corresponding to the given guid is enabled.
+        #
+        # (Code contributed by Paul Marks, thanks!)
+        #
+        try:
+            # This hard-coded location seems to be consistent, at least
+            # from Windows 2000 through Vista.
+            connection_key = winreg.OpenKey(
+                lm,
+                r'SYSTEM\CurrentControlSet\Control\Network'
+                r'\{4D36E972-E325-11CE-BFC1-08002BE10318}'
+                r'\%s\Connection' % guid)
+
+            try:
+                # The PnpInstanceID points to a key inside Enum
+                (pnp_id, ttype) = winreg.QueryValueEx(
+                    connection_key, 'PnpInstanceID')
+
+                if ttype != winreg.REG_SZ:
+                    raise ValueError  # pragma: no cover
+
+                device_key = winreg.OpenKey(
+                    lm, r'SYSTEM\CurrentControlSet\Enum\%s' % pnp_id)
+
+                try:
+                    # Get ConfigFlags for this device
+                    (flags, ttype) = winreg.QueryValueEx(
+                        device_key, 'ConfigFlags')
+
+                    if ttype != winreg.REG_DWORD:
+                        raise ValueError  # pragma: no cover
+
+                    # Based on experimentation, bit 0x1 indicates that the
+                    # device is disabled.
+                    return not flags & 0x1
+
+                finally:
+                    device_key.Close()
+            finally:
+                connection_key.Close()
+        except Exception:  # pragma: no cover
+            return False
+
+    def _compute_timeout(self, start, lifetime=None):
+        lifetime = self.lifetime if lifetime is None else lifetime
+        now = time.time()
+        duration = now - start
+        if duration < 0:
+            if duration < -1:
+                # Time going backwards is bad.  Just give up.
+                raise Timeout(timeout=duration)
+            else:
+                # Time went backwards, but only a little.  This can
+                # happen, e.g. under vmware with older linux kernels.
+                # Pretend it didn't happen.
+                now = start
+        if duration >= lifetime:
+            raise Timeout(timeout=duration)
+        return min(lifetime - duration, self.timeout)
+
+    def _get_qnames_to_try(self, qname, search):
+        # This is a separate method so we can unit test the search
+        # rules without requiring the Internet.
+        if search is None:
+            search = self.use_search_by_default
+        qnames_to_try = []
+        if qname.is_absolute():
+            qnames_to_try.append(qname)
+        else:
+            abs_qname = qname.concatenate(dns.name.root)
+            if search:
+                if len(self.search) > 0:
+                    # There is a search list, so use it exclusively
+                    search_list = self.search[:]
+                elif self.domain != dns.name.root and self.domain is not None:
+                    # We have some notion of a domain that isn't the root,