Skip to content

Commit 7a20fd3

Browse files
authored
Include NSEC records for non-existant types when responding with addresses (#972)
Implements datatracker.ietf.org/doc/html/rfc6762#section-6.2
1 parent 768a23c commit 7a20fd3

2 files changed

Lines changed: 83 additions & 20 deletions

File tree

tests/test_handlers.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ def _process_outgoing_packet(out):
108108
_process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate))
109109

110110
# The additonals should all be suppresed since they are all in the answers section
111+
# There will be one NSEC additional to indicate the lack of AAAA record
111112
#
112-
assert nbr_answers == 4 and nbr_additionals == 0 and nbr_authorities == 0
113+
assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities == 0
113114
nbr_answers = nbr_additionals = nbr_authorities = 0
114115

115116
# unregister
@@ -143,7 +144,9 @@ def _process_outgoing_packet(out):
143144
[r.DNSIncoming(packet) for packet in query.packets()], False
144145
)
145146
_process_outgoing_packet(construct_outgoing_multicast_answers(question_answers.mcast_aggregate))
146-
assert nbr_answers == 4 and nbr_additionals == 0 and nbr_authorities == 0
147+
148+
# There will be one NSEC additional to indicate the lack of AAAA record
149+
assert nbr_answers == 4 and nbr_additionals == 1 and nbr_authorities == 0
147150
nbr_answers = nbr_additionals = nbr_authorities = 0
148151

149152
# unregister
@@ -271,7 +274,9 @@ def test_ptr_optimization():
271274
has_txt = True
272275
elif answer.type == const._TYPE_A:
273276
has_a = True
274-
assert nbr_answers == 1 and nbr_additionals == 3
277+
assert nbr_answers == 1 and nbr_additionals == 4
278+
# There will be one NSEC additional to indicate the lack of AAAA record
279+
275280
assert has_srv and has_txt and has_a
276281

277282
# unregister
@@ -406,7 +411,7 @@ def test_unicast_response():
406411
[r.DNSIncoming(packet) for packet in query.packets()], True
407412
)
408413
for answers in (question_answers.ucast, question_answers.mcast_aggregate):
409-
has_srv = has_txt = has_a = False
414+
has_srv = has_txt = has_a = has_aaaa = has_nsec = False
410415
nbr_additionals = 0
411416
nbr_answers = len(answers)
412417
additionals = set().union(*answers.values())
@@ -418,8 +423,14 @@ def test_unicast_response():
418423
has_txt = True
419424
elif answer.type == const._TYPE_A:
420425
has_a = True
421-
assert nbr_answers == 1 and nbr_additionals == 3
422-
assert has_srv and has_txt and has_a
426+
elif answer.type == const._TYPE_AAAA:
427+
has_aaaa = True
428+
elif answer.type == const._TYPE_NSEC:
429+
has_nsec = True
430+
# There will be one NSEC additional to indicate the lack of AAAA record
431+
assert nbr_answers == 1 and nbr_additionals == 4
432+
assert has_srv and has_txt and has_a and has_nsec
433+
assert not has_aaaa
423434

424435
# unregister
425436
zc.registry.async_remove(info)
@@ -497,7 +508,7 @@ def test_qu_response():
497508
zc.register_service(info)
498509

499510
def _validate_complete_response(answers):
500-
has_srv = has_txt = has_a = False
511+
has_srv = has_txt = has_a = has_aaaa = has_nsec = False
501512
nbr_answers = len(answers.keys())
502513
additionals = set().union(*answers.values())
503514
nbr_additionals = len(additionals)
@@ -509,8 +520,13 @@ def _validate_complete_response(answers):
509520
has_txt = True
510521
elif answer.type == const._TYPE_A:
511522
has_a = True
512-
assert nbr_answers == 1 and nbr_additionals == 3
513-
assert has_srv and has_txt and has_a
523+
elif answer.type == const._TYPE_AAAA:
524+
has_aaaa = True
525+
elif answer.type == const._TYPE_NSEC:
526+
has_nsec = True
527+
assert nbr_answers == 1 and nbr_additionals == 4
528+
assert has_srv and has_txt and has_a and has_nsec
529+
assert not has_aaaa
514530

