zeroconf 0.149.7__tar.gz → 0.149.8__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.

Files changed (100) hide show
  1. {zeroconf-0.149.7 → zeroconf-0.149.8}/CHANGELOG.md +23 -0
  2. {zeroconf-0.149.7 → zeroconf-0.149.8}/PKG-INFO +1 -1
  3. {zeroconf-0.149.7 → zeroconf-0.149.8}/pyproject.toml +4 -4
  4. {zeroconf-0.149.7 → zeroconf-0.149.8}/setup.py +1 -1
  5. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/__init__.py +1 -1
  6. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_protocol/incoming.pxd +1 -0
  7. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_protocol/incoming.py +14 -0
  8. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/__init__.py +14 -1
  9. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/services/test_types.py +3 -3
  10. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_handlers.py +6 -6
  11. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_protocol.py +79 -0
  12. {zeroconf-0.149.7 → zeroconf-0.149.8}/COPYING +0 -0
  13. {zeroconf-0.149.7 → zeroconf-0.149.8}/README.rst +0 -0
  14. {zeroconf-0.149.7 → zeroconf-0.149.8}/build_ext.py +0 -0
  15. {zeroconf-0.149.7 → zeroconf-0.149.8}/docs/Makefile +0 -0
  16. {zeroconf-0.149.7 → zeroconf-0.149.8}/docs/_ext/zeroconfautodocfix.py +0 -0
  17. {zeroconf-0.149.7 → zeroconf-0.149.8}/docs/api.rst +0 -0
  18. {zeroconf-0.149.7 → zeroconf-0.149.8}/docs/conf.py +0 -0
  19. {zeroconf-0.149.7 → zeroconf-0.149.8}/docs/index.rst +0 -0
  20. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_cache.pxd +0 -0
  21. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_cache.py +0 -0
  22. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_core.py +0 -0
  23. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_dns.pxd +0 -0
  24. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_dns.py +0 -0
  25. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_engine.py +0 -0
  26. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_exceptions.py +0 -0
  27. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/__init__.py +0 -0
  28. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/answers.pxd +0 -0
  29. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/answers.py +0 -0
  30. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
  31. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
  32. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/query_handler.pxd +0 -0
  33. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/query_handler.py +0 -0
  34. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/record_manager.pxd +0 -0
  35. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_handlers/record_manager.py +0 -0
  36. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_history.pxd +0 -0
  37. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_history.py +0 -0
  38. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_listener.pxd +0 -0
  39. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_listener.py +0 -0
  40. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_logger.py +0 -0
  41. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_protocol/__init__.py +0 -0
  42. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_protocol/outgoing.pxd +0 -0
  43. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_protocol/outgoing.py +0 -0
  44. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_record_update.pxd +0 -0
  45. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_record_update.py +0 -0
  46. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/__init__.pxd +0 -0
  47. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/__init__.py +0 -0
  48. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/browser.pxd +0 -0
  49. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/browser.py +0 -0
  50. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/info.pxd +0 -0
  51. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/info.py +0 -0
  52. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/registry.pxd +0 -0
  53. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/registry.py +0 -0
  54. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_services/types.py +0 -0
  55. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_transport.py +0 -0
  56. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_updates.pxd +0 -0
  57. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_updates.py +0 -0
  58. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/__init__.py +0 -0
  59. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/asyncio.py +0 -0
  60. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/ipaddress.pxd +0 -0
  61. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/ipaddress.py +0 -0
  62. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/name.py +0 -0
  63. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/net.py +0 -0
  64. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/time.pxd +0 -0
  65. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/_utils/time.py +0 -0
  66. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/asyncio.py +0 -0
  67. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/const.py +0 -0
  68. {zeroconf-0.149.7 → zeroconf-0.149.8}/src/zeroconf/py.typed +0 -0
  69. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/__init__.py +0 -0
  70. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/helpers.py +0 -0
  71. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_cache.py +0 -0
  72. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_cache_bound.py +0 -0
  73. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_incoming.py +0 -0
  74. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_mark_seen.py +0 -0
  75. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_outgoing.py +0 -0
  76. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_send.py +0 -0
  77. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/benchmarks/test_txt_properties.py +0 -0
  78. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/conftest.py +0 -0
  79. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/services/__init__.py +0 -0
  80. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/services/test_browser.py +0 -0
  81. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/services/test_info.py +0 -0
  82. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/services/test_registry.py +0 -0
  83. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_asyncio.py +0 -0
  84. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_cache.py +0 -0
  85. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_circular_imports.py +0 -0
  86. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_core.py +0 -0
  87. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_dns.py +0 -0
  88. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_engine.py +0 -0
  89. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_exceptions.py +0 -0
  90. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_history.py +0 -0
  91. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_init.py +0 -0
  92. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_listener.py +0 -0
  93. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_logger.py +0 -0
  94. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_services.py +0 -0
  95. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/test_updates.py +0 -0
  96. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/utils/__init__.py +0 -0
  97. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/utils/test_asyncio.py +0 -0
  98. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/utils/test_ipaddress.py +0 -0
  99. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/utils/test_name.py +0 -0
  100. {zeroconf-0.149.7 → zeroconf-0.149.8}/tests/utils/test_net.py +0 -0
