From 4f44089c050d19e9d8923a5c116c4f3fa328fd81 Mon Sep 17 00:00:00 2001 From: SeoFood Date: Sun, 11 Oct 2026 02:45:41 +0200 Subject: [PATCH 1/2] mDNS: ignore truncated DNS responses before updating discovery state --- pyatv/core/mdns.py | 18 ++++++++++++------ tests/core/test_mdns.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pyatv/core/mdns.py b/pyatv/core/mdns.py index f48f6ff85..d146db7dc 100644 --- a/pyatv/core/mdns.py +++ b/pyatv/core/mdns.py @@ -5,6 +5,7 @@ import logging import math import socket +import struct from types import SimpleNamespace import typing import weakref @@ -247,7 +248,13 @@ def datagram_received(self, data: bytes, _) -> None: Total=len(self.queries), ) - self.parser.add_message(DnsMessage().unpack(data)) + try: + decoded_msg = DnsMessage().unpack(data) + except (UnicodeDecodeError, struct.error): + log_binary(_LOGGER, "Failed to decode message", Msg=data) + return + + self.parser.add_message(decoded_msg) self.received_responses += 1 if self.received_responses == len(self.queries): @@ -423,17 +430,13 @@ def datagram_received(self, data, addr) -> None: Data=data, ) - query_resp = self.query_responses.setdefault( - addr[0], QueryResponse(count=0, deep_sleep=False, parser=ServiceParser()) - ) - # Suppress decode errors for now (but still log) try: decoded_msg = DnsMessage().unpack(data) parser = ServiceParser() services = parser.add_message(decoded_msg).parse() - except UnicodeDecodeError: + except (UnicodeDecodeError, struct.error): log_binary(_LOGGER, "Failed to decode message", Msg=data) return @@ -448,6 +451,9 @@ def datagram_received(self, data, addr) -> None: ): return + query_resp = self.query_responses.setdefault( + addr[0], QueryResponse(count=0, deep_sleep=False, parser=ServiceParser()) + ) is_sleep_proxy = all(service.port == 0 for service in services) query_resp.count += 1 query_resp.deep_sleep |= is_sleep_proxy diff --git a/tests/core/test_mdns.py b/tests/core/test_mdns.py index 2c4891a55..de15f490e 100644 --- a/tests/core/test_mdns.py +++ b/tests/core/test_mdns.py @@ -264,3 +264,34 @@ def test_parse_ignore_duplicate_records(): records = parser.table["service._abc._tcp.local"] assert mdns.QueryType.SRV in records assert len(records[mdns.QueryType.SRV]) == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("length", range(12)) +@pytest.mark.parametrize("unicast", [False, True]) +async def test_truncated_dns_header_then_valid_response(length, unicast, caplog): + if unicast: + protocol = mdns.UnicastDnsSdClientProtocol( + [MEDIAREMOTE_SERVICE], "127.0.0.1", 1 + ) + else: + protocol = mdns.MulticastDnsSdClientProtocol( + asyncio.get_running_loop(), [MEDIAREMOTE_SERVICE], "224.0.0.251", 5353, None + ) + address = ("127.0.0.1", 5353) + protocol.datagram_received(bytes(length), address) + if unicast: + assert protocol.received_responses == 0 + else: + assert not protocol.query_responses + query = mdns.create_service_queries([MEDIAREMOTE_SERVICE], dns.QueryType.PTR)[0] + response = fake_udns.create_response(query, TEST_SERVICES) + protocol.datagram_received(response.pack(), address) + if unicast: + assert protocol.received_responses == 1 + services = protocol.parser.parse() + else: + assert protocol.query_responses[address[0]].count == 1 + services = protocol.query_responses[address[0]].parser.parse() + assert any(service.type == MEDIAREMOTE_SERVICE for service in services) + assert "exception during data handling" not in caplog.text From 564c6eff6d4a1387d50678a199171ad48b02489c Mon Sep 17 00:00:00 2001 From: SeoFood Date: Mon, 12 Oct 2026 07:05:30 +0200 Subject: [PATCH 2/2] test: cover filtered mDNS replies and remove ineffective log assertion --- tests/core/test_mdns.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/core/test_mdns.py b/tests/core/test_mdns.py index de15f490e..fe81b9863 100644 --- a/tests/core/test_mdns.py +++ b/tests/core/test_mdns.py @@ -269,7 +269,7 @@ def test_parse_ignore_duplicate_records(): @pytest.mark.asyncio @pytest.mark.parametrize("length", range(12)) @pytest.mark.parametrize("unicast", [False, True]) -async def test_truncated_dns_header_then_valid_response(length, unicast, caplog): +async def test_truncated_dns_header_then_valid_response(length, unicast): if unicast: protocol = mdns.UnicastDnsSdClientProtocol( [MEDIAREMOTE_SERVICE], "127.0.0.1", 1 @@ -294,4 +294,28 @@ async def test_truncated_dns_header_then_valid_response(length, unicast, caplog) assert protocol.query_responses[address[0]].count == 1 services = protocol.query_responses[address[0]].parser.parse() assert any(service.type == MEDIAREMOTE_SERVICE for service in services) - assert "exception during data handling" not in caplog.text + + +@pytest.mark.asyncio +@pytest.mark.parametrize("unrelated_service", [False, True], ids=["empty", "unrelated"]) +async def test_filtered_multicast_reply_then_valid_response(unrelated_service): + protocol = mdns.MulticastDnsSdClientProtocol( + asyncio.get_running_loop(), [MEDIAREMOTE_SERVICE], "224.0.0.251", 5353, None + ) + address = ("127.0.0.1", 5353) + response = dns.DnsMessage() + if unrelated_service: + service_type = "_unrelated._tcp.local" + query = mdns.create_service_queries([service_type], dns.QueryType.PTR)[0] + response = fake_udns.create_response( + query, {service_type: TEST_SERVICES[MEDIAREMOTE_SERVICE]} + ) + protocol.datagram_received(response.pack(), address) + assert not protocol.query_responses + + query = mdns.create_service_queries([MEDIAREMOTE_SERVICE], dns.QueryType.PTR)[0] + response = fake_udns.create_response(query, TEST_SERVICES) + protocol.datagram_received(response.pack(), address) + assert protocol.query_responses[address[0]].count == 1 + services = protocol.query_responses[address[0]].parser.parse() + assert any(service.type == MEDIAREMOTE_SERVICE for service in services)