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

fix: fail service resolution fast when nsec denies the needed address… · python-zeroconf/python-zeroconf@77f8cf0 · GitHub

Commit 77f8cf0

Browse files
authored
fix: fail service resolution fast when nsec denies the needed address types (#1827)
1 parent ca9d027 commit 77f8cf0

3 files changed

Lines changed: 568 additions & 25 deletions

File tree

‎src/zeroconf/_services/info.pxd‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ cdef class ServiceInfo(RecordUpdateListener):
6060
cdef public str key
6161
cdef public cython.list _ipv4_addresses
6262
cdef public cython.list _ipv6_addresses
63+
cdef public bint _ipv4_denied
64+
cdef public bint _ipv6_denied
6365
cdef public object port
6466
cdef public object weight
6567
cdef public object priority
@@ -80,7 +82,7 @@ cdef class ServiceInfo(RecordUpdateListener):
8082
cdef public cython.set _query_record_types
8183
cdef public bint _txt_seen
8284

83-
@cython.locals(record_update=RecordUpdate, update=bint, cache=DNSCache)
85+
@cython.locals(record_update=RecordUpdate, record=DNSRecord, nsec_records=list)
8486
cpdef void async_update_records(self, object zc, double now, cython.list records)
8587

8688
@cython.locals(cache=DNSCache)
@@ -115,8 +117,16 @@ cdef class ServiceInfo(RecordUpdateListener):
115117
)
116118
cdef bint _process_record_threadsafe(self, object zc, DNSRecord record, double now)
117119

118-
@cython.locals(rdtypes=cython.list)
119-
cdef bint _process_nsec_record(self, DNSNsec record)
120+
@cython.locals(cache=DNSCache)
121+
cdef void _load_records_for_new_server_from_cache(self, object zc, double now)
122+
123+
@cython.locals(
124+
rdtypes=cython.list,
125+
updated=cython.bint,
126+
ipv4_denied=cython.bint,
127+
ipv6_denied=cython.bint,
128+
)
129+
cdef bint _process_nsec_record(self, DNSNsec record, str record_key)
120130

121131
@cython.locals(existing_idx=int, existing=object)
122132
cdef bint _upsert_ipv6_address(self, object ip_addr)