@@ -2,6 +2,29 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.149.8 (2026-05-19)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Bound NSEC bitmap length against record end
10
+ ([#1731](https://github.com/python-zeroconf/python-zeroconf/pull/1731),
11
+ [`1d83550`](https://github.com/python-zeroconf/python-zeroconf/commit/1d83550c58ed0ea69b611a907cd4bdfcb2eef535))
12
+
13
+ ### Testing
14
+
15
+ - Give IPv6-only loopback find() its own timeout
16
+ ([#1721](https://github.com/python-zeroconf/python-zeroconf/pull/1721),
17
+ [`fcd1ffb`](https://github.com/python-zeroconf/python-zeroconf/commit/fcd1ffb4af9b3b26bc0ecae30641251a4c9a4ba6))
18
+
19
+ - Widen LOOPBACK_FIND_TIMEOUT under PyPy
20
+ ([#1742](https://github.com/python-zeroconf/python-zeroconf/pull/1742),
21
+ [`6924606`](https://github.com/python-zeroconf/python-zeroconf/commit/69246065085fa25f9baf20abbf0eaddcb4a4d88c))
22
+
23
+ - Widen safety margin in test_response_aggregation_timings_multiple
24
+ ([#1737](https://github.com/python-zeroconf/python-zeroconf/pull/1737),
25
+ [`228af17`](https://github.com/python-zeroconf/python-zeroconf/commit/228af178657a70dd2e76420a8187345de6ede46f))
26
+
27
+
5
28
  ## v0.149.7 (2026-05-18)
6
29
 
7
30
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zeroconf
3
- Version: 0.149.7
3
+ Version: 0.149.8
4
4
  Summary: A pure python implementation of multicast DNS service discovery
5
5
  License-Expression: LGPL-2.1-or-later
6
6
  License-File: COPYING
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "zeroconf"
7
- version = "0.149.7"
7
+ version = "0.149.8"
8
8
  license = "LGPL-2.1-or-later"
9
9
  description = "A pure python implementation of multicast DNS service discovery"
10
10
  readme = "README.rst"
@@ -84,13 +84,13 @@ python = "^3.10"
84
84
  ifaddr = ">=0.1.7"
85
85
 
86
86
  [tool.poetry.group.dev.dependencies]
87
- pytest = ">=7.2,<9.0"
87
+ pytest = ">=9.0.3,<10.0"
88
88
  pytest-cov = ">=4,<8"
89
- pytest-asyncio = ">=0.20.3,<1.3.0"
89
+ pytest-asyncio = ">=1.3.0,<1.4.0"
90
90
  cython = "^3.2.4"
91
91
  setuptools = ">=65.6.3,<83.0.0"
92
92
  pytest-timeout = "^2.1.0"
93
- pytest-codspeed = ">=4.5.0,<5.0"
93
+ pytest-codspeed = ">=5.0.2,<6.0"
94
94
 
95
95
  [tool.poetry.group.docs.dependencies]
96
96
  sphinx = "^7.4.7 || ^8.1.3"
@@ -19,7 +19,7 @@ install_requires = \
19
19
 
20
20
  setup_kwargs = {
21
21
  'name': 'zeroconf',
22
- 'version': '0.149.7',
22
+ 'version': '0.149.8',
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.7"
91
+ __version__ = "0.149.8"
92
92
  __license__ = "LGPL"
93
93
 
94
94
 
@@ -128,6 +128,7 @@ cdef class DNSIncoming:
128
128
  byte="unsigned int",
129
129
  i="unsigned int",
130
130
  bitmap_length="unsigned int",
131
+ bitmap_end="unsigned int",
131
132
  )
132
133
  cdef list _read_bitmap(self, unsigned int end)
133
134
 
@@ -388,9 +388,23 @@ class DNSIncoming:
388
388
  offset = self.offset
389
389
  offset_plus_one = offset + 1
390
390
  offset_plus_two = offset + 2
391
+ # RFC 4034 §4.1.2: each window block is window-number byte +
392
+ # bitmap-length byte (1..32) + bitmap. A bitmap_length that walks
393
+ # past the record's declared end would otherwise leave self.offset
394
+ # pointing inside (or past) the next record header, corrupting
395
+ # every subsequent record in the same packet.
396
+ if offset_plus_two > end:
397
+ raise IncomingDecodeError(
398
+ f"NSEC bitmap window header truncated at offset {offset} from {self.source}"
399
+ )
391
400
  window = view[offset]
392
401
  bitmap_length = view[offset_plus_one]
393
402
  bitmap_end = offset_plus_two + bitmap_length
403
+ if bitmap_length == 0 or bitmap_length > 32 or bitmap_end > end:
404
+ raise IncomingDecodeError(
405
+ f"NSEC bitmap length {bitmap_length} invalid or overruns record end "
406
+ f"at offset {offset} from {self.source}"
407
+ )
394
408
  for i, byte in enumerate(self.data[offset_plus_two:bitmap_end]):
395
409
  for bit in range(8):
396
410
  if byte & (0x80 >> bit):
@@ -23,6 +23,7 @@ USA
23
23
  from __future__ import annotations
24
24
 
25
25
  import asyncio
26
+ import platform
26
27
  import socket
27
28
  import time
28
29
  from functools import cache
@@ -35,6 +36,8 @@ from zeroconf._history import QuestionHistory
35
36
 
36
37
  _MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution
37
38
 
39
+ _IS_PYPY = platform.python_implementation() == "PyPy"
40
+
38
41
  # get_service_info / async_request timeout for tests using the
39
42
  # `quick_request_timing` fixture. The fixture cuts the initial-query
40
43
  # delay to ~15ms (10ms _LISTENER_TIME + 1-5ms jitter), so 50ms is
@@ -49,7 +52,17 @@ QUICK_REQUEST_TIMEOUT_MS = 50
49
52
  # the `quick_timing` fixture, which shrinks the browser's first-query
50
53
  # delay from RFC 6762 §5.2's 20-120ms window to 1-5ms; with that shave
51
54
  # the registrar's response lands inside ~10ms and 75ms is ~7x headroom.
52
- LOOPBACK_FIND_TIMEOUT = 0.075
55
+ # PyPy's JIT is still warming up the first time this path runs early in
56
+ # the suite, so the round trip is too slow for 75ms; give it more room.
57
+ LOOPBACK_FIND_TIMEOUT = 0.3 if _IS_PYPY else 0.075
58
+
59
+ # IPv6-only `find()` on Linux GitHub runners can hit `[Errno 101] Network
60
+ # is unreachable` on the `::1` socket and falls back to the `fe80::` link-
61
+ # local interface, which adds latency the IPv4 loopback path never pays.
62
+ # PyPy widens that further with JIT warmup. The 75ms budget that works on
63
+ # IPv4 loopback is too tight for the V6Only path under those conditions
64
+ # — give it more headroom.
65
+ IPV6_LOOPBACK_FIND_TIMEOUT = 0.5
53
66
 
54
67
 
55
68
  class QuestionHistoryWithoutSuppression(QuestionHistory):
@@ -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 LOOPBACK_FIND_TIMEOUT, _clear_cache, has_working_ipv6
14
+ from .. import IPV6_LOOPBACK_FIND_TIMEOUT, LOOPBACK_FIND_TIMEOUT, _clear_cache, has_working_ipv6
15
15
 
16
16
  log = logging.getLogger("zeroconf")
17
17
  original_logging_level = logging.NOTSET
@@ -116,11 +116,11 @@ def test_integration_with_listener_ipv6(quick_timing, disable_duplicate_packet_s
116
116
  zeroconf_registrar.registry.async_add(info)
117
117
  try:
118
118
  service_types = ZeroconfServiceTypes.find(
119
- ip_version=r.IPVersion.V6Only, timeout=LOOPBACK_FIND_TIMEOUT
119
+ ip_version=r.IPVersion.V6Only, timeout=IPV6_LOOPBACK_FIND_TIMEOUT
120
120
  )
121
121
  assert type_ in service_types
122
122
  _clear_cache(zeroconf_registrar)
123
- service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=LOOPBACK_FIND_TIMEOUT)
123
+ service_types = ZeroconfServiceTypes.find(zc=zeroconf_registrar, timeout=IPV6_LOOPBACK_FIND_TIMEOUT)
124
124
  assert type_ in service_types
125
125
 
126
126
  finally:
@@ -1783,19 +1783,19 @@ async def test_response_aggregation_timings_multiple(run_isolated, disable_dupli
1783
1783
  protocol.last_time = 0 # manually reset the last time to avoid duplicate packet suppression
1784
1784
  protocol.datagram_received(query2.packets()[0], ("127.0.0.1", const._MDNS_PORT))
1785
1785
  protocol.last_time = 0 # manually reset the last time to avoid duplicate packet suppression
1786
- # The delay should increase with two packets and
1787
- # 900ms is beyond the maximum aggregation delay
1788
- # when there is no network protection delay
1789
- await asyncio.sleep(0.9)
1786
+ # The minimum protected send_after is 1000ms + 20ms random; sleep
1787
+ # well under that so coarse timers on slow runners cannot push the
1788
+ # send into this window and flake the assertion.
1789
+ await asyncio.sleep(0.5)
1790
1790
  calls = send_mock.mock_calls
1791
1791
  assert len(calls) == 0
1792
1792
 
1793
1793
  # 1000ms (1s network protection delays)
1794
- # - 900ms (already slept)
1794
+ # - 500ms (already slept)
1795
1795
  # + 120ms (maximum random delay)
1796
1796
  # + 200ms (maximum protected aggregation delay)
1797
1797
  # + 20ms (execution time)
1798
- await asyncio.sleep(millis_to_seconds(1000 - 900 + 120 + 200 + 20))
1798
+ await asyncio.sleep(millis_to_seconds(1000 - 500 + 120 + 200 + 20))
1799
1799
  calls = send_mock.mock_calls
1800
1800
  assert len(calls) == 1
1801
1801
  outgoing = send_mock.call_args[0][0]
@@ -807,6 +807,85 @@ def test_parse_packet_with_nsec_record():
807
807
  assert nsec_record.next_name == "MyHome54 (2)._meshcop._udp.local."
808
808
 
809
809
 
810
+ def test_nsec_bitmap_length_overruns_record_end():
811
+ """Reject NSEC bitmap whose declared length runs past the record boundary."""
812
+ # 0 questions, 2 answers. Answer 1 is a malformed NSEC: rdlength=9, but the
813
+ # bitmap window claims length=255 — overrunning the record. Answer 2 is a
814
+ # PTR that must still parse because the offset for the next record stays
815
+ # pinned to the NSEC's declared end.
816
+ packet = (
817
+ b"\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x00"
818
+ b"\x04test\x05local\x00"
819
+ b"\x00\x2f\x80\x01"
820
+ b"\x00\x00\x11\x94"
821
+ b"\x00\x09"
822
+ b"\xc0\x0c"
823
+ b"\x00\xff"
824
+ b"\x80\x00\x00\x00\x00"
825
+ b"\xc0\x0c"
826
+ b"\x00\x0c\x00\x01"
827
+ b"\x00\x00\x11\x94"
828
+ b"\x00\x02"
829
+ b"\xc0\x0c"
830
+ )
831
+ parsed = r.DNSIncoming(packet)
832
+ answers = parsed.answers()
833
+ ptrs = [a for a in answers if isinstance(a, r.DNSPointer)]
834
+ assert len(ptrs) == 1
835
+ assert ptrs[0].alias == "test.local."
836
+ # The malformed NSEC must not surface — if it did, it would carry rdtypes
837
+ # synthesized from bytes past the record boundary.
838
+ assert not any(isinstance(a, r.DNSNsec) for a in answers)
839
+
840
+
841
+ def test_nsec_bitmap_zero_length_window_rejected():
842
+ """A bitmap window with length=0 violates RFC 4034 §4.1.2 and must be rejected."""
843
+ packet = (
844
+ b"\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x00"
845
+ b"\x04test\x05local\x00"
846
+ b"\x00\x2f\x80\x01"
847
+ b"\x00\x00\x11\x94"
848
+ b"\x00\x04"
849
+ b"\xc0\x0c"
850
+ b"\x00\x00"
851
+ b"\xc0\x0c"
852
+ b"\x00\x0c\x00\x01"
853
+ b"\x00\x00\x11\x94"
854
+ b"\x00\x02"
855
+ b"\xc0\x0c"
856
+ )
857
+ parsed = r.DNSIncoming(packet)
858
+ answers = parsed.answers()
859
+ ptrs = [a for a in answers if isinstance(a, r.DNSPointer)]
860
+ assert len(ptrs) == 1
861
+ assert not any(isinstance(a, r.DNSNsec) for a in answers)
862
+
863
+
864
+ def test_nsec_bitmap_truncated_window_header_rejected():
865
+ """Reject NSEC bitmap with a trailing byte too short to hold a window header."""
866
+ packet = (
867
+ b"\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x00"
868
+ b"\x04test\x05local\x00"
869
+ b"\x00\x2f\x80\x01"
870
+ b"\x00\x00\x11\x94"
871
+ b"\x00\x06"
872
+ b"\xc0\x0c"
873
+ b"\x00\x01\x80"
874
+ b"\xff"
875
+ b"\xc0\x0c"
876
+ b"\x00\x0c\x00\x01"
877
+ b"\x00\x00\x11\x94"
878
+ b"\x00\x02"
879
+ b"\xc0\x0c"
880
+ )
881
+ parsed = r.DNSIncoming(packet)
882
+ answers = parsed.answers()
883
+ ptrs = [a for a in answers if isinstance(a, r.DNSPointer)]
884
+ assert len(ptrs) == 1
885
+ assert ptrs[0].alias == "test.local."
886
+ assert not any(isinstance(a, r.DNSNsec) for a in answers)
887
+
888
+
810
889
  def test_records_same_packet_share_fate():
811
890
  """Test records in the same packet all have the same created time."""
812
891
  out = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_AA)
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