11from __future__ import annotations
22
3- from functools import cached_property
4-
5- import dns
3+ from functools import cache , cached_property
4+ from socket import getaddrinfo
5+
6+ import dns .name
7+ import dns .rdataclass
8+ import dns .rdatatype
9+ import dns .rdtypes
10+ import dns .resolver
611import psl_dns
712from django .conf import settings
813from django .contrib .auth .models import AnonymousUser
914from django .core .exceptions import ValidationError
1015from django .db import models
1116from django .db .models import CharField , F , Manager , Q , Value
1217from django .db .models .functions import Concat , Length
18+ from django .utils import timezone
1319from django_prometheus .models import ExportModelOperationsMixin
1420from dns .exception import Timeout
1521from dns .resolver import NoNameservers
2329
2430psl = psl_dns .PSL (resolver = settings .PSL_RESOLVER , timeout = 0.5 )
2531
32+ # CHECKING DISABLED general-purpose resolver for queries to the public DNS
33+ resolver_CD = dns .resolver .Resolver (configure = False )
34+ resolver_CD .nameservers = settings .RESOLVERS
35+ resolver_CD .flags = (resolver_CD .flags or 0 ) | dns .flags .CD | dns .flags .AD | dns .flags .RD
36+
2637
2738class DomainManager (Manager ):
2839 def filter_qname (self , qname : str , ** kwargs ) -> models .query .QuerySet :
@@ -52,6 +63,25 @@ class RenewalState(models.IntegerChoices):
5263 NOTIFIED = 2
5364 WARNED = 3
5465
66+ class DelegationStatus (models .IntegerChoices ):
67+ NOT_DELEGATED = 0
68+ ELSEWHERE = 1
69+ PARTIAL = 2
70+ EXCLUSIVE = 3
71+ MULTI = 4
72+ ERROR_NXDOMAIN = 128
73+ ERROR_NO_NAMESERVERS = 129
74+ ERROR_TIMEOUT = 130
75+
76+ class SecurityStatus (models .IntegerChoices ):
77+ INSECURE = 0
78+ FOREIGN_KEYS = 1
79+ SECURE_EXCLUSIVE = 2
80+ SECURE = 3
81+ ERROR_NXDOMAIN = 128
82+ ERROR_NO_NAMESERVERS = 130
83+ ERROR_TIMEOUT = 131
84+
5585 created = models .DateTimeField (auto_now_add = True )
5686 name = models .CharField (
5787 max_length = 191 , unique = True , validators = validate_domain_name
@@ -63,6 +93,26 @@ class RenewalState(models.IntegerChoices):
6393 choices = RenewalState .choices , db_index = True , default = RenewalState .IMMORTAL
6494 )
6595 renewal_changed = models .DateTimeField (auto_now_add = True )
96+ delegation_status = models .IntegerField (
97+ choices = DelegationStatus .choices ,
98+ default = None ,
99+ null = True ,
100+ blank = True ,
101+ )
102+ delegation_status_touched = models .DateTimeField (
103+ default = None , null = True , blank = True
104+ )
105+ delegation_status_changed = models .DateTimeField (
106+ default = None , null = True , blank = True
107+ )
108+ security_status = models .IntegerField (
109+ choices = SecurityStatus .choices ,
110+ default = None ,
111+ null = True ,
112+ blank = True ,
113+ )
114+ security_status_touched = models .DateTimeField (default = None , null = True , blank = True )
115+ security_status_changed = models .DateTimeField (default = None , null = True , blank = True )
66116
67117 _keys = None
68118 objects = DomainManager ()
@@ -177,6 +227,105 @@ def is_registrable(self):
177227
178228 return True
179229
230+ @staticmethod
231+ @cache # located at object-level to start with clear cache for new objects
232+ def _lookup (target ) -> set [str ]:
233+ try :
234+ addrinfo = getaddrinfo (str (target ), None )
235+ except OSError :
236+ return set ()
237+ return {v [- 1 ][0 ] for v in addrinfo }
238+
239+ def update_dns_delegation_status (self ) -> DelegationStatus :
240+ """Queries the DNS to determine the delegation status of this domian and
241+ update the delegation status on record."""
242+ old_delegation_status = self .delegation_status
243+ our_ns_names = {dns .name .from_text (ns ) for ns in settings .DEFAULT_NS }
244+
245+ try :
246+ auth_ns_names = {
247+ rr .target for rr in resolver_CD .resolve (self .name , dns .rdatatype .NS , raise_on_no_answer = False )
248+ }
249+ except dns .resolver .NXDOMAIN :
250+ self .delegation_status = self .DelegationStatus .ERROR_NXDOMAIN
251+ except dns .resolver .NoNameservers :
252+ self .delegation_status = self .DelegationStatus .ERROR_NO_NAMESERVERS
253+ except dns .resolver .LifetimeTimeout :
254+ self .delegation_status = self .DelegationStatus .ERROR_TIMEOUT
255+ else :
256+
257+ if our_ns_names == auth_ns_names :
258+ # just ours
259+ self .delegation_status = self .DelegationStatus .EXCLUSIVE
260+ elif our_ns_names < auth_ns_names :
261+ # all of ours, and others
262+ self .delegation_status = self .DelegationStatus .MULTI
263+ elif our_ns_names & auth_ns_names :
264+ # some of ours, and others
265+ self .delegation_status = self .DelegationStatus .PARTIAL
266+ elif auth_ns_names :
267+ # none of ours, but not empty
268+ self .delegation_status = self .DelegationStatus .ELSEWHERE
269+ elif auth_ns_names == set ():
270+ # empty
271+ self .delegation_status = self .DelegationStatus .NOT_DELEGATED
272+ elif auth_ns_names is None :
273+ # error
274+ self .delegation_status = self .DelegationStatus
275+
276+ now = timezone .now ()
277+ self .delegation_status_touched = now
278+ if old_delegation_status != self .delegation_status :
279+ self .delegation_status_changed = now
280+ return self .delegation_status
281+
282+ def update_dns_security_status (self ) -> SecurityStatus :
283+ """Queries the DNS to determine the security status of this domain and
284+ updates the security status on record."""
285+ old_security_status = self .security_status
286+
287+ if self .delegation_status not in [
288+ self .DelegationStatus .MULTI ,
289+ self .DelegationStatus .EXCLUSIVE ,
290+ ]:
291+ self .security_status = None
292+ return None
293+
294+ try :
295+ auth_ds = set (resolver_CD .resolve (self .name , dns .rdatatype .DS , raise_on_no_answer = False ))
296+ except dns .resolver .NXDOMAIN :
297+ self .security_status = self .SecurityStatus .ERROR_NXDOMAIN
298+ except dns .resolver .NoNameservers :
299+ self .delegation_status = self .SecurityStatus .ERROR_NO_NAMESERVERS
300+ except dns .resolver .LifetimeTimeout :
301+ self .delegation_status = self .SecurityStatus .ERROR_TIMEOUT
302+ else :
303+ auth_ds = {ds for ds in auth_ds if ds .digest_type == 2 }
304+
305+ # Compute overlap of delegation DS records with ours
306+ our_ds_set = {
307+ dns .rdata .from_text (rdclass = "IN" , rdtype = "DS" , tok = ds )
308+ for key in self .keys
309+ for ds in key .get ("ds" , [])
310+ if dns .rdata .from_text (rdclass = "IN" , rdtype = "DS" , tok = ds ).digest_type
311+ == 2 # only digest type 2 is mandatory
312+ }
313+
314+ if our_ds_set == auth_ds :
315+ self .security_status = self .SecurityStatus .SECURE_EXCLUSIVE
316+ elif our_ds_set < auth_ds :
317+ self .security_status = self .SecurityStatus .SECURE
318+ elif auth_ds != set ():
319+ self .security_status = self .SecurityStatus .FOREIGN_KEYS
320+ else :
321+ self .security_status = self .SecurityStatus .INSECURE
322+
323+ now = timezone .now ()
324+ self .security_status_touched = now
325+ if old_security_status != self .security_status :
326+ self .security_status_changed = now
327+ return self .security_status
328+
180329 @property
181330 def keys (self ):
182331 if not self ._keys :
0 commit comments