Skip to content

Commit 3e40d90

Browse files
authored
Fix internetstandards#1869 - Add potential double query to avoid false DNSSEC negative from cache (internetstandards#1878)
Also removes unused allow_bogus from resolver API
1 parent be4dd2f commit 3e40d90

File tree

1 file changed

+64
-35
lines changed

1 file changed

+64
-35
lines changed

checks/resolver.py

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@
44
import dns
55
from django.conf import settings
66
from dns.edns import EDECode
7-
from dns.exception import ValidationFailure
87
from dns.flags import Flag, EDNSFlag
98
from dns.message import Message, make_query
109
from dns.query import udp_with_fallback
1110
from dns.rdatatype import RdataType
1211
from dns.rdtypes.ANY import TLSA, CAA
13-
from dns.resolver import Resolver, NXDOMAIN, NoAnswer
12+
from dns.resolver import Resolver, NXDOMAIN, NoAnswer, NoNameservers
1413
import socket
1514

1615
DNS_TIMEOUT = 5
@@ -41,44 +40,44 @@ def from_message(cls, message: Message):
4140
return cls(DNSSECStatus.INSECURE)
4241

4342

44-
def dns_resolve_a(qname: str, allow_bogus=True) -> list[str]:
45-
rrset, dnssec_status = dns_resolve(qname, RdataType.A, allow_bogus)
43+
def dns_resolve_a(qname: str) -> list[str]:
44+
rrset, dnssec_status = dns_resolve(qname, RdataType.A)
4645
return [rr.address for rr in rrset]
4746

4847

49-
def dns_resolve_aaaa(qname: str, allow_bogus=True) -> list[str]:
50-
rrset, dnssec_status = dns_resolve(qname, RdataType.AAAA, allow_bogus)
48+
def dns_resolve_aaaa(qname: str) -> list[str]:
49+
rrset, dnssec_status = dns_resolve(qname, RdataType.AAAA)
5150
return [rr.address for rr in rrset]
5251

5352

54-
def dns_resolve_mx(qname: str, allow_bogus=True) -> list[tuple[str, int]]:
55-
rrset, dnssec_status = dns_resolve(qname, RdataType.MX, allow_bogus)
53+
def dns_resolve_mx(qname: str) -> list[tuple[str, int]]:
54+
rrset, dnssec_status = dns_resolve(qname, RdataType.MX)
5655
return [(str(rr.exchange), rr.preference) for rr in rrset]
5756

5857

59-
def dns_resolve_ns(qname: str, allow_bogus=True) -> list[str]:
60-
rrset, dnssec_status = dns_resolve(qname, RdataType.NS, allow_bogus)
58+
def dns_resolve_ns(qname: str) -> list[str]:
59+
rrset, dnssec_status = dns_resolve(qname, RdataType.NS)
6160
return [str(rr.target) for rr in rrset]
6261

6362

64-
def dns_resolve_tlsa(qname: str, allow_bogus=True) -> tuple[list[TLSA], DNSSECStatus]:
65-
rrset, dnssec_status = dns_resolve(qname, RdataType.TLSA, allow_bogus)
63+
def dns_resolve_tlsa(qname: str) -> tuple[list[TLSA], DNSSECStatus]:
64+
rrset, dnssec_status = dns_resolve(qname, RdataType.TLSA, guarantee_accurate_secure=True)
6665
return rrset, dnssec_status
6766

6867

69-
def dns_resolve_txt(qname: str, allow_bogus=True) -> list[str]:
70-
rrset, dnssec_status = dns_resolve(qname, RdataType.TXT, allow_bogus)
68+
def dns_resolve_txt(qname: str) -> list[str]:
69+
rrset, dnssec_status = dns_resolve(qname, RdataType.TXT)
7170
return ["".join([dns.rdata._escapify(s) for s in rr.strings]) for rr in rrset]
7271

7372

74-
def dns_resolve_spf(qname: str, allow_bogus=True) -> Optional[str]:
75-
strings = dns_resolve_txt(qname, allow_bogus)
73+
def dns_resolve_spf(qname: str) -> Optional[str]:
74+
strings = dns_resolve_txt(qname)
7675
spf_records = [s for s in strings if s.lower().startswith("v=spf1")]
7776
return spf_records[0] if len(spf_records) == 1 else None
7877

7978

80-
def dns_resolve_soa(qname: str, allow_bogus=True, raise_on_no_answer=True) -> DNSSECStatus:
81-
rrset, dnssec_status = dns_resolve(qname, RdataType.SOA, allow_bogus, raise_on_no_answer)
79+
def dns_resolve_soa(qname: str, raise_on_no_answer=True) -> DNSSECStatus:
80+
rrset, dnssec_status = dns_resolve(qname, RdataType.SOA, raise_on_no_answer, guarantee_accurate_secure=True)
8281
return dnssec_status
8382

8483

@@ -89,7 +88,9 @@ def dns_resolve_caa(qname: str) -> tuple[str, Iterable[CAA.CAA]]:
8988
"""
9089
while True:
9190
try:
92-
answer = _get_resolver().resolve(dns.name.from_text(qname), RdataType.CAA, raise_on_no_answer=True)
91+
answer = _get_resolver(cd_flag=True).resolve(
92+
dns.name.from_text(qname), RdataType.CAA, raise_on_no_answer=True
93+
)
9394
return str(answer.canonical_name), answer.rrset
9495
except (NoAnswer, NXDOMAIN):
9596
qname = dns_climb_tree(qname)
@@ -98,7 +99,7 @@ def dns_resolve_caa(qname: str) -> tuple[str, Iterable[CAA.CAA]]:
9899

99100

100101
def dns_resolve_reverse(ipaddr: str) -> list[str]:
101-
answer = _get_resolver().resolve_address(ipaddr)
102+
answer = _get_resolver(cd_flag=True).resolve_address(ipaddr)
102103
return [rr.to_text() for rr in answer.rrset]
103104

104105

@@ -111,11 +112,32 @@ def dns_check_ns_connectivity(probe_qname: str, target_ip: str, port: int = 53)
111112
return False
112113

113114

114-
def dns_resolve(qname: str, rr_type: RdataType, allow_bogus=True, raise_on_no_answer=True):
115-
answer = _get_resolver().resolve(dns.name.from_text(qname), rr_type, raise_on_no_answer=raise_on_no_answer)
116-
dnssec_status = DNSSECStatus.from_message(answer.response)
117-
if dnssec_status == DNSSECStatus.BOGUS and not allow_bogus:
118-
raise ValidationFailure()
115+
def dns_resolve(qname: str, rr_type: RdataType, raise_on_no_answer=True, guarantee_accurate_secure=False):
116+
"""
117+
Resolve the provided qname/record type.
118+
Returns the RRset and the DNSSEC status, with a caveat.
119+
120+
raise_on_no_answer: if True, raises NoAnswer for no answer, if False, no exception raised
121+
122+
guarantee_accurate_secure:
123+
Certain caching scenarios may lead us to falsely mark a response as insecure, when it is secure,
124+
due to a missing AD bit when a response for the same qname was cached to resolve a different query,
125+
in combination with our CD flag.
126+
This is OK in most cases, but when it is not, we need to do a double query to prevent this,
127+
enabled with guarantee_accurate_secure=True.
128+
https://github.com/internetstandards/Internet.nl/issues/1869
129+
"""
130+
resolve_params = {"qname": dns.name.from_text(qname), "rdtype": rr_type, "raise_on_no_answer": raise_on_no_answer}
131+
if guarantee_accurate_secure:
132+
try:
133+
answer = _get_resolver(cd_flag=False).resolve(**resolve_params)
134+
dnssec_status = DNSSECStatus.from_message(answer.response)
135+
except NoNameservers: # dnspython's translation for servfail
136+
answer = _get_resolver(cd_flag=True).resolve(**resolve_params)
137+
dnssec_status = DNSSECStatus.BOGUS
138+
else:
139+
answer = _get_resolver(cd_flag=True).resolve(**resolve_params)
140+
dnssec_status = DNSSECStatus.from_message(answer.response)
119141
return answer.rrset, dnssec_status
120142

121143

@@ -126,22 +148,29 @@ def dns_climb_tree(qname: str) -> Optional[str]:
126148
return parent.to_text()
127149

128150

129-
_resolver = None
151+
_resolver_without_cd = None
152+
_resolver_with_cd = None
130153

131154

132-
def _get_resolver():
155+
def _get_resolver(cd_flag: bool):
133156
# Resolvers are thread safe once configured
134-
global _resolver
135-
if not _resolver:
136-
_resolver = _create_resolver()
137-
return _resolver
138-
139-
140-
def _create_resolver() -> Resolver:
157+
global _resolver_with_cd
158+
if not _resolver_with_cd:
159+
_resolver_with_cd = _create_resolver(cd_flag=True)
160+
global _resolver_without_cd
161+
if not _resolver_without_cd:
162+
_resolver_without_cd = _create_resolver(cd_flag=False)
163+
if cd_flag:
164+
return _resolver_with_cd
165+
return _resolver_without_cd
166+
167+
168+
def _create_resolver(cd_flag: bool) -> Resolver:
141169
resolver = Resolver(configure=False)
142170
resolver.nameservers = [socket.gethostbyname(settings.RESOLVER_INTERNAL_VALIDATING)]
143171
resolver.edns = True
144-
resolver.flags = Flag.CD
172+
if cd_flag:
173+
resolver.flags = Flag.CD
145174
resolver.ednsflags = EDNSFlag.DO
146175
resolver.lifetime = DNS_TIMEOUT
147176
return resolver

0 commit comments

Comments
 (0)