Skip to content

Commit a1030fb

Browse files
committed
wip
1 parent 71ab0fd commit a1030fb

File tree

2 files changed

+135
-1
lines changed

2 files changed

+135
-1
lines changed

api/api/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from datetime import timedelta
88

99
from django.conf.global_settings import PASSWORD_HASHERS as DEFAULT_PASSWORD_HASHERS
10+
import dns.resolver
1011

1112
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
1213

@@ -160,6 +161,13 @@
160161
]
161162

162163
DESECSTACK_DOMAIN = os.environ["DESECSTACK_DOMAIN"]
164+
RESOLVERS = [
165+
'9.9.9.9', '2620:fe::fe', # Quad9
166+
'1.1.1.1', '2606:4700:4700::1111', # Cloudflare
167+
'8.8.8.8', '2001:4860:4860::8888', # Google
168+
]
169+
resolver = dns.resolver.Resolver(configure=False)
170+
resolver.nameserver = RESOLVERS
163171

164172
# default NS records
165173
DEFAULT_NS = [name + "." for name in os.environ["DESECSTACK_NS"].strip().split()]

api/desecapi/models/domains.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from __future__ import annotations
22

33
from functools import cached_property
4+
from functools import cache
5+
from socket import getaddrinfo
46

5-
import dns
7+
import dns.name
8+
import dns.rdataclass
9+
import dns.rdatatype
10+
import dns.resolver
611
import psl_dns
712
from django.conf import settings
813
from django.contrib.auth.models import AnonymousUser
@@ -52,6 +57,19 @@ class RenewalState(models.IntegerChoices):
5257
NOTIFIED = 2
5358
WARNED = 3
5459

60+
class DelegationStatus(models.IntegerChoices):
61+
NOT_DELEGATED = 0
62+
ELSEWHERE = 1
63+
PARTIAL = 2
64+
EXCLUSIVE = 3
65+
MULTI = 4
66+
67+
class SecurityStatus(models.IntegerChoices):
68+
INSECURE = 0
69+
BOGUS = 1
70+
SECURE_EXCLUSIVE = 2
71+
SECURE = 3
72+
5573
created = models.DateTimeField(auto_now_add=True)
5674
name = models.CharField(
5775
max_length=191, unique=True, validators=validate_domain_name
@@ -177,6 +195,114 @@ def is_registrable(self):
177195

178196
return True
179197

198+
@cache
199+
def dns_auth_ns(self) -> set[dns.name.Name]:
200+
"""Queries the DNS to determine the names of authoritative NS
201+
servers for this domain."""
202+
name = dns.name.from_text(self.name)
203+
204+
# determine parent
205+
parent = name.parent()
206+
while len(parent):
207+
ns_response = settings.resolver.resolve(parent, dns.rdatatype.NS)
208+
if ns_response.answer:
209+
break
210+
parent = parent.parent()
211+
212+
# extract NS records at the parent
213+
try:
214+
ns = ns_response.find_rrset(ns_response.answer, parent, dns.rdataclass.IN, dns.rdatatype.NS)
215+
except KeyError: # TODO see if dnspython has an API that includes the except clause already
216+
ns = set()
217+
218+
# look up addresses for parent's NS records
219+
addr = {ip for rr in ns for ip in self._lookup(rr.target)}
220+
221+
# query these NS to determine authoritative NS names
222+
self.resolver = dns.resolver.Resolver()
223+
self.resolver.nameserver = list(addr)
224+
try:
225+
ns_authoritative = dns.resolver.resolve(dns.name.from_text(self.name), dns.rdatatype.NS)
226+
auth_ns_names = {name.target for name in ns_authoritative}
227+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
228+
auth_ns_names = set()
229+
230+
return auth_ns_names
231+
232+
@cache # located at object-level to start with clear cache for new objects
233+
def _lookup(target) -> set[str]:
234+
try:
235+
addrinfo = getaddrinfo(str(target), None)
236+
except OSError:
237+
return set()
238+
return {v[-1][0] for v in addrinfo}
239+
240+
@cache
241+
def dns_auth_ds(self) -> set: # TODO update type hint
242+
"""Queries the DNS to determine the authoritative DS records
243+
of this domain."""
244+
name = dns.name.from_text(self.name)
245+
res = settings.resolver.resolve(name, dns.rdatatype.DS)
246+
try:
247+
ds = res.find_rrset(
248+
res.answer, name, dns.rdataclass.IN, dns.rdatatype.DS
249+
)
250+
except KeyError:
251+
ds = set()
252+
return {rr.to_text() for rr in ds} # TODO use dnspython objects instead of str
253+
254+
def update_dns_delegation_status(self) -> DelegationStatus:
255+
"""Queries the DNS to determine the delegation status of this domian and
256+
update the delegation status on record."""
257+
auth_ns_names = self.dns_auth_ns()
258+
our_ns_names = {dns.name.from_text(ns) for ns in settings.DEFAULT_NS}
259+
260+
if our_ns_names == auth_ns_names:
261+
# just ours
262+
self.delegation_status = self.DelegationStatus.EXCLUSIVE
263+
elif our_ns_names < auth_ns_names:
264+
# all of ours, and others
265+
self.delegation_status = self.DelegationStatus.MULTI
266+
elif our_ns_names & auth_ns_names:
267+
# some of ours, and others
268+
self.delegation_status = self.DelegationStatus.PARTIAL
269+
elif auth_ns_names:
270+
# none of ours, but not empty
271+
self.delegation_status = self.DelegationStatus.ELSEWHERE
272+
else:
273+
# empty
274+
self.delegation_status = self.DelegationStatus.NOT_DELEGATED
275+
276+
return self.delegation_status
277+
278+
def update_dns_security_status(self) -> SecurityStatus:
279+
"""Queries the DNS to determine the security status of this domain and
280+
updates the security status on record."""
281+
if self.delegation_status not in [self.DelegationStatus.MULTI, self.DelegationStatus.EXCLUSIVE]:
282+
self.security_status = None
283+
return None
284+
285+
auth_ds_set = self.dns_auth_ds()
286+
287+
# Compute overlap of delegation DS records with ours
288+
our_ds_set = set()
289+
for key in self.keys: # TODO upgrade this to dnspython objects
290+
# Only digest type 2 is mandatory to implement; delegation only fully set up if present
291+
our_ds_set |= {
292+
ds.lower() for ds in key["ds"] if ds.split(" ")[2] == "2"
293+
}
294+
295+
if our_ds_set == auth_ds_set:
296+
self.security_status = self.SecurityStatus.SECURE_EXCLUSIVE
297+
elif our_ds_set < auth_ds_set:
298+
self.security_status = self.SecurityStatus.SECURE
299+
elif auth_ds_set == set():
300+
self.security_status = self.SecurityStatus.INSECURE
301+
else:
302+
self.security_status = self.SecurityStatus.BOGUS
303+
304+
return self.security_status
305+
180306
@property
181307
def keys(self):
182308
if not self._keys:

0 commit comments

Comments
 (0)