‎src/zeroconf/_services/info.py‎

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ class ServiceInfo(RecordUpdateListener):
186186
"_dns_text_cache",
187187
"_get_address_and_nsec_records_cache",
188188
"_ipv4_addresses",
189+
"_ipv4_denied",
189190
"_ipv6_addresses",
191+
"_ipv6_denied",
190192
"_name",
191193
"_new_records_futures",
192194
"_properties",
@@ -233,6 +235,8 @@ def __init__(
233235
self.key = name.lower()
234236
self._ipv4_addresses: list[ZeroconfIPv4Address] = []
235237
self._ipv6_addresses: list[ZeroconfIPv6Address] = []
238+
self._ipv4_denied = False
239+
self._ipv6_denied = False
236240
if addresses is not None:
237241
self.addresses = addresses
238242
elif parsed_addresses is not None:
@@ -571,8 +575,21 @@ def async_update_records(self, zc: Zeroconf, now: float_, records: list[RecordUp
571575
"""
572576
new_records_futures = self._new_records_futures
573577
updated: bool = False
578+
nsec_records = None
574579
for record_update in records:
575-
updated |= self._process_record_threadsafe(zc, record_update.new, now)
580+
record = record_update.new
581+
# NSEC records are processed last so a denial for an SRV target
582+
# learned later in the same batch is not discarded (wire order of
583+
# records in a response is not guaranteed).
584+
if type(record) is DNSNsec:
585+
if nsec_records is None:
586+
nsec_records = []
587+
nsec_records.append(record)
588+
continue
589+
updated |= self._process_record_threadsafe(zc, record, now)
590+
if nsec_records is not None:
591+
for record in nsec_records:
592+
updated |= self._process_record_threadsafe(zc, record, now)
576593
if updated and new_records_futures:
577594
_resolve_all_futures_to_none(new_records_futures)
578595

@@ -620,6 +637,14 @@ def _process_record_threadsafe(self, zc: Zeroconf, record: DNSRecord, now: float
620637
assert isinstance(ip_addr, ZeroconfIPv6Address)
621638
return self._upsert_ipv6_address(ip_addr)
622639

640+
if record_type is DNSNsec:
641+
if record_key not in (self.server_key, self.key):
642+
return False
643+
dns_nsec_record = record
644+
if TYPE_CHECKING:
645+
assert isinstance(dns_nsec_record, DNSNsec)
646+
return self._process_nsec_record(dns_nsec_record, record_key)
647+
623648
if record_key != self.key:
624649
return False
625650

@@ -644,29 +669,48 @@ def _process_record_threadsafe(self, zc: Zeroconf, record: DNSRecord, now: float
644669
self.weight = dns_service_record.weight
645670
self.priority = dns_service_record.priority
646671
if old_server_key != self.server_key:
647-
self._set_ipv4_addresses_from_cache(zc, now)
648-
self._set_ipv6_addresses_from_cache(zc, now)
672+
self._load_records_for_new_server_from_cache(zc, now)
649673
return True
650674

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-
657675
return False
658676

659-
def _process_nsec_record(self, record: DNSNsec) -> bool:
660-
"""Record a TXT denial from an NSEC record at the service name."""
677+
def _load_records_for_new_server_from_cache(self, zc: Zeroconf, now: float_) -> None:
678+
"""Re-derive per-host state, denials included, after the SRV target changed."""
679+
self._ipv4_denied = False
680+
self._ipv6_denied = False
681+
self._set_ipv4_addresses_from_cache(zc, now)
682+
self._set_ipv6_addresses_from_cache(zc, now)
683+
if TYPE_CHECKING:
684+
assert self.server_key is not None
685+
cache = zc.cache
686+
cached_server_nsec_record = cache.get_by_details(self.server_key, _TYPE_NSEC, _CLASS_IN)
687+
if cached_server_nsec_record:
688+
self._process_record_threadsafe(zc, cached_server_nsec_record, now)
689+
690+
def _process_nsec_record(self, record: DNSNsec, record_key: str_) -> bool:
691+
"""Record the denials asserted by an NSEC record (RFC 6762 §6.1)."""
661692
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
693+
updated = False
694+
if record_key == self.server_key:
695+
# Each NSEC is an authoritative snapshot of what exists at the
696+
# host, so recompute both flags instead of accumulating denials.
697+
ipv4_denied = _TYPE_A not in rdtypes
698+
ipv6_denied = _TYPE_AAAA not in rdtypes
699+
if ipv4_denied != self._ipv4_denied or ipv6_denied != self._ipv6_denied:
700+
self._ipv4_denied = ipv4_denied
701+
self._ipv6_denied = ipv6_denied
702+
updated = True
703+
if (
704+
record_key == self.key
705+
and not self._txt_seen
706+
and _TYPE_SRV in rdtypes
707+
and _TYPE_TXT not in rdtypes
708+
):
709+
# Requiring the SRV bit keeps older python-zeroconf NSECs, which listed the
710+
# missing address types, from being misread as a TXT denial.
711+
self._txt_seen = True
712+
updated = True
713+
return updated
670714

671715
def dns_addresses(
672716
self,
@@ -866,6 +910,9 @@ def _load_from_cache(self, zc: Zeroconf, now: float_) -> bool:
866910
"""
867911
cache = zc.cache
868912
original_server_key = self.server_key
913+
# Denials are only trusted until the next cache load re-derives them.
914+
self._ipv4_denied = False
915+
self._ipv6_denied = False
869916
cached_srv_record = cache.get_by_details(self._name, _TYPE_SRV, _CLASS_IN)
870917
if cached_srv_record:
871918
self._process_record_threadsafe(zc, cached_srv_record, now)
@@ -878,12 +925,16 @@ def _load_from_cache(self, zc: Zeroconf, now: float_) -> bool:
878925
self._process_record_threadsafe(zc, cached_nsec_record, now)
879926
if original_server_key == self.server_key:
880927
# If there is a srv which changes the server_key,
881-
# A and AAAA will already be loaded from the cache
882-
# and we do not want to do it twice
928+
# A, AAAA, and the server NSEC will already be loaded
929+
# from the cache and we do not want to do it twice
883930
for record in self._get_address_records_from_cache_by_type(zc, _TYPE_A):
884931
self._process_record_threadsafe(zc, record, now)
885932
for record in self._get_address_records_from_cache_by_type(zc, _TYPE_AAAA):
886933
self._process_record_threadsafe(zc, record, now)
934+
if self.server_key and not self._is_complete:
935+
cached_server_nsec_record = cache.get_by_details(self.server_key, _TYPE_NSEC, _CLASS_IN)
936+
if cached_server_nsec_record:
937+
self._process_record_threadsafe(zc, cached_server_nsec_record, now)
887938
return self._is_complete
888939

889940
@property
@@ -897,6 +948,14 @@ def _is_complete(self) -> bool:
897948
"""
898949
return bool(self._txt_seen and (self._ipv4_addresses or self._ipv6_addresses))
899950

951+
@property
952+
def _is_denied(self) -> bool:
953+
"""Every address type this request needs was denied via NSEC (RFC 6762 section 6.1)."""
954+
query_types = self._query_record_types
955+
return (_TYPE_A not in query_types or (self._ipv4_denied and not self._ipv4_addresses)) and (
956+
_TYPE_AAAA not in query_types or (self._ipv6_denied and not self._ipv6_addresses)
957+
)
958+
900959
def request(
901960
self,
902961
zc: Zeroconf,
@@ -978,7 +1037,7 @@ async def async_request(
9781037
try:
9791038
zc.async_add_listener(self, None)
9801039
while not self._is_complete:
981-
if last <= now:
1040+
if last <= now or self._is_denied:
9821041
return False
9831042
if next_ <= now:
9841043
this_question_type = question_type or (QU_QUESTION if first_request else QM_QUESTION)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL