Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
14dda5a
wip
bdraco Jun 14, 2021
d7ff112
fix
bdraco Jun 14, 2021
c912afa
fix
bdraco Jun 14, 2021
7856200
flooding check
bdraco Jun 14, 2021
53489f9
fixes
bdraco Jun 14, 2021
9811354
fises
bdraco Jun 14, 2021
25d88ca
fises
bdraco Jun 14, 2021
eae2094
remove useless super
bdraco Jun 14, 2021
cf0527f
pylint
bdraco Jun 14, 2021
fa876ab
fixes
bdraco Jun 14, 2021
236cff9
wip
bdraco Jun 14, 2021
1ad8e68
tweak
bdraco Jun 14, 2021
89abc1b
tweak
bdraco Jun 14, 2021
27ad348
wip
bdraco Jun 14, 2021
5cedce4
wip
bdraco Jun 14, 2021
e907ab8
wip
bdraco Jun 14, 2021
0ee8558
wip
bdraco Jun 14, 2021
8ce5aa7
wip
bdraco Jun 14, 2021
fd26ea8
fixes
bdraco Jun 14, 2021
7ccb0ee
fixes
bdraco Jun 14, 2021
3579743
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 14, 2021
e896c90
Update zeroconf/_handlers.py
bdraco Jun 14, 2021
17b634b
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 14, 2021
d0b68e5
wip
bdraco Jun 14, 2021
7370184
typing
bdraco Jun 14, 2021
96a0aa4
no literal
bdraco Jun 14, 2021
52a6b20
no literal
bdraco Jun 14, 2021
1d9ed38
no literal
bdraco Jun 14, 2021
d11bd6c
no literal
bdraco Jun 14, 2021
013cf3c
tweak
bdraco Jun 14, 2021
340a285
tweak
bdraco Jun 14, 2021
f3a17d3
Add the ability for ServiceInfo.dns_addresses to filter by address type
bdraco Jun 14, 2021
a67f3b9
Merge branch 'dns_addresses_by_version' into query_handlers_fixes
bdraco Jun 14, 2021
63d131e
Merge branch 'master' into query_handlers_fixes
bdraco Jun 14, 2021
ffcd3f4
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 14, 2021
eca9429
fixes
bdraco Jun 14, 2021
6ec4e15
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 14, 2021
6c67ef5
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 14, 2021
9d38eb7
Merge branch 'master' into query_handlers_fixes
bdraco Jun 14, 2021
a5a28d3
Merge branch 'master' into query_handlers_fixes
bdraco Jun 14, 2021
800c907
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 15, 2021
ba0cb5c
Add is_recent property to DNSRecord
bdraco Jun 15, 2021
aa54801
Merge branch 'is_recent' into query_handlers_fixes
bdraco Jun 15, 2021
f07a5f3
reduce
bdraco Jun 15, 2021
8ed696c
Add is_recent property to DNSRecord
bdraco Jun 15, 2021
47cca0a
Merge branch 'is_recent' into query_handlers_fixes
bdraco Jun 15, 2021
4164f3f
fixes
bdraco Jun 15, 2021
72ef24a
Add is_recent property to DNSRecord
bdraco Jun 15, 2021
52251f1
Merge branch 'is_recent' into query_handlers_fixes
bdraco Jun 15, 2021
4d1df72
Merge remote-tracking branch 'upstream/master' into query_handlers_fixes
bdraco Jun 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,107 @@ def test_unicast_response():
zc.close()


def test_qu_response():
"""Handle multicast incoming with the QU bit set."""
# instantiate a zeroconf instance
zc = Zeroconf(interfaces=['127.0.0.1'])

# service definition
type_ = "_test-srvc-type._tcp.local."
other_type_ = "_notthesame._tcp.local."
name = "xxxyyy"
registration_name = "%s.%s" % (name, type_)
registration_name2 = "%s.%s" % (name, other_type_)
desc = {'path': '/~paulsm/'}
info = ServiceInfo(
type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[socket.inet_aton("10.0.1.2")]
)
info2 = ServiceInfo(
other_type_,
registration_name2,
80,
0,
0,
desc,
"ash-other.local.",
addresses=[socket.inet_aton("10.0.4.2")],
)
# register
zc.register_service(info)

def _validate_complete_response(query, out):
assert out.id == query.id
has_srv = has_txt = has_a = False
nbr_additionals = 0
nbr_answers = len(out.answers)
nbr_authorities = len(out.authorities)
for answer in out.additionals:
nbr_additionals += 1
if answer.type == const._TYPE_SRV:
has_srv = True
elif answer.type == const._TYPE_TXT:
has_txt = True
elif answer.type == const._TYPE_A:
has_a = True
assert nbr_answers == 1 and nbr_additionals == 3 and nbr_authorities == 0
assert has_srv and has_txt and has_a

# With QU should respond to only unicast when the answer has been recently multicast
query = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_AA)
question = r.DNSQuestion(info.type, const._TYPE_PTR, const._CLASS_IN)
question.unique = True # Set the QU bit
assert question.unicast is True
query.add_question(question)

unicast_out, multicast_out = zc.query_handler.response(
r.DNSIncoming(query.packets()[0]), "1.2.3.4", const._MDNS_PORT
)
assert multicast_out is None
_validate_complete_response(query, unicast_out)

_clear_cache(zc)
# With QU should respond to only multicast since the response hasn't been seen since 75% of the ttl
query = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_AA)
question = r.DNSQuestion(info.type, const._TYPE_PTR, const._CLASS_IN)
question.unique = True # Set the QU bit
assert question.unicast is True
query.add_question(question)
unicast_out, multicast_out = zc.query_handler.response(
r.DNSIncoming(query.packets()[0]), "1.2.3.4", const._MDNS_PORT
)
assert unicast_out is None
_validate_complete_response(query, multicast_out)

# With QU set and an authorative answer (probe) should respond to both unitcast and multicast since the response hasn't been seen since 75% of the ttl
query = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_AA)
question = r.DNSQuestion(info.type, const._TYPE_PTR, const._CLASS_IN)
question.unique = True # Set the QU bit
assert question.unicast is True
query.add_question(question)
query.add_authorative_answer(info2.dns_pointer())
unicast_out, multicast_out = zc.query_handler.response(
r.DNSIncoming(query.packets()[0]), "1.2.3.4", const._MDNS_PORT
)
_validate_complete_response(query, unicast_out)
_validate_complete_response(query, multicast_out)

_inject_response(zc, r.DNSIncoming(multicast_out.packets()[0]))
# With the cache repopulated; should respond to only unicast when the answer has been recently multicast
query = r.DNSOutgoing(const._FLAGS_QR_QUERY | const._FLAGS_AA)
question = r.DNSQuestion(info.type, const._TYPE_PTR, const._CLASS_IN)
question.unique = True # Set the QU bit
assert question.unicast is True
query.add_question(question)
unicast_out, multicast_out = zc.query_handler.response(
r.DNSIncoming(query.packets()[0]), "1.2.3.4", const._MDNS_PORT
)
assert multicast_out is None
_validate_complete_response(query, unicast_out)
# unregister
zc.unregister_service(info)
zc.close()


def test_known_answer_supression():
zc = Zeroconf(interfaces=['127.0.0.1'])
type_ = "_knownservice._tcp.local."
Expand Down
47 changes: 42 additions & 5 deletions zeroconf/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ def __init__(self, cache: DNSCache, msg: DNSIncoming, ucast_source: bool) -> Non
self._ucast: _RecordSetType = {RecordSetKeys.Answers: set(), RecordSetKeys.Additionals: set()}
self._mcast: _RecordSetType = {RecordSetKeys.Answers: set(), RecordSetKeys.Additionals: set()}

def add_qu_question_response(
self,
answers: Set[DNSRecord],
additionals: Set[DNSRecord],
) -> None:
"""Generate a response to a multicast QU query."""
self._add_qu_question_response_to_target(answers, RecordSetKeys.Answers)
self._add_qu_question_response_to_target(additionals, RecordSetKeys.Additionals)

def _add_qu_question_response_to_target(self, target: Set[DNSRecord], answer_type: RecordSetKeys) -> None:
"""Add part of the QU response."""
for record in target:
if self._is_probe:
self._ucast[answer_type].add(record)
if not self._has_mcast_within_one_quarter_ttl(record):
self._mcast[answer_type].add(record)
elif not self._is_probe:
self._ucast[answer_type].add(record)

def add_ucast_question_response(self, answers: Set[DNSRecord], additionals: Set[DNSRecord]) -> None:
"""Generate a response to a unicast query."""
self._ucast[RecordSetKeys.Answers].update(answers)
Expand Down Expand Up @@ -119,8 +138,23 @@ def _construct_outgoing_from_record_set(
out.add_answer_at_time(answer, 0)
for additional in rrset[RecordSetKeys.Additionals]:
out.add_additional_answer(additional)

return out

def _has_mcast_within_one_quarter_ttl(self, record: DNSRecord) -> bool:
"""Check to see if a record has been mcasted recently.

https://datatracker.ietf.org/doc/html/rfc6762#section-5.4
When receiving a question with the unicast-response bit set, a
responder SHOULD usually respond with a unicast packet directed back
to the querier. However, if the responder has not multicast that
record recently (within one quarter of its TTL), then the responder
SHOULD instead multicast the response so as to keep all the peer
caches up to date
"""
maybe_entry = self._cache.get(record)
return bool(maybe_entry and maybe_entry.is_recent(self._now))

def _suppress_mcasts_from_last_second(self, records: Set[DNSRecord]) -> None:
"""Remove any records that were already sent in the last second."""
records -= set(record for record in records if self._has_mcast_record_in_last_second(record))
Expand Down Expand Up @@ -226,11 +260,14 @@ def response( # pylint: disable=unused-argument

for question in msg.questions:
all_answers = self._answer_any_question(msg, question)
if ucast_source:
query_res.add_ucast_question_response(*all_answers)
# We always multicast as well even if its a unicast
# source as long as we haven't done it recently (75% of ttl)
query_res.add_mcast_question_response(*all_answers)
if not ucast_source and question.unicast:
query_res.add_qu_question_response(*all_answers)
else:
if ucast_source:
query_res.add_ucast_question_response(*all_answers)
# We always multicast as well even if its a unicast
# source as long as we haven't done it recently (75% of ttl)
query_res.add_mcast_question_response(*all_answers)

return query_res.outgoing_unicast(), query_res.outgoing_multicast()

Expand Down