FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix: correct nsec bitmap semantics and resolve txt denials · python-zeroconf/python-zeroconf@7d711a4 · GitHub

Commit 7d711a4

Browse files
committed
fix: correct nsec bitmap semantics and resolve txt denials
1 parent 4fac0be commit 7d711a4

6 files changed

Lines changed: 367 additions & 46 deletions

File tree

‎src/zeroconf/_handlers/query_handler.pxd‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import cython
33

44
from .._cache cimport DNSCache
5-
from .._dns cimport DNSAddress, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet
5+
from .._dns cimport DNSAddress, DNSNsec, DNSPointer, DNSQuestion, DNSRecord, DNSRRSet
66
from .._history cimport QuestionHistory
77
from .._protocol.incoming cimport DNSIncoming
88
from .._services.info cimport ServiceInfo
@@ -83,7 +83,12 @@ cdef class QueryHandler:
8383
@cython.locals(service=ServiceInfo)
8484
cdef void _add_pointer_answers(self, list services, cython.dict answer_set, DNSRRSet known_answers)
8585

86-
@cython.locals(service=ServiceInfo, dns_address=DNSAddress)
86+
@cython.locals(
87+
service=ServiceInfo,
88+
dns_address=DNSAddress,
89+
type_seen=cython.bint,
90+
nsec=DNSNsec,
91+
)
8792
cdef void _add_address_answers(self, list services, cython.dict answer_set, DNSRRSet known_answers, cython.uint type_)
8893

8994
@cython.locals(question_lower_name=str, type_=cython.uint, service=ServiceInfo)

