44import dns
55from django .conf import settings
66from dns .edns import EDECode
7- from dns .exception import ValidationFailure
87from dns .flags import Flag , EDNSFlag
98from dns .message import Message , make_query
109from dns .query import udp_with_fallback
1110from dns .rdatatype import RdataType
1211from dns .rdtypes .ANY import TLSA , CAA
13- from dns .resolver import Resolver , NXDOMAIN , NoAnswer
12+ from dns .resolver import Resolver , NXDOMAIN , NoAnswer , NoNameservers
1413import socket
1514
1615DNS_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
100101def 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