515531
# With QU should respond to only unicast when the answer has been recently multicast
516532
query = r.DNSOutgoing(const._FLAGS_QR_QUERY)
@@ -635,6 +651,21 @@ def test_known_answer_supression():
635651
assert not question_answers.mcast_aggregate
636652
assert not question_answers.mcast_aggregate_last_second
637653

654+
# Test NSEC record returned when there is no AAAA record and we expectly ask
655+
generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
656+
question = r.DNSQuestion(server_name, const._TYPE_AAAA, const._CLASS_IN)
657+
generated.add_question(question)
658+
for dns_address in info.dns_addresses():
659+
generated.add_answer_at_time(dns_address, now)
660+
packets = generated.packets()
661+
question_answers = zc.query_handler.async_response([r.DNSIncoming(packet) for packet in packets], False)
662+
assert not question_answers.ucast
663+
expected_nsec_record: r.DNSNsec = list(question_answers.mcast_now)[0]
664+
assert const._TYPE_A not in expected_nsec_record.rdtypes
665+
assert const._TYPE_AAAA in expected_nsec_record.rdtypes
666+
assert not question_answers.mcast_aggregate
667+
assert not question_answers.mcast_aggregate_last_second
668+
638669
# Test SRV supression
639670
generated = r.DNSOutgoing(const._FLAGS_QR_QUERY)
640671
question = r.DNSQuestion(registration_name, const._TYPE_SRV, const._CLASS_IN)

zeroconf/_handlers.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@
2626
from typing import Dict, Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union, cast
2727

