zeroconf 0.149.4__tar.gz → 0.149.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of zeroconf might be problematic. Click here for more details.
- {zeroconf-0.149.4 → zeroconf-0.149.5}/CHANGELOG.md +31 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/PKG-INFO +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/pyproject.toml +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/setup.py +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/__init__.py +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_protocol/incoming.pxd +1 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_protocol/incoming.py +10 -4
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/__init__.py +14 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/conftest.py +19 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/services/test_info.py +84 -77
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/services/test_types.py +11 -9
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_asyncio.py +17 -9
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_handlers.py +5 -1
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_protocol.py +22 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/COPYING +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/README.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/build_ext.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/docs/Makefile +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/docs/_ext/zeroconfautodocfix.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/docs/api.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/docs/conf.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/docs/index.rst +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_cache.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_core.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_dns.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_dns.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_engine.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_exceptions.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/answers.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/answers.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/query_handler.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/query_handler.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/record_manager.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_handlers/record_manager.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_history.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_history.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_listener.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_listener.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_logger.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_protocol/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_protocol/outgoing.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_protocol/outgoing.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_record_update.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_record_update.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/__init__.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/browser.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/browser.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/info.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/info.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/registry.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/registry.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_services/types.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_transport.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_updates.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_updates.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/ipaddress.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/ipaddress.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/name.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/net.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/time.pxd +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/_utils/time.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/const.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/src/zeroconf/py.typed +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/helpers.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/test_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/test_incoming.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/test_outgoing.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/test_send.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/benchmarks/test_txt_properties.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/services/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/services/test_browser.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/services/test_registry.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_cache.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_circular_imports.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_core.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_dns.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_engine.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_exceptions.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_history.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_init.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_listener.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_logger.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_services.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/test_updates.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/utils/__init__.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/utils/test_asyncio.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/utils/test_ipaddress.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/utils/test_name.py +0 -0
- {zeroconf-0.149.4 → zeroconf-0.149.5}/tests/utils/test_net.py +0 -0
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.149.5 (2026-05-18)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Bound DNS compression-pointer chain depth in DNSIncoming
|
|
10
|
+
([#1719](https://github.com/python-zeroconf/python-zeroconf/pull/1719),
|
|
11
|
+
[`f9e2359`](https://github.com/python-zeroconf/python-zeroconf/commit/f9e23592137f30fdf7ef710dba065da31c79b1cf))
|
|
12
|
+
|
|
13
|
+
### Testing
|
|
14
|
+
|
|
15
|
+
- Cap helper get_service_info timeout in suppression test (~3.0s → ~1.85s)
|
|
16
|
+
([#1708](https://github.com/python-zeroconf/python-zeroconf/pull/1708),
|
|
17
|
+
[`ee3c7d7`](https://github.com/python-zeroconf/python-zeroconf/commit/ee3c7d74ff45327a3a6d520b86a691e21e2bc219))
|
|
18
|
+
|
|
19
|
+
- Drop ZeroconfServiceTypes.find() timeouts from 500ms to 200ms on loopback
|
|
20
|
+
([#1710](https://github.com/python-zeroconf/python-zeroconf/pull/1710),
|
|
21
|
+
[`64d143d`](https://github.com/python-zeroconf/python-zeroconf/commit/64d143d2ee7874ee1d9cef0dd2799c008b4aa791))
|
|
22
|
+
|
|
23
|
+
- Eliminate test_get_info_single race by injecting from the send mock
|
|
24
|
+
([#1716](https://github.com/python-zeroconf/python-zeroconf/pull/1716),
|
|
25
|
+
[`963d3d7`](https://github.com/python-zeroconf/python-zeroconf/commit/963d3d70e1cde056967eba0d8747ddcd247ae707))
|
|
26
|
+
|
|
27
|
+
- Fix race in test_register_and_lookup_type_by_uppercase_name
|
|
28
|
+
([#1712](https://github.com/python-zeroconf/python-zeroconf/pull/1712),
|
|
29
|
+
[`91aa21d`](https://github.com/python-zeroconf/python-zeroconf/commit/91aa21d52a0873f5fc12d43675b1b521dfe20519))
|
|
30
|
+
|
|
31
|
+
- Speed up service-info request tests with quick_request_timing fixture
|
|
32
|
+
([#1709](https://github.com/python-zeroconf/python-zeroconf/pull/1709),
|
|
33
|
+
[`4bae30a`](https://github.com/python-zeroconf/python-zeroconf/commit/4bae30a2ed0910ee7c4f1d0f92f2c400a7b10f31))
|
|
34
|
+
|
|
35
|
+
|
|
5
36
|
## v0.149.4 (2026-05-17)
|
|
6
37
|
|
|
7
38
|
### Bug Fixes
|
|
@@ -19,7 +19,7 @@ install_requires = \
|
|
|
19
19
|
|
|
20
20
|
setup_kwargs = {
|
|
21
21
|
'name': 'zeroconf',
|
|
22
|
-
'version': '0.149.
|
|
22
|
+
'version': '0.149.5',
|
|
23
23
|
'description': 'A pure python implementation of multicast DNS service discovery',
|
|
24
24
|
'long_description': 'python-zeroconf\n===============\n\n.. image:: https://github.com/python-zeroconf/python-zeroconf/workflows/CI/badge.svg\n :target: https://github.com/python-zeroconf/python-zeroconf?query=workflow%3ACI+branch%3Amaster\n\n.. image:: https://img.shields.io/pypi/v/zeroconf.svg\n :target: https://pypi.python.org/pypi/zeroconf\n\n.. image:: https://codecov.io/gh/python-zeroconf/python-zeroconf/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/python-zeroconf/python-zeroconf\n\n.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\n :target: https://codspeed.io/python-zeroconf/python-zeroconf\n :alt: Codspeed.io status for python-zeroconf\n\n.. image:: https://readthedocs.org/projects/python-zeroconf/badge/?version=latest\n :target: https://python-zeroconf.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n`Documentation <https://python-zeroconf.readthedocs.io/en/latest/>`_.\n\nThis is fork of pyzeroconf, Multicast DNS Service Discovery for Python,\noriginally by Paul Scott-Murphy (https://github.com/paulsm/pyzeroconf),\nmodified by William McBrine (https://github.com/wmcbrine/pyzeroconf).\n\nThe original William McBrine\'s fork note::\n\n This fork is used in all of my TiVo-related projects: HME for Python\n (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo.\n Before this, I was tracking the changes for zeroconf.py in three\n separate repos. I figured I should have an authoritative source.\n\n Although I make changes based on my experience with TiVos, I expect that\n they\'re generally applicable. This version also includes patches found\n on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere\n around the net -- not always well-documented, sorry.\n\nCompatible with:\n\n* Bonjour\n* Avahi\n\nCompared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:\n\n* isn\'t tied to Bonjour or Avahi\n* doesn\'t use D-Bus\n* doesn\'t force you to use particular event loop or Twisted (asyncio is used under the hood but not required)\n* is pip-installable\n* has PyPI distribution\n* has an optional cython extension for performance (pure python is supported as well)\n\nPython compatibility\n--------------------\n\n* CPython 3.10+\n* PyPy 3.10+\n\nVersioning\n----------\n\nThis project uses semantic versioning.\n\nStatus\n------\n\nThis project is actively maintained.\n\nTraffic Reduction\n-----------------\n\nBefore version 0.32, most traffic reduction techniques described in https://datatracker.ietf.org/doc/html/rfc6762#section-7\nwhere not implemented which could lead to excessive network traffic. It is highly recommended that version 0.32 or later\nis used if this is a concern.\n\nIPv6 support\n------------\n\nIPv6 support is relatively new and currently limited, specifically:\n\n* `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX\n systems.\n* Dual-stack IPv6 sockets are used, which may not be supported everywhere (some\n BSD variants do not have them).\n* Listening on localhost (`::1`) does not work. Help with understanding why is\n appreciated.\n\nHow to get python-zeroconf?\n===========================\n\n* PyPI page https://pypi.org/project/zeroconf/\n* GitHub project https://github.com/python-zeroconf/python-zeroconf\n\nThe easiest way to install python-zeroconf is using pip::\n\n pip install zeroconf\n\n\n\nHow do I use it?\n================\n\nHere\'s an example of browsing for a service:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class MyListener(ServiceListener):\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} updated")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} removed")\n\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"Service {name} added, service info: {info}")\n\n\n zeroconf = Zeroconf()\n listener = MyListener()\n browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)\n try:\n input("Press enter to exit...\\n\\n")\n finally:\n zeroconf.close()\n\n.. note::\n\n Discovery and service registration use *all* available network interfaces by default.\n If you want to customize that you need to specify ``interfaces`` argument when\n constructing ``Zeroconf`` object (see the code for details).\n\nIf you don\'t know the name of the service you need to browse for, try:\n\n.. code-block:: python\n\n from zeroconf import ZeroconfServiceTypes\n print(\'\\n\'.join(ZeroconfServiceTypes.find()))\n\nSee examples directory for more.\n\nChangelog\n=========\n\n`Changelog <CHANGELOG.md>`_\n\nLicense\n=======\n\nLGPL, see COPYING file for details.\n',
|
|
25
25
|
'author': 'Paul Scott-Murphy',
|
|
@@ -88,7 +88,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
|
|
|
88
88
|
|
|
89
89
|
__author__ = "Paul Scott-Murphy, William McBrine"
|
|
90
90
|
__maintainer__ = "Jakub Stasiak <jakub@stasiak.at>"
|
|
91
|
-
__version__ = "0.149.
|
|
91
|
+
__version__ = "0.149.5"
|
|
92
92
|
__license__ = "LGPL"
|
|
93
93
|
|
|
94
94
|
|
|
@@ -83,7 +83,7 @@ cdef class DNSIncoming:
|
|
|
83
83
|
link_py_int=object,
|
|
84
84
|
linked_labels=cython.list
|
|
85
85
|
)
|
|
86
|
-
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers)
|
|
86
|
+
cdef unsigned int _decode_labels_at_offset(self, unsigned int off, cython.list labels, cython.set seen_pointers, unsigned int depth)
|
|
87
87
|
|
|
88
88
|
@cython.locals(offset="unsigned int")
|
|
89
89
|
cdef void _read_header(self)
|
|
@@ -60,7 +60,7 @@ DNS_COMPRESSION_POINTER_LEN = 2
|
|
|
60
60
|
MAX_DNS_LABELS = 128
|
|
61
61
|
MAX_NAME_LENGTH = 253
|
|
62
62
|
|
|
63
|
-
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError)
|
|
63
|
+
DECODE_EXCEPTIONS = (IndexError, struct.error, IncomingDecodeError, RecursionError)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
_seen_logs: dict[str, int | tuple] = {}
|
|
@@ -409,7 +409,7 @@ class DNSIncoming:
|
|
|
409
409
|
labels: list[str] = []
|
|
410
410
|
seen_pointers: set[int] = set()
|
|
411
411
|
original_offset = self.offset
|
|
412
|
-
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers)
|
|
412
|
+
self.offset = self._decode_labels_at_offset(original_offset, labels, seen_pointers, 0)
|
|
413
413
|
self._name_cache[original_offset] = labels
|
|
414
414
|
name = ".".join(labels) + "."
|
|
415
415
|
if len(name) > MAX_NAME_LENGTH:
|
|
@@ -418,8 +418,14 @@ class DNSIncoming:
|
|
|
418
418
|
)
|
|
419
419
|
return name
|
|
420
420
|
|
|
421
|
-
def _decode_labels_at_offset(
|
|
421
|
+
def _decode_labels_at_offset(
|
|
422
|
+
self, off: _int, labels: list[str], seen_pointers: set[int], depth: _int
|
|
423
|
+
) -> int:
|
|
422
424
|
# This is a tight loop that is called frequently, small optimizations can make a difference.
|
|
425
|
+
if depth > MAX_DNS_LABELS:
|
|
426
|
+
raise IncomingDecodeError(
|
|
427
|
+
f"DNS compression pointer chain exceeds {MAX_DNS_LABELS} at {off} from {self.source}"
|
|
428
|
+
)
|
|
423
429
|
view = self.view
|
|
424
430
|
while off < self._data_len:
|
|
425
431
|
length = view[off]
|
|
@@ -457,7 +463,7 @@ class DNSIncoming:
|
|
|
457
463
|
if not linked_labels:
|
|
458
464
|
linked_labels = []
|
|
459
465
|
seen_pointers.add(link_py_int)
|
|
460
|
-
self._decode_labels_at_offset(link, linked_labels, seen_pointers)
|
|
466
|
+
self._decode_labels_at_offset(link, linked_labels, seen_pointers, depth + 1)
|
|
461
467
|
self._name_cache[link_py_int] = linked_labels
|
|
462
468
|
labels.extend(linked_labels)
|
|
463
469
|
if len(labels) > MAX_DNS_LABELS:
|
|
@@ -35,6 +35,20 @@ from zeroconf._history import QuestionHistory
|
|
|
35
35
|
|
|
36
36
|
_MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
|
|
37
37
|
|
|
38
|
+
# get_service_info / async_request timeout for tests using the
|
|
39
|
+
# `quick_request_timing` fixture. The fixture cuts the initial-query
|
|
40
|
+
# delay to ~15ms (10ms _LISTENER_TIME + 1-5ms jitter), so 50ms is
|
|
41
|
+
# ample headroom for tests that only need to observe the first one
|
|
42
|
+
# or two queries.
|
|
43
|
+
QUICK_REQUEST_TIMEOUT_MS = 50
|
|
44
|
+
|
|
45
|
+
# Timeout for ZeroconfServiceTypes.find() / AsyncZeroconfServiceTypes.async_find()
|
|
46
|
+
# in loopback integration tests. `find()` is just `time.sleep(timeout)` —
|
|
47
|
+
# it doesn't short-circuit on the first matching response — so the
|
|
48
|
+
# timeout becomes a lower bound on the test runtime. On loopback the
|
|
49
|
+
# registrar's response lands within a few ms; 200ms is ~50x headroom.
|
|
50
|
+
LOOPBACK_FIND_TIMEOUT = 0.2
|
|
51
|
+
|
|
38
52
|
|
|
39
53
|
class QuestionHistoryWithoutSuppression(QuestionHistory):
|
|
40
54
|
def suppresses(self, question: DNSQuestion, now: float, known_answers: set[DNSRecord]) -> bool:
|
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
|
|
11
11
|
from zeroconf import _core, const
|
|
12
12
|
from zeroconf._handlers import query_handler
|
|
13
|
+
from zeroconf._services import info as service_info
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@pytest.fixture(autouse=True)
|
|
@@ -59,3 +60,21 @@ def quick_timing() -> Generator[None]:
|
|
|
59
60
|
patch.object(_core, "_UNREGISTER_TIME", 10),
|
|
60
61
|
):
|
|
61
62
|
yield
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def quick_request_timing() -> Generator[None]:
|
|
67
|
+
"""Shorten the initial-query delay used by AsyncServiceInfo.async_request.
|
|
68
|
+
|
|
69
|
+
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
|
|
70
|
+
§5.2) help spread queries from multiple clients on real networks.
|
|
71
|
+
On loopback they're pure overhead — get_service_info-style tests
|
|
72
|
+
wait ~250ms before the first query even fires. Opt in by adding
|
|
73
|
+
`quick_request_timing` to a test's argument list, then drop the
|
|
74
|
+
test's own timeouts (which had to accommodate that delay).
|
|
75
|
+
"""
|
|
76
|
+
with (
|
|
77
|
+
patch.object(service_info, "_LISTENER_TIME", 10),
|
|
78
|
+
patch.object(service_info, "_AVOID_SYNC_DELAY_RANDOM_INTERVAL", (1, 5)),
|
|
79
|
+
):
|
|
80
|
+
yield
|
|
@@ -23,7 +23,7 @@ from zeroconf._services.info import ServiceInfo
|
|
|
23
23
|
from zeroconf._utils.net import IPVersion
|
|
24
24
|
from zeroconf.asyncio import AsyncZeroconf
|
|
25
25
|
|
|
26
|
-
from .. import _inject_response, has_working_ipv6
|
|
26
|
+
from .. import QUICK_REQUEST_TIMEOUT_MS, _inject_response, has_working_ipv6
|
|
27
27
|
|
|
28
28
|
log = logging.getLogger("zeroconf")
|
|
29
29
|
original_logging_level = logging.NOTSET
|
|
@@ -430,9 +430,9 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
430
430
|
|
|
431
431
|
return r.DNSIncoming(generated.packets()[0])
|
|
432
432
|
|
|
433
|
-
def get_service_info_helper(zc, type, name):
|
|
433
|
+
def get_service_info_helper(zc, type, name, timeout):
|
|
434
434
|
nonlocal service_info
|
|
435
|
-
service_info = zc.get_service_info(type, name)
|
|
435
|
+
service_info = zc.get_service_info(type, name, timeout)
|
|
436
436
|
service_info_event.set()
|
|
437
437
|
|
|
438
438
|
try:
|
|
@@ -453,9 +453,14 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
453
453
|
for question in seed_history_questions:
|
|
454
454
|
zc.question_history.add_question_at_time(question, far_future, set())
|
|
455
455
|
|
|
456
|
+
# No answers ever come back (all queries are suppressed),
|
|
457
|
+
# so cap the helper at the worst-case sum of the three
|
|
458
|
+
# phase waits below plus margin instead of the 3000ms
|
|
459
|
+
# default. Phase 3 waits ~1.6s (the 999ms QM gap plus
|
|
460
|
+
# jitter and 500ms buffer); 1500ms covers it.
|
|
456
461
|
helper_thread = threading.Thread(
|
|
457
462
|
target=get_service_info_helper,
|
|
458
|
-
args=(zc, service_type, service_name),
|
|
463
|
+
args=(zc, service_type, service_name, 1500),
|
|
459
464
|
)
|
|
460
465
|
helper_thread.start()
|
|
461
466
|
wait_time = (const._LISTENER_TIME + info._AVOID_SYNC_DELAY_RANDOM_INTERVAL[1] + 500) / 1000
|
|
@@ -506,6 +511,7 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
506
511
|
zc.remove_all_service_listeners()
|
|
507
512
|
zc.close()
|
|
508
513
|
|
|
514
|
+
@pytest.mark.usefixtures("quick_request_timing")
|
|
509
515
|
def test_get_info_single(self):
|
|
510
516
|
zc = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
511
517
|
|
|
@@ -516,91 +522,88 @@ class TestServiceInfo(unittest.TestCase):
|
|
|
516
522
|
service_address = "10.0.1.2"
|
|
517
523
|
|
|
518
524
|
service_info = None
|
|
519
|
-
send_event = Event()
|
|
520
525
|
service_info_event = Event()
|
|
521
526
|
|
|
522
|
-
|
|
527
|
+
ttl = 120
|
|
528
|
+
response_records = [
|
|
529
|
+
r.DNSText(
|
|
530
|
+
service_name,
|
|
531
|
+
const._TYPE_TXT,
|
|
532
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
533
|
+
ttl,
|
|
534
|
+
service_text,
|
|
535
|
+
),
|
|
536
|
+
r.DNSService(
|
|
537
|
+
service_name,
|
|
538
|
+
const._TYPE_SRV,
|
|
539
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
540
|
+
ttl,
|
|
541
|
+
0,
|
|
542
|
+
0,
|
|
543
|
+
80,
|
|
544
|
+
service_server,
|
|
545
|
+
),
|
|
546
|
+
r.DNSAddress(
|
|
547
|
+
service_server,
|
|
548
|
+
const._TYPE_A,
|
|
549
|
+
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
550
|
+
ttl,
|
|
551
|
+
socket.inet_pton(socket.AF_INET, service_address),
|
|
552
|
+
),
|
|
553
|
+
]
|
|
523
554
|
|
|
524
|
-
def
|
|
525
|
-
|
|
526
|
-
|
|
555
|
+
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
|
|
556
|
+
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
|
|
557
|
+
for record in records:
|
|
558
|
+
generated.add_answer_at_time(record, 0)
|
|
559
|
+
return r.DNSIncoming(generated.packets()[0])
|
|
527
560
|
|
|
528
|
-
|
|
529
|
-
|
|
561
|
+
sent_queries: list[r.DNSOutgoing] = []
|
|
562
|
+
|
|
563
|
+
def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
|
|
564
|
+
"""Capture each query and, on the first one, fill the cache
|
|
565
|
+
inline so the next iteration of `async_request` finds
|
|
566
|
+
`_is_complete=True` and exits without sending another query.
|
|
567
|
+
|
|
568
|
+
Running the inject from inside `send` keeps it on the event
|
|
569
|
+
loop thread and atomic with the first send — eliminating the
|
|
570
|
+
test-thread → `run_coroutine_threadsafe` race that flaked
|
|
571
|
+
under PyPy + use_cython when `quick_request_timing` shortens
|
|
572
|
+
the inter-iteration delay to ~15ms.
|
|
573
|
+
"""
|
|
574
|
+
sent_queries.append(out)
|
|
575
|
+
if len(sent_queries) == 1:
|
|
576
|
+
zc.record_manager.async_updates_from_response(mock_incoming_msg(response_records))
|
|
577
|
+
|
|
578
|
+
def get_service_info_helper(zc, type, name):
|
|
579
|
+
nonlocal service_info
|
|
580
|
+
service_info = zc.get_service_info(type, name)
|
|
581
|
+
service_info_event.set()
|
|
530
582
|
|
|
531
583
|
# patch the zeroconf send
|
|
532
584
|
with patch.object(zc, "async_send", send):
|
|
533
|
-
|
|
534
|
-
def mock_incoming_msg(records: Iterable[r.DNSRecord]) -> r.DNSIncoming:
|
|
535
|
-
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
|
|
536
|
-
|
|
537
|
-
for record in records:
|
|
538
|
-
generated.add_answer_at_time(record, 0)
|
|
539
|
-
|
|
540
|
-
return r.DNSIncoming(generated.packets()[0])
|
|
541
|
-
|
|
542
|
-
def get_service_info_helper(zc, type, name):
|
|
543
|
-
nonlocal service_info
|
|
544
|
-
service_info = zc.get_service_info(type, name)
|
|
545
|
-
service_info_event.set()
|
|
546
|
-
|
|
547
585
|
try:
|
|
548
|
-
ttl = 120
|
|
549
586
|
helper_thread = threading.Thread(
|
|
550
587
|
target=get_service_info_helper,
|
|
551
588
|
args=(zc, service_type, service_name),
|
|
552
589
|
)
|
|
553
590
|
helper_thread.start()
|
|
554
|
-
wait_time = 1
|
|
555
591
|
|
|
556
|
-
#
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in last_sent.questions
|
|
561
|
-
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in last_sent.questions
|
|
562
|
-
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in last_sent.questions
|
|
563
|
-
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in last_sent.questions
|
|
564
|
-
assert service_info is None
|
|
565
|
-
|
|
566
|
-
# Expect no further queries
|
|
567
|
-
last_sent = None
|
|
568
|
-
send_event.clear()
|
|
569
|
-
_inject_response(
|
|
570
|
-
zc,
|
|
571
|
-
mock_incoming_msg(
|
|
572
|
-
[
|
|
573
|
-
r.DNSText(
|
|
574
|
-
service_name,
|
|
575
|
-
const._TYPE_TXT,
|
|
576
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
577
|
-
ttl,
|
|
578
|
-
service_text,
|
|
579
|
-
),
|
|
580
|
-
r.DNSService(
|
|
581
|
-
service_name,
|
|
582
|
-
const._TYPE_SRV,
|
|
583
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
584
|
-
ttl,
|
|
585
|
-
0,
|
|
586
|
-
0,
|
|
587
|
-
80,
|
|
588
|
-
service_server,
|
|
589
|
-
),
|
|
590
|
-
r.DNSAddress(
|
|
591
|
-
service_server,
|
|
592
|
-
const._TYPE_A,
|
|
593
|
-
const._CLASS_IN | const._CLASS_UNIQUE,
|
|
594
|
-
ttl,
|
|
595
|
-
socket.inet_pton(socket.AF_INET, service_address),
|
|
596
|
-
),
|
|
597
|
-
]
|
|
598
|
-
),
|
|
599
|
-
)
|
|
600
|
-
send_event.wait(wait_time)
|
|
601
|
-
assert last_sent is None
|
|
592
|
+
# Helper should complete promptly — the inline inject in
|
|
593
|
+
# `send` populates the cache before the request loop's
|
|
594
|
+
# next iteration.
|
|
595
|
+
service_info_event.wait(1)
|
|
602
596
|
assert service_info is not None
|
|
603
597
|
|
|
598
|
+
# First (and only) query: QU for SRV/TXT/A/AAAA.
|
|
599
|
+
assert len(sent_queries) == 1
|
|
600
|
+
first_sent = sent_queries[0]
|
|
601
|
+
assert len(first_sent.questions) == 4
|
|
602
|
+
assert r.DNSQuestion(service_name, const._TYPE_SRV, const._CLASS_IN) in first_sent.questions
|
|
603
|
+
assert r.DNSQuestion(service_name, const._TYPE_TXT, const._CLASS_IN) in first_sent.questions
|
|
604
|
+
assert r.DNSQuestion(service_name, const._TYPE_A, const._CLASS_IN) in first_sent.questions
|
|
605
|
+
assert r.DNSQuestion(service_name, const._TYPE_AAAA, const._CLASS_IN) in first_sent.questions
|
|
606
|
+
|
|
604
607
|
finally:
|
|
605
608
|
helper_thread.join()
|
|
606
609
|
zc.remove_all_service_listeners()
|
|
@@ -980,7 +983,7 @@ def test_serviceinfo_accepts_bytes_or_string_dict():
|
|
|
980
983
|
assert info_service.dns_text().text == b"\x0epath=/~paulsm/"
|
|
981
984
|
|
|
982
985
|
|
|
983
|
-
def test_asking_qu_questions():
|
|
986
|
+
def test_asking_qu_questions(quick_request_timing):
|
|
984
987
|
"""Verify explicitly asking QU questions."""
|
|
985
988
|
type_ = "_quservice._tcp.local."
|
|
986
989
|
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -999,12 +1002,14 @@ def test_asking_qu_questions():
|
|
|
999
1002
|
|
|
1000
1003
|
# patch the zeroconf send
|
|
1001
1004
|
with patch.object(zeroconf, "async_send", send):
|
|
1002
|
-
zeroconf.get_service_info(
|
|
1005
|
+
zeroconf.get_service_info(
|
|
1006
|
+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QU
|
|
1007
|
+
)
|
|
1003
1008
|
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
|
|
1004
1009
|
zeroconf.close()
|
|
1005
1010
|
|
|
1006
1011
|
|
|
1007
|
-
def test_asking_qm_questions():
|
|
1012
|
+
def test_asking_qm_questions(quick_request_timing):
|
|
1008
1013
|
"""Verify explicitly asking QM questions."""
|
|
1009
1014
|
type_ = "_quservice._tcp.local."
|
|
1010
1015
|
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
@@ -1023,7 +1028,9 @@ def test_asking_qm_questions():
|
|
|
1023
1028
|
|
|
1024
1029
|
# patch the zeroconf send
|
|
1025
1030
|
with patch.object(zeroconf, "async_send", send):
|
|
1026
|
-
zeroconf.get_service_info(
|
|
1031
|
+
zeroconf.get_service_info(
|
|
1032
|
+
f"name.{type_}", type_, QUICK_REQUEST_TIMEOUT_MS, question_type=r.DNSQuestionType.QM
|
|
1033
|
+
)
|
|
1027
1034
|
assert first_outgoing.questions[0].unicast is False # type: ignore[union-attr]
|
|
1028
1035
|
zeroconf.close()
|
|
1029
1036
|
|
|
@@ -11,7 +11,7 @@ import unittest
|
|
|
11
11
|
import zeroconf as r
|
|
12
12
|
from zeroconf import ServiceInfo, Zeroconf, ZeroconfServiceTypes
|
|
13
13
|
|
|
14
|
-
from .. import _clear_cache, has_working_ipv6
|
|
14
|
+
from .. import LOOPBACK_FIND_TIMEOUT, _clear_cache, has_working_ipv6
|
|
15
15
|
|
|
16
16
|
log = logging.getLogger("zeroconf")
|
|
17
17
|
original_logging_level = logging.NOTSET
|
|
@@ -47,10 +47,10 @@ def test_integration_with_listener(disable_duplicate_packet_suppression):
|
|
|
47
47
|
)
|
|
48
48
|
zeroconf_registrar.registry.async_add(info)
|
|
49
49
|
try:
|
|
50
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
50
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=LOOPBACK_FIND_TIMEOUT)
|
|
51
51
|
assert type_ in service_types
|
|
52
52
|
_clear_cache(zeroconf_registrar)
|
|
53
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
53
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT)
|
|
54
54
|
assert type_ in service_types
|
|
55
55
|
|
|
56
56
|
finally:
|
|
@@ -79,10 +79,10 @@ def test_integration_with_listener_v6_records(disable_duplicate_packet_suppressi
|
|
|
79
79
|
)
|
|
80
80
|
zeroconf_registrar.registry.async_add(info)
|
|
81
81
|
try:
|
|
82
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
82
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=LOOPBACK_FIND_TIMEOUT)
|
|
83
83
|
assert type_ in service_types
|
|
84
84
|
_clear_cache(zeroconf_registrar)
|
|
85
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
85
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT)
|
|
86
86
|
assert type_ in service_types
|
|
87
87
|
|
|
88
88
|
finally:
|
|
@@ -115,10 +115,12 @@ def test_integration_with_listener_ipv6(disable_duplicate_packet_suppression):
|
|
|
115
115
|
)
|
|
116
116
|
zeroconf_registrar.registry.async_add(info)
|
|
117
117
|
try:
|
|
118
|
-
service_types = ZeroconfServiceTypes.find(
|
|
118
|
+
service_types = ZeroconfServiceTypes.find(
|
|
119
|
+
ip_version=r.IPVersion.V6Only, timeout=LOOPBACK_FIND_TIMEOUT
|
|
120
|
+
)
|
|
119
121
|
assert type_ in service_types
|
|
120
122
|
_clear_cache(zeroconf_registrar)
|
|
121
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
123
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT)
|
|
122
124
|
assert type_ in service_types
|
|
123
125
|
|
|
124
126
|
finally:
|
|
@@ -147,10 +149,10 @@ def test_integration_with_subtype_and_listener(disable_duplicate_packet_suppress
|
|
|
147
149
|
)
|
|
148
150
|
zeroconf_registrar.registry.async_add(info)
|
|
149
151
|
try:
|
|
150
|
-
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=
|
|
152
|
+
service_types = ZeroconfServiceTypes.find(interfaces=["127.0.0.1"], timeout=LOOPBACK_FIND_TIMEOUT)
|
|
151
153
|
assert discovery_type in service_types
|
|
152
154
|
_clear_cache(zeroconf_registrar)
|
|
153
|
-
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=
|
|
155
|
+
service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT)
|
|
154
156
|
assert discovery_type in service_types
|
|
155
157
|
|
|
156
158
|
finally:
|
|
@@ -43,6 +43,8 @@ from zeroconf.asyncio import (
|
|
|
43
43
|
from zeroconf.const import _LISTENER_TIME
|
|
44
44
|
|
|
45
45
|
from . import (
|
|
46
|
+
LOOPBACK_FIND_TIMEOUT,
|
|
47
|
+
QUICK_REQUEST_TIMEOUT_MS,
|
|
46
48
|
QuestionHistoryWithoutSuppression,
|
|
47
49
|
_clear_cache,
|
|
48
50
|
has_working_ipv6,
|
|
@@ -918,14 +920,20 @@ async def test_async_zeroconf_service_types(quick_timing: None) -> None:
|
|
|
918
920
|
)
|
|
919
921
|
task = await zeroconf_registrar.async_register_service(info)
|
|
920
922
|
await task
|
|
921
|
-
#
|
|
922
|
-
|
|
923
|
+
# Wait for the last announce broadcast before clearing. With
|
|
924
|
+
# `quick_timing` the broadcasts use _REGISTER_TIME=10ms apart so
|
|
925
|
+
# 50ms is plenty.
|
|
926
|
+
await asyncio.sleep(0.05)
|
|
923
927
|
_clear_cache(zeroconf_registrar.zeroconf)
|
|
924
928
|
try:
|
|
925
|
-
service_types = await AsyncZeroconfServiceTypes.async_find(
|
|
929
|
+
service_types = await AsyncZeroconfServiceTypes.async_find(
|
|
930
|
+
interfaces=["127.0.0.1"], timeout=LOOPBACK_FIND_TIMEOUT
|
|
931
|
+
)
|
|
926
932
|
assert type_ in service_types
|
|
927
933
|
_clear_cache(zeroconf_registrar.zeroconf)
|
|
928
|
-
service_types = await AsyncZeroconfServiceTypes.async_find(
|
|
934
|
+
service_types = await AsyncZeroconfServiceTypes.async_find(
|
|
935
|
+
aiozc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT
|
|
936
|
+
)
|
|
929
937
|
assert type_ in service_types
|
|
930
938
|
|
|
931
939
|
finally:
|
|
@@ -1139,7 +1147,7 @@ async def test_integration(quick_timing: None) -> None:
|
|
|
1139
1147
|
|
|
1140
1148
|
|
|
1141
1149
|
@pytest.mark.asyncio
|
|
1142
|
-
async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
1150
|
+
async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu(quick_request_timing):
|
|
1143
1151
|
"""Verify the service info first question is QU and subsequent ones are QM questions."""
|
|
1144
1152
|
type_ = "_quservice._tcp.local."
|
|
1145
1153
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
@@ -1182,11 +1190,11 @@ async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu():
|
|
|
1182
1190
|
# patch the zeroconf send
|
|
1183
1191
|
with patch.object(zeroconf_info, "async_send", send):
|
|
1184
1192
|
aiosinfo = AsyncServiceInfo(type_, registration_name)
|
|
1185
|
-
# Patch _is_complete so we send multiple times.
|
|
1186
|
-
# the QU query at 0ms
|
|
1187
|
-
#
|
|
1193
|
+
# Patch _is_complete so we send multiple times. Under
|
|
1194
|
+
# `quick_request_timing` both the QU query at 0ms and the QM
|
|
1195
|
+
# query at ~15ms land well inside QUICK_REQUEST_TIMEOUT_MS.
|
|
1188
1196
|
with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
|
|
1189
|
-
await aiosinfo.async_request(aiozc.zeroconf,
|
|
1197
|
+
await aiosinfo.async_request(aiozc.zeroconf, QUICK_REQUEST_TIMEOUT_MS)
|
|
1190
1198
|
try:
|
|
1191
1199
|
assert first_outgoing.questions[0].unicast is True # type: ignore[union-attr]
|
|
1192
1200
|
assert second_outgoing.questions[0].unicast is False # type: ignore[attr-defined]
|
|
@@ -219,7 +219,11 @@ class TestRegistrar(unittest.TestCase):
|
|
|
219
219
|
for _ in range(50):
|
|
220
220
|
time.sleep(0.02)
|
|
221
221
|
info.load_from_cache(zc)
|
|
222
|
-
|
|
222
|
+
# Wait for both A and TXT records — they arrive as separate
|
|
223
|
+
# cache updates and the listener may schedule the assertions
|
|
224
|
+
# between the two. Breaking on just `info.addresses` makes
|
|
225
|
+
# the test flaky under PyPy / skip_cython.
|
|
226
|
+
if info.addresses and info.properties:
|
|
223
227
|
break
|
|
224
228
|
assert info.addresses == [socket.inet_pton(socket.AF_INET, "1.2.3.4")]
|
|
225
229
|
assert info.properties == {b"version": b"1.0"}
|
|
@@ -1011,6 +1011,28 @@ def test_label_compression_attack():
|
|
|
1011
1011
|
assert len(parsed.answers()) == 1
|
|
1012
1012
|
|
|
1013
1013
|
|
|
1014
|
+
def test_dns_compression_pointer_chain_depth_attack() -> None:
|
|
1015
|
+
"""Test our wire parser rejects deeply chained compression pointers without recursing."""
|
|
1016
|
+
# Build a packet with one question whose name is a 1500-deep chain of forward
|
|
1017
|
+
# compression pointers, ending in a root label. Each pointer is 2 bytes,
|
|
1018
|
+
# so chain length easily exceeds CPython's default recursion limit.
|
|
1019
|
+
header = b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
|
|
1020
|
+
# Question at offset 12: pointer to offset 18 (past the question's type/class).
|
|
1021
|
+
question_name = bytes([0xC0, 18])
|
|
1022
|
+
question_type_class = b"\x00\x01\x00\x01"
|
|
1023
|
+
chain_depth = 1500
|
|
1024
|
+
chain = bytearray()
|
|
1025
|
+
for i in range(chain_depth):
|
|
1026
|
+
target = 18 + 2 * (i + 1)
|
|
1027
|
+
chain.append(0xC0 | (target >> 8))
|
|
1028
|
+
chain.append(target & 0xFF)
|
|
1029
|
+
chain.append(0x00)
|
|
1030
|
+
packet = header + question_name + question_type_class + bytes(chain)
|
|
1031
|
+
parsed = r.DNSIncoming(packet, ("1.2.3.4", 5353))
|
|
1032
|
+
assert parsed.valid is False
|
|
1033
|
+
assert parsed.questions == []
|
|
1034
|
+
|
|
1035
|
+
|
|
1014
1036
|
def test_dns_compression_loop_attack():
|
|
1015
1037
|
"""Test our wire parser does not loop forever when dns compression is in a loop."""
|
|
1016
1038
|
packet = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|