diff --git a/api/dnshelper.py b/api/dnshelper.py new file mode 100644 index 0000000..ce9c378 --- /dev/null +++ b/api/dnshelper.py @@ -0,0 +1,129 @@ +import dns.name +import dns.message +import dns.query +import dns.resolver + +#from collections import defaultdict +# FIXME: DNS timeouts + +def compareRecords(rrset, expected): + result = { + "nameMissing": [], + "rrMissing": [], + "rrExtra": [], + } + + for domain, rrtype, content in expected: + for rrrec in rrset: + if domain == rrrec.name.to_text() and dns.rdatatype.from_text(rrtype) == rrrec.rdtype: + for name in content: + if name not in map(lambda _x: _x.to_text(), rrrec.items): + # record missing + result["rrMissing"].append((domain, rrtype, name)) + + for item in rrrec.items: + if item.to_text() not in content: + # superfluous record + result["rrExtra"].append((domain, rrtype, item.to_text())) + + break + else: + # domain + rr nicht in nameserver + result["nameMissing"].append((domain, rrtype)) + + success = not any(len(_x) > 0 for _x in result.values()) + print("NARF", success, result) + + return success, result + + +def dnsQuery(domain, rrType, nameserverIp): + dname = dns.name.from_text(domain) + req = dns.message.make_query(dname, dns.rdatatype.from_text(rrType)) + resp = dns.query.udp(req, nameserverIp, timeout=2.0) + + if resp.rcode() != dns.rcode.NXDOMAIN: + rrset = resp.answer + resp.authority + resp.additional + return True, rrset + else: + return False, [] + + +def checkDomain(domain, tldNameserver, nameservers): + print(domain, tldNameserver, nameservers) + result = [] + + # build record set + nsRecords = [(domain, "NS", list(ns.name for ns in nameservers))] + glueRecords = [] + for ns in nameservers: + if ns.name.endswith("." + domain): + if ns.glueIPv4 or ns.glueIPv6: + if ns.glueIPv4: + glueRecords.append((ns.name, "A", [ns.glueIPv4])) + if ns.glueIPv6: + glueRecords.append((ns.name, "AAAA", [ns.glueIPv6])) + else: + result.append(("err", "Nameserver %s is under domain %s, but has no glue entries." % (ns.name, domain))) + + print(nsRecords, glueRecords) + + # 1. TLD nameserver + try: + found, rrset = dnsQuery(domain, "ANY", tldNameserver) + if found: + #print(rrset, nsRecords, glueRecords) + success, errors = compareRecords(rrset, nsRecords + glueRecords) + if success: + result.append(("succ", "All records present in TLD nameserver")) + else: + result.append(("err", "Record mismatch between TLD nameserver and WHOIS database", errors)) + else: + result.append(("err", "Domain %s not found in TLD nameserver" % (domain,))) + except (dns.exception.Timeout, OSError): + result.append(("err", "TLD nameserver is currently not reachable")) + + # find other records... + + # 2. your nameservers + for ns in nameservers: + addr = None + if ns.glueIPv4: + addr = ns.glueIPv4 + elif ns.glueIPv6: + addr = ns.glueIPv6 + else: + for rrType in ("A", "AAAA"): + try: + r = dns.resolver.Resolver() + r.timeout = 2.0 + q = r.query(ns.name, rdtype=dns.rdatatype.from_text(rrType)) + addr = q.response.answer[0].items[0].address + except (dns.exception.DNSException, OSError): + pass + + if addr: + err = False + errDict = {"nameMissing": [], "rrMissing": [], "rrExtra": []} + try: + for rec in (nsRecords + glueRecords): + found, rrset = dnsQuery(rec[0], rec[1], addr) + + success, errors = compareRecords(rrset, nsRecords + glueRecords) + if not success: + err = True + for k in errors.keys(): + errDict[k].extend(errors[k]) + + if not err: + result.append(("succ", "Nameserver %s is configured correctly" % ns.name)) + else: + print(" ==> ", errDict, addr) + result.append(("err", "Nameserver %s recordset does not match the database" % (ns.name,), errDict)) + except (dns.exception.DNSException, OSError): + result.append(("err", "Nameserver %s is not reachable (via %s)" % (ns.name, addr))) + + else: + result.append(("err", "Can't resolv an ip address for nameserver %s" % ns.name)) + + return result diff --git a/api/views.py b/api/views.py index 93a833a..56b3e22 100644 --- a/api/views.py +++ b/api/views.py @@ -7,6 +7,7 @@ from django.db.models import Q from whoisdb.models import ASBlock, ASNumber, InetNum from domains.models import Domain, ReverseZone from dnmgmt.settings import TLD_NAMESERVERS +from .dnshelper import checkDomain as helperCheckDomain import ipaddress @@ -187,7 +188,8 @@ def checkDomain(request): ret["success"] = True ret["domain"] = domain.name - ret["result"] = checkDomain(domain.name, TLD_NAMESERVERS, domain.nameservers.all()) + # FIXME: change this if we ever have more than one... + ret["result"] = helperCheckDomain(domain.name, TLD_NAMESERVERS[0], domain.nameservers.all()) except Domain.DoesNotExist: ret["errorMsg"] = "Domain does not exist" @@ -210,7 +212,8 @@ def checkRzone(request): raise ReverseZone.DoesNotExist() ret["success"] = True - ret["result"] = checkDomain(rzone.name, TLD_NAMESERVERS, rzone.nameservers.all()) + # FIXME: change this if we ever have more than one... + ret["result"] = helperCheckDomain(rzone.name, TLD_NAMESERVERS[0], rzone.nameservers.all()) except Domain.DoesNotExist: ret["errorMsg"] = "ReverseZone does not exist" diff --git a/domains/urls.py b/domains/urls.py index 2d1fa50..2b331a9 100644 --- a/domains/urls.py +++ b/domains/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ url(r'^$', domains_views.overview, name='overview'), url(r'domain/create/$', domains_views.DomainCreate.as_view(), name='domain-create'), + url(r'domain/check/(?P[a-z0-9.-]+)/$', domains_views.DomainCheck.as_view(), name='domain-check'), url(r'domain/show/(?P[a-z0-9.-]+)/$', domains_views.DomainDetail.as_view(), name='domain-show'), url(r'domain/edit/(?P[a-z0-9.-]+)/$', domains_views.DomainEdit.as_view(), name='domain-edit'), url(r'domain/delete/(?P[a-z0-9.-]+)/$', domains_views.DomainDelete.as_view(), name='domain-delete'), diff --git a/domains/views.py b/domains/views.py index 276eb3e..7886371 100644 --- a/domains/views.py +++ b/domains/views.py @@ -34,6 +34,14 @@ class DomainCreate(LoginRequiredMixin, CreateView): return kwargs +class DomainCheck(LoginRequiredMixin, DetailView): + model = Domain + slug_field = "name" + slug_url_kwarg = "domain" + context_object_name = "domain" + + template_name = "domains/domain_check.html" + class DomainDetail(LoginRequiredMixin, DetailView): model = Domain slug_field = "name" diff --git a/static/img/loader.gif b/static/img/loader.gif new file mode 100644 index 0000000..3288d10 Binary files /dev/null and b/static/img/loader.gif differ diff --git a/templates/domains/domain_check.html b/templates/domains/domain_check.html new file mode 100644 index 0000000..b7948e2 --- /dev/null +++ b/templates/domains/domain_check.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% load staticfiles %} + +{% block content %} +
+
+
+
Checking domain {{ domain.name }}
+
+
+ +
+
+
+
+
+ + +{% endblock %} +