2828
from ._cache import DNSCache, _UniqueRecordsType
29-
from ._dns import DNSAddress, DNSPointer, DNSQuestion, DNSRRSet, DNSRecord
29+
from ._dns import DNSAddress, DNSNsec, DNSPointer, DNSQuestion, DNSRRSet, DNSRecord
3030
from ._history import QuestionHistory
3131
from ._logger import log
3232
from ._protocol import DNSIncoming, DNSOutgoing
33+
from ._services.info import ServiceInfo
3334
from ._services.registry import ServiceRegistry
3435
from ._updates import RecordUpdate, RecordUpdateListener
3536
from ._utils.time import current_time_millis, millis_to_seconds
3637
from .const import (
3738
_CLASS_IN,
39+
_CLASS_UNIQUE,
3840
_DNS_OTHER_TTL,
3941
_DNS_PTR_MIN_TTL,
4042
_FLAGS_AA,
@@ -44,6 +46,7 @@
4446
_TYPE_A,
4547
_TYPE_AAAA,
4648
_TYPE_ANY,
49+
_TYPE_NSEC,
4750
_TYPE_PTR,
4851
_TYPE_SRV,
4952
_TYPE_TXT,
@@ -56,7 +59,8 @@
5659
_AnswerWithAdditionalsType = Dict[DNSRecord, Set[DNSRecord]]
5760

5861
_MULTICAST_DELAY_RANDOM_INTERVAL = (20, 120)
59-
_RESPOND_IMMEDIATE_TYPES = {_TYPE_SRV, _TYPE_A, _TYPE_AAAA}
62+
_ADDRESS_RECORD_TYPES = {_TYPE_A, _TYPE_AAAA}
63+
_RESPOND_IMMEDIATE_TYPES = {_TYPE_NSEC, _TYPE_SRV, *_ADDRESS_RECORD_TYPES}
6064

6165

6266
class QuestionAnswers(NamedTuple):
@@ -78,6 +82,15 @@ def _message_is_probe(msg: DNSIncoming) -> bool:
7882
return msg.num_authorities > 0
7983

8084

85+
def construct_nsec_record(name: str, types: List[int], now: float) -> DNSNsec:
86+
"""Construct an NSEC record for name and a list of dns types.
87+
88+
This function should only be used for SRV/A/AAAA records
89+
which have a TTL of _DNS_OTHER_TTL
90+
"""
91+
return DNSNsec(name, _TYPE_NSEC, _CLASS_IN | _CLASS_UNIQUE, _DNS_OTHER_TTL, name, types, created=now)
92+
93+
8194
def construct_outgoing_multicast_answers(answers: _AnswerWithAdditionalsType) -> DNSOutgoing:
8295
"""Add answers and additionals to a DNSOutgoing."""
8396
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, multicast=True)
@@ -244,12 +257,23 @@ def _add_pointer_answers(
244257
# Add recommended additional answers according to
245258
# https://tools.ietf.org/html/rfc6763#section-12.1.
246259
dns_pointer = service.dns_pointer(created=now)
247-
if not known_answers.suppresses(dns_pointer):
248-
answer_set[dns_pointer] = {
249-
service.dns_service(created=now),
250-
service.dns_text(created=now),
251-
*service.dns_addresses(created=now),
252-
}
260+
if known_answers.suppresses(dns_pointer):
261+
continue
262+
additionals: Set[DNSRecord] = {service.dns_service(created=now), service.dns_text(created=now)}
263+
additionals |= self._get_address_and_nsec_records(service, now)
264+
answer_set[dns_pointer] = additionals
265+
266+
def _get_address_and_nsec_records(self, service: ServiceInfo, now: float) -> Set[DNSRecord]:
267+
"""Build a set of address records and NSEC records for non-present record types."""
268+
seen_types: Set[int] = set()
269+
records: Set[DNSRecord] = set()
270+
for dns_address in service.dns_addresses(created=now):
271+
seen_types.add(dns_address.type)
272+
records.add(dns_address)
273+
missing_types: Set[int] = _ADDRESS_RECORD_TYPES - seen_types
274+
if missing_types:
275+
records.add(construct_nsec_record(service.server, list(missing_types), now))
276+
return records
253277

254278
def _add_address_answers(
255279
self,
@@ -263,13 +287,21 @@ def _add_address_answers(
263287
for service in self.registry.async_get_infos_server(name):
264288
answers: List[DNSAddress] = []
265289
additionals: Set[DNSRecord] = set()
290+
seen_types: Set[int] = set()
266291
for dns_address in service.dns_addresses(created=now):
292+
seen_types.add(dns_address.type)
267293
if dns_address.type != type_:
268294
additionals.add(dns_address)
269295
elif not known_answers.suppresses(dns_address):
270296
answers.append(dns_address)
271-
for answer in answers:
272-
answer_set[answer] = additionals
297+
missing_types: Set[int] = _ADDRESS_RECORD_TYPES - seen_types
298+
if answers:
299+
if missing_types:
300+
additionals.add(construct_nsec_record(service.server, list(missing_types), now))
301+
for answer in answers:
302+
answer_set[answer] = additionals
303+
elif type_ in missing_types:
304+
answer_set[construct_nsec_record(service.server, list(missing_types), now)] = set()
273305

274306
def _answer_question(
275307
self,
@@ -299,7 +331,7 @@ def _answer_question(
299331
# https://tools.ietf.org/html/rfc6763#section-12.2.
300332
dns_service = service.dns_service(created=now)
301333
if not known_answers.suppresses(dns_service):
302-
answer_set[dns_service] = set(service.dns_addresses(created=now))
334+
answer_set[dns_service] = self._get_address_and_nsec_records(service, now)
303335
if type_ in (_TYPE_TXT, _TYPE_ANY):
304336
dns_text = service.dns_text(created=now)
305337
if not known_answers.suppresses(dns_text):

0 commit comments

Comments
 (0)