‎src/zeroconf/_handlers/query_handler.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,23 +264,24 @@ def _add_address_answers(
264264
for service in services:
265265
answers: list[DNSAddress] = []
266266
additionals: set[DNSRecord] = set()
267-
seen_types: set[int] = set()
267+
type_seen = False
268268
for dns_address in service._dns_addresses(None, _IPVersion_ALL):
269-
seen_types.add(dns_address.type)
270269
if dns_address.type != type_:
271270
additionals.add(dns_address)
272-
elif not known_answers.suppresses(dns_address):
273-
answers.append(dns_address)
274-
missing_types: set[int] = _ADDRESS_RECORD_TYPES - seen_types
271+
else:
272+
type_seen = True
273+
if not known_answers.suppresses(dns_address):
274+
answers.append(dns_address)
275275
if answers:
276-
if missing_types:
277-
assert service.server is not None, "Service server must be set for NSEC record."
278-
additionals.add(service._dns_nsec(list(missing_types), None))
276+
nsec = service._dns_address_nsec(None)
277+
if nsec is not None:
278+
additionals.add(nsec)
279279
for answer in answers:
280280
answer_set[answer] = additionals
281-
elif type_ in missing_types:
282-
assert service.server is not None, "Service server must be set for NSEC record."
283-
answer_set[service._dns_nsec(list(missing_types), None)] = set()
281+
elif not type_seen and type_ in _ADDRESS_RECORD_TYPES:
282+
nsec = service._dns_address_nsec(None)
283+
if nsec is not None:
284+
answer_set[nsec] = set()
284285

285286
def _answer_question(
286287
self,

‎src/zeroconf/_services/info.pxd‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ cdef object QM_QUESTION
4545
cdef object _IPVersion_All_value
4646
cdef object _IPVersion_V4Only_value
4747

48-
cdef cython.set _ADDRESS_RECORD_TYPES
49-
5048
cdef unsigned int _DUPLICATE_QUESTION_INTERVAL
5149

5250
cdef bint TYPE_CHECKING
@@ -77,6 +75,7 @@ cdef class ServiceInfo(RecordUpdateListener):
7775
cdef public DNSService _dns_service_cache
7876
cdef public DNSText _dns_text_cache
7977
cdef public cython.list _dns_address_cache
78+
cdef public DNSNsec _dns_address_nsec_cache
8079
cdef public cython.set _get_address_and_nsec_records_cache
8180
cdef public cython.set _query_record_types
8281
cdef public bint _txt_seen
@@ -111,10 +110,14 @@ cdef class ServiceInfo(RecordUpdateListener):
111110
@cython.locals(
112111
dns_service_record=DNSService,
113112
dns_text_record=DNSText,
114-
dns_address_record=DNSAddress
113+
dns_address_record=DNSAddress,
114+
dns_nsec_record=DNSNsec
115115
)
116116
cdef bint _process_record_threadsafe(self, object zc, DNSRecord record, double now)
117117

118+
@cython.locals(rdtypes=cython.list)
119+
cdef bint _process_nsec_record(self, DNSNsec record)
120+
118121
@cython.locals(existing_idx=int, existing=object)
119122
cdef bint _upsert_ipv6_address(self, object ip_addr)
120123

@@ -143,7 +146,8 @@ cdef class ServiceInfo(RecordUpdateListener):
143146
@cython.locals(cacheable=cython.bint)
144147
cdef DNSText _dns_text(self, object override_ttl)
145148

146-
cdef DNSNsec _dns_nsec(self, cython.list missing_types, object override_ttl)
149+
@cython.locals(cacheable=cython.bint, has_v4=cython.bint, has_v6=cython.bint)
150+
cdef DNSNsec _dns_address_nsec(self, object override_ttl)
147151

148152
@cython.locals(cacheable=cython.bint)
149153
cdef cython.set _get_address_and_nsec_records(self, object override_ttl)

‎src/zeroconf/_services/info.py‎

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import asyncio
2626
import random
27+
import warnings
2728
from collections.abc import Sequence
2829
from typing import TYPE_CHECKING, cast
2930

@@ -62,7 +63,6 @@
6263
from .._utils.net import IPVersion, _encode_address
6364
from .._utils.time import current_time_millis
6465
from ..const import (
65-
_ADDRESS_RECORD_TYPES,
6666
_CLASS_IN,
6767
_CLASS_IN_UNIQUE,
6868
_DNS_HOST_TTL,
@@ -180,6 +180,7 @@ class ServiceInfo(RecordUpdateListener):
180180
__slots__ = (
181181
"_decoded_properties",
182182
"_dns_address_cache",
183+
"_dns_address_nsec_cache",
183184
"_dns_pointer_cache",
184185
"_dns_service_cache",
185186
"_dns_text_cache",
@@ -255,6 +256,7 @@ def __init__(
255256
self.other_ttl = other_ttl
256257
self._new_records_futures: set[asyncio.Future] | None = None
257258
self._dns_address_cache: list[DNSAddress] | None = None
259+
self._dns_address_nsec_cache: DNSNsec | None = None
258260
self._dns_pointer_cache: DNSPointer | None = None
259261
self._dns_service_cache: DNSService | None = None
260262
self._dns_text_cache: DNSText | None = None
@@ -294,6 +296,7 @@ def addresses(self, value: list[bytes]) -> None:
294296
self._ipv4_addresses.clear()
295297
self._ipv6_addresses.clear()
296298
self._dns_address_cache = None
299+
self._dns_address_nsec_cache = None
297300
self._get_address_and_nsec_records_cache = None
298301

299302
for address in value:
@@ -336,6 +339,7 @@ def decoded_properties(self) -> dict[str, str | None]:
336339
def async_clear_cache(self) -> None:
337340
"""Clear the cache for this service info."""
338341
self._dns_address_cache = None
342+
self._dns_address_nsec_cache = None
339343
self._dns_pointer_cache = None
340344
self._dns_service_cache = None
341345
self._dns_text_cache = None
@@ -644,8 +648,26 @@ def _process_record_threadsafe(self, zc: Zeroconf, record: DNSRecord, now: float
644648
self._set_ipv6_addresses_from_cache(zc, now)
645649
return True
646650

651+
if record_type is DNSNsec:
652+
dns_nsec_record = record
653+
if TYPE_CHECKING:
654+
assert isinstance(dns_nsec_record, DNSNsec)
655+
return self._process_nsec_record(dns_nsec_record)
656+
647657
return False
648658

659+
def _process_nsec_record(self, record: DNSNsec) -> bool:
660+
"""Record a TXT denial from an NSEC record at the service name."""
661+
rdtypes = record.rdtypes
662+
# RFC 6762 §6.1: the type bitmap lists the rrtypes that exist, so SRV
663+
# present with TXT absent denies the TXT record. Requiring the SRV bit
664+
# also keeps older python-zeroconf NSECs, which listed the missing
665+
# address types, from being misread as a TXT denial.
666+
if self._txt_seen or _TYPE_SRV not in rdtypes or _TYPE_TXT in rdtypes:
667+
return False
668+
self._txt_seen = True
669+
return True
670+
649671
def dns_addresses(
650672
self,
651673
override_ttl: int_ | None = None,
@@ -751,39 +773,58 @@ def _dns_text(self, override_ttl: int_ | None) -> DNSText:
751773
self._dns_text_cache = record
752774
return record
753775

754-
def dns_nsec(self, missing_types: list[int], override_ttl: int_ | None = None) -> DNSNsec:
755-
"""Return DNSNsec from ServiceInfo."""
756-
return self._dns_nsec(missing_types, override_ttl)
776+
def dns_nsec(self, missing_types: list[int], override_ttl: int_ | None = None) -> DNSNsec | None:
777+
"""Deprecated: use dns_address_nsec instead; missing_types is ignored."""
778+
warnings.warn(
779+
"dns_nsec is deprecated, and will be removed in a future version. "
780+
"Use dns_address_nsec instead; missing_types is ignored.",
781+
DeprecationWarning,
782+
stacklevel=2,
783+
)
784+
return self._dns_address_nsec(override_ttl)
785+
786+
def dns_address_nsec(self, override_ttl: int_ | None = None) -> DNSNsec | None:
787+
"""Return DNSNsec asserting which address types exist, or None if not applicable."""
788+
return self._dns_address_nsec(override_ttl)
757789

758-
def _dns_nsec(self, missing_types: list[int_], override_ttl: int_ | None) -> DNSNsec:
759-
"""Return DNSNsec from ServiceInfo."""
760-
return DNSNsec(
761-
self._name,
790+
def _dns_address_nsec(self, override_ttl: int_ | None) -> DNSNsec | None:
791+
cacheable = override_ttl is None
792+
if self._dns_address_nsec_cache is not None and cacheable:
793+
return self._dns_address_nsec_cache
794+
# RFC 6762 §6.1: the type bitmap lists the rrtypes that exist at the
795+
# name. Both families present leaves nothing to deny; neither leaves
796+
# nothing to assert (an empty bitmap is unencodable).
797+
has_v4 = bool(self._ipv4_addresses)
798+
has_v6 = bool(self._ipv6_addresses)
799+
if has_v4 == has_v6:
800+
return None
801+
assert self.server is not None, "Service server must be set for NSEC record."
802+
record = DNSNsec(
803+
self.server,
762804
_TYPE_NSEC,
763805
_CLASS_IN_UNIQUE,
764806
override_ttl if override_ttl is not None else self.host_ttl,
765-
self._name,
766-
missing_types,
807+
self.server,
808+
[_TYPE_A] if has_v4 else [_TYPE_AAAA],
767809
0.0,
768810
)
811+
if cacheable:
812+
self._dns_address_nsec_cache = record
813+
return record
769814

770815
def get_address_and_nsec_records(self, override_ttl: int_ | None = None) -> set[DNSRecord]:
771-
"""Build a set of address records and NSEC records for non-present record types."""
816+
"""Build a set of address records plus an NSEC asserting which address types exist."""
772817
return self._get_address_and_nsec_records(override_ttl)
773818

774819
def _get_address_and_nsec_records(self, override_ttl: int_ | None) -> set[DNSRecord]:
775-
"""Build a set of address records and NSEC records for non-present record types."""
820+
"""Build a set of address records plus an NSEC asserting which address types exist."""
776821
cacheable = override_ttl is None
777822
if self._get_address_and_nsec_records_cache is not None and cacheable:
778823
return self._get_address_and_nsec_records_cache
779-
missing_types: set[int] = _ADDRESS_RECORD_TYPES.copy()
780-
records: set[DNSRecord] = set()
781-
for dns_address in self._dns_addresses(override_ttl, IPVersion.All):
782-
missing_types.discard(dns_address.type)
783-
records.add(dns_address)
784-
if missing_types:
785-
assert self.server is not None, "Service server must be set for NSEC record."
786-
records.add(self._dns_nsec(list(missing_types), override_ttl))
824+
records: set[DNSRecord] = set(self._dns_addresses(override_ttl, IPVersion.All))
825+
nsec = self._dns_address_nsec(override_ttl)
826+
if nsec is not None:
827+
records.add(nsec)
787828
if cacheable:
788829
self._get_address_and_nsec_records_cache = records
789830
return records
@@ -831,6 +872,10 @@ def _load_from_cache(self, zc: Zeroconf, now: float_) -> bool:
831872
cached_txt_record = cache.get_by_details(self._name, _TYPE_TXT, _CLASS_IN)
832873
if cached_txt_record:
833874
self._process_record_threadsafe(zc, cached_txt_record, now)
875+
if not self._txt_seen:
876+
cached_nsec_record = cache.get_by_details(self._name, _TYPE_NSEC, _CLASS_IN)
877+
if cached_nsec_record:
878+
self._process_record_threadsafe(zc, cached_nsec_record, now)
834879
if original_server_key == self.server_key:
835880
# If there is a srv which changes the server_key,
836881
# A and AAAA will already be loaded from the cache
@@ -847,7 +892,8 @@ def _is_complete(self) -> bool:
847892
848893
RFC 6763 section 6 requires every DNS-SD service to have a TXT record,
849894
so a service is not complete until one has been seen. An empty TXT
850-
record counts as seen; a missing one does not.
895+
record counts as seen, as does an NSEC record denying its existence
896+
(RFC 6762 section 6.1); a missing one does not.
851897
"""
852898
return bool(self._txt_seen and (self._ipv4_addresses or self._ipv6_addresses))
853899

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL