Skip to content

Commit 8a25a44

Browse files
authored
Implement multi-packet known answer supression (#687)
- Implements https://datatracker.ietf.org/doc/html/rfc6762#section-7.2 - Fixes #499
1 parent 4865d2b commit 8a25a44

4 files changed

Lines changed: 324 additions & 50 deletions

File tree

tests/test_core.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
""" Unit tests for zeroconf._core """
66

7+
import asyncio
78
import itertools
89
import logging
910
import os
@@ -18,7 +19,7 @@
1819
import zeroconf as r
1920
from zeroconf import _core, const, ServiceBrowser, Zeroconf
2021

21-
from . import has_working_ipv6, _inject_response
22+
from . import has_working_ipv6, _clear_cache, _inject_response
2223

2324
log = logging.getLogger('zeroconf')
2425
original_logging_level = logging.NOTSET
@@ -423,3 +424,184 @@ def test_sending_unicast():
423424
assert zc.cache.get(entry) is not None
424425

425426
zc.close()
427+
428+
429+
def test_tc_bit_defers():
430+
zc = Zeroconf(interfaces=['127.0.0.1'])
431+
type_ = "_tcbitdefer._tcp.local."
432+
name = "knownname"
433+
name2 = "knownname2"
434+
name3 = "knownname3"
435+
436+
registration_name = "%s.%s" % (name, type_)
437+
registration2_name = "%s.%s" % (name2, type_)
438+
registration3_name = "%s.%s" % (name3, type_)
439+
440+
desc = {'path': '/~paulsm/'}
441+
server_name = "ash-2.local."
442+
server_name2 = "ash-3.local."
443+
server_name3 = "ash-4.local."
444+
445+
info = r.ServiceInfo(
446+
type_, registration_name, 80, 0, 0, desc, server_name, addresses=[socket.inet_aton("10.0.1.2")]
447+
)
448+
info2 = r.ServiceInfo(
449+
type_, registration2_name, 80, 0, 0, desc, server_name2, addresses=[socket.inet_aton("10.0.1.2")]
450+
)
451+
info3 = r.ServiceInfo(
452+
type_, registration3_name, 80, 0, 0, desc, server_name3, addresses=[socket.inet_aton("10.0.1.2")]
453+
)
454+
zc.registry.add(info)
455+
zc.registry.add(info2)
456+
zc.registry.add(info3)
457+
458+
def threadsafe_query(*args):
459+
async def make_query():
460+
zc.handle_query(*args)
461+
462+
asyncio.run_coroutine_threadsafe(make_query(), zc.loop).result()
463+
464+
now = r.current_time_millis()
465+
_clear_cache(zc)
466+
467+
generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
468+
question = r.DNSQuestion(type_, const._TYPE_PTR, const._CLASS_IN)
469+
generated.add_question(question)
470+
for _ in range(300):
471+
# Add so many answers we end up with another packet
472+
generated.add_answer_at_time(info.dns_pointer(), now)
473+
generated.add_answer_at_time(info2.dns_pointer(), now)
474+
generated.add_answer_at_time(info3.dns_pointer(), now)
475+
packets = generated.packets()
476+
assert len(packets) == 4
477+
expected_deferred = []
478+
source_ip = '203.0.113.13'
479+
480+
next_packet = r.DNSIncoming(packets.pop(0))
481+
expected_deferred.append(next_packet)
482+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
483+
assert zc._deferred[source_ip] == expected_deferred
484+
assert source_ip in zc._timers
485+
486+
next_packet = r.DNSIncoming(packets.pop(0))
487+
expected_deferred.append(next_packet)
488+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
489+
assert zc._deferred[source_ip] == expected_deferred
490+
assert source_ip in zc._timers
491+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
492+
assert zc._deferred[source_ip] == expected_deferred
493+
assert source_ip in zc._timers
494+
495+
next_packet = r.DNSIncoming(packets.pop(0))
496+
expected_deferred.append(next_packet)
497+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
498+
assert zc._deferred[source_ip] == expected_deferred
499+
assert source_ip in zc._timers
500+
501+
next_packet = r.DNSIncoming(packets.pop(0))
502+
expected_deferred.append(next_packet)
503+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
504+
assert source_ip not in zc._deferred
505+
assert source_ip not in zc._timers
506+
507+
# unregister
508+
zc.unregister_service(info)
509+
zc.close()
510+
511+
512+
def test_tc_bit_defers_last_response_missing():
513+
zc = Zeroconf(interfaces=['127.0.0.1'])
514+
type_ = "_knowndefer._tcp.local."
515+
name = "knownname"
516+
name2 = "knownname2"
517+
name3 = "knownname3"
518+
519+
registration_name = "%s.%s" % (name, type_)
520+
registration2_name = "%s.%s" % (name2, type_)
521+
registration3_name = "%s.%s" % (name3, type_)
522+
523+
desc = {'path': '/~paulsm/'}
524+
server_name = "ash-2.local."
525+
server_name2 = "ash-3.local."
526+
server_name3 = "ash-4.local."
527+
528+
info = r.ServiceInfo(
529+
type_, registration_name, 80, 0, 0, desc, server_name, addresses=[socket.inet_aton("10.0.1.2")]
530+
)
531+
info2 = r.ServiceInfo(
532+
type_, registration2_name, 80, 0, 0, desc, server_name2, addresses=[socket.inet_aton("10.0.1.2")]
533+
)
534+
info3 = r.ServiceInfo(
535+
type_, registration3_name, 80, 0, 0, desc, server_name3, addresses=[socket.inet_aton("10.0.1.2")]
536+
)
537+
zc.registry.add(info)
538+
zc.registry.add(info2)
539+
zc.registry.add(info3)
540+
541+
def threadsafe_query(*args):
542+
async def make_query():
543+
zc.handle_query(*args)
544+
545+
asyncio.run_coroutine_threadsafe(make_query(), zc.loop).result()
546+
547+
now = r.current_time_millis()
548+
_clear_cache(zc)
549+
source_ip = '203.0.113.12'
550+
551+
generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
552+
question = r.DNSQuestion(type_, const._TYPE_PTR, const._CLASS_IN)
553+
generated.add_question(question)
554+
for _ in range(300):
555+
# Add so many answers we end up with another packet
556+
generated.add_answer_at_time(info.dns_pointer(), now)
557+
generated.add_answer_at_time(info2.dns_pointer(), now)
558+
generated.add_answer_at_time(info3.dns_pointer(), now)
559+
packets = generated.packets()
560+
assert len(packets) == 4
561+
expected_deferred = []
562+
563+
next_packet = r.DNSIncoming(packets.pop(0))
564+
expected_deferred.append(next_packet)
565+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
566+
assert zc._deferred[source_ip] == expected_deferred
567+
timer1 = zc._timers[source_ip]
568+
569+
next_packet = r.DNSIncoming(packets.pop(0))
570+
expected_deferred.append(next_packet)
571+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
572+
assert zc._deferred[source_ip] == expected_deferred
573+
timer2 = zc._timers[source_ip]
574+
if sys.version_info >= (3, 7):
575+
assert timer1.cancelled()
576+
assert timer2 != timer1
577+
578+
# Send the same packet again to similar multi interfaces
579+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
580+
assert zc._deferred[source_ip] == expected_deferred
581+
assert source_ip in zc._timers
582+
timer3 = zc._timers[source_ip]
583+
if sys.version_info >= (3, 7):
584+
assert not timer3.cancelled()
585+
assert timer3 == timer2
586+
587+
next_packet = r.DNSIncoming(packets.pop(0))
588+
expected_deferred.append(next_packet)
589+
threadsafe_query(next_packet, source_ip, const._MDNS_PORT)
590+
assert zc._deferred[source_ip] == expected_deferred
591+
assert source_ip in zc._timers
592+
timer4 = zc._timers[source_ip]
593+
if sys.version_info >= (3, 7):
594+
assert timer3.cancelled()
595+
assert timer4 != timer3
596+
597+
for _ in range(7):
598+
time.sleep(0.1)
599+
if source_ip not in zc._timers:
600+
break
601+
602+
assert source_ip not in zc._deferred
603+
assert source_ip not in zc._timers
604+
605+
# unregister
606+
zc.registry.remove(info)
607+
zc.close()

0 commit comments

Comments
 (0)