Skip to content

Commit 137a5d6

Browse files
authored
fix: skip NSEC records in ServiceBrowser to suppress spurious updates (#1762)
1 parent 4ffba87 commit 137a5d6

3 files changed

Lines changed: 75 additions & 1 deletion

File tree

src/zeroconf/_services/browser.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ cdef bint TYPE_CHECKING
1414
cdef object cached_possible_types
1515
cdef cython.uint _EXPIRE_REFRESH_TIME_PERCENT, _MAX_MSG_TYPICAL, _DNS_PACKET_HEADER_LEN
1616
cdef cython.uint _TYPE_PTR
17+
cdef cython.uint _TYPE_NSEC
1718
cdef object _CLASS_IN
1819
cdef object SERVICE_STATE_CHANGE_ADDED, SERVICE_STATE_CHANGE_REMOVED, SERVICE_STATE_CHANGE_UPDATED
1920
cdef cython.set _ADDRESS_RECORD_TYPES

src/zeroconf/_services/browser.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
_MDNS_ADDR,
6464
_MDNS_ADDR6,
6565
_MDNS_PORT,
66+
_TYPE_NSEC,
6667
_TYPE_PTR,
6768
)
6869

@@ -678,7 +679,12 @@ def async_update_records(self, zc: Zeroconf, now: float_, records: list[RecordUp
678679
old_record = record_update.old
679680
record_type = record.type
680681

681-
if record_type is _TYPE_PTR:
682+
# NSEC records assert non-existence of a record type
683+
# (RFC 6762 §6.1); skip so we do not fire spurious updates.
684+
if record_type == _TYPE_NSEC:
685+
continue
686+
687+
if record_type == _TYPE_PTR:
682688
if TYPE_CHECKING:
683689
record = cast(DNSPointer, record)
684690
pointer = record

tests/services/test_browser.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,73 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de
10851085
zc.close()
10861086

10871087

1088+
def test_service_browser_nsec_record_does_not_trigger_update():
1089+
"""NSEC records assert non-existence and must not fire ServiceStateChange.Updated."""
1090+
zc = Zeroconf(interfaces=["127.0.0.1"])
1091+
type_ = "_hap._tcp.local."
1092+
registration_name = f"xxxyyy.{type_}"
1093+
callbacks: list[tuple[str, str, str]] = []
1094+
service_added = Event()
1095+
1096+
class MyServiceListener(r.ServiceListener):
1097+
def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
1098+
if name == registration_name:
1099+
callbacks.append(("add", type_, name))
1100+
service_added.set()
1101+
1102+
def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
1103+
if name == registration_name:
1104+
callbacks.append(("remove", type_, name))
1105+
1106+
def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def]
1107+
if name == registration_name:
1108+
callbacks.append(("update", type_, name))
1109+
1110+
listener = MyServiceListener()
1111+
browser = r.ServiceBrowser(zc, type_, None, listener)
1112+
try:
1113+
desc = {"path": "/~paulsm/"}
1114+
address = socket.inet_aton("10.0.1.2")
1115+
info = ServiceInfo(type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[address])
1116+
1117+
_inject_response(
1118+
zc,
1119+
mock_incoming_msg(
1120+
[
1121+
info.dns_pointer(),
1122+
info.dns_service(),
1123+
info.dns_text(),
1124+
*info.dns_addresses(),
1125+
]
1126+
),
1127+
)
1128+
assert service_added.wait(timeout=5), "add_service callback never fired"
1129+
1130+
# NSEC inject runs synchronously through the event loop; once
1131+
# _inject_response returns, async_update_records has already
1132+
# decided not to enqueue a callback for the NSEC record.
1133+
_inject_response(
1134+
zc,
1135+
mock_incoming_msg(
1136+
[
1137+
r.DNSNsec(
1138+
registration_name,
1139+
const._TYPE_NSEC,
1140+
const._CLASS_IN | const._CLASS_UNIQUE,
1141+
const._DNS_OTHER_TTL,
1142+
registration_name,
1143+
[const._TYPE_AAAA],
1144+
),
1145+
]
1146+
),
1147+
)
1148+
1149+
assert callbacks == [("add", type_, registration_name)]
1150+
finally:
1151+
browser.cancel()
1152+
zc.close()
1153+
1154+
10881155
def test_service_browser_uses_non_strict_names():
10891156
"""Verify we can look for technically invalid names as we cannot change what others do."""
10901157

0 commit comments

Comments
 (0)