From 10580c4b4bffd85420b58aa727fb102727391771 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 1 May 2017 06:11:54 +0200 Subject: [PATCH] DNS checking API --- api/dnshelper.py | 129 ++++++++++++++++++++++++++++ api/views.py | 7 +- domains/urls.py | 1 + domains/views.py | 8 ++ static/img/loader.gif | Bin 0 -> 3208 bytes templates/domains/domain_check.html | 79 +++++++++++++++++ 6 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 api/dnshelper.py create mode 100644 static/img/loader.gif create mode 100644 templates/domains/domain_check.html 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 0000000000000000000000000000000000000000..3288d1035d70bb86517e2c233f1a904e41f06b29 GIT binary patch literal 3208 zcmc(iX;4#H9>pJdFE7h`I{IF)0|5<6L}(j=N}5%L009EB2nYfyF)E0PvIqo$u!IC; z4PgyY5|S9AEh38G)(9eq4TbH7_UHg@yWrlIJ$6smIADL7s^P;_O;ykRc9soXl`UC*LwQJXkii*0rx|*7rI2=x7WaRkx_~XZqFJ8R3c=2Kg zf@aSAv8+BJ8+^hyay>(QR@t*blbKzsf0}bscEqRc5Hd3o(-N5RyW=zWB*zQw6Zh>* z2CROCDAbu#D`)S|J_o(lL9Yn3l*+8RdiRD_>iNz$#_IAzCna&Wl5 zSF_(rRCDD!wi#i8oAm&jYtn2_@VB%2-H*G%bN#|(6R6N?wM)3u`PiGzwuX7qmTgyF zpE)h0kuoxQ9?=kW7Y!=R@DmhU9)vwT*EZWzJ zrt+=2tqFts72yIp?|gvdLhs8Hfku^Z(){gmN%Y=K#P|%fkvgUj~HfIp3CuXqCtYGtJ#me+n+-LmP( z*XNuk%!aH8bIE@_Bj46>M*dSro|7<6vZ7WUHh5YQzN$>IJFqCb|CT!wj~R2C2%=q{ zpt8rzY$aw?W?=Ustv{jo?Ow@ZRkLe<)NItY>Cyhle*wR59dTdF6(@{5^ zAQBOB*hNtc3bkY-8{Cm$nFS@elbTtSqrt7MB{h_4y+~`!mVa}?c&N>&?P}GqdMuhQ z&@TD5Czd((DcG_Su~dKKV)Pj$-qi1WHM8_vc^O4?^!oY|tmK~i!{fjd&@_1E(T~r7 z_REZy&hMT^ySJB3W7l$4YhR`M(J7S5S~+4Q&3HPa)z%zPpisOp$^ zTEe99ig2$5_qFr!$;7A6CJ}PJmRhli>w?LC}Y`#HLGy6 zMU4EhL~dKCN5Ut;U2jd*83ShBNiu zcJB0l9>1Modc?-oM<R4?}3g}UJ%@K);kriq>)e*rh%hdqM)5Q)*+O8 zXm;SEbs@koiYS!9YXIclSg+5m_s~yrW#kKMdiRszg(gCP5HPmP7L)vCf8@fxUh6qY z@Z#TmkjzAZX{rwE+q|K~F2v5{_@vt%>yT_a#fF03SFt{0RXvDAiaY~K9CgS1O>frXgAjBCS}mEd4mIWZ$=ovd5| zR?GRdU}d6+Q`+JRW)|=v7$)XNkn3yE`!nAiSCvOB1jKT zG<1aK3s<0b0m==egTD#8i(Of=1pGDTOCho0XpIOMQ&P87cVKY1W=C6kIg z9cH=@a&zbm2+`|{(_?YC9fdm?1TY~-pwlBn?>=(~1pDKbco6jloP;0-cqRiwV1A_S zEyV0Dj8Pwy!nekzaN>{)7rgZ&_QLxK{~1yRe865^yx>}+a!ECd>#MMwddow z@CU{l+Rt$xuXuf}?ga{3IAr?Raql^c@a%sI0U5m}HvJ5O1#I%_MMPt#BH>OqUZ{-k zt>4Xzz=%jT*FVW(uYkWyx}9Gw$HdN*qU?Bit#ji(Wi7p-u|_8?h^%szIS^s^fNM}b zgGy>|=cbEufpguY5_6w~&ZLv=Bo06UF9EYIY;Er-1VK)SyF&!|J{axiE1z^(hXwVq zsFS=K-#zC}CcOs^8W{KAt+kK)jYDgDYbCXv{{rwsgqtIU3<910$CJi)s?? z_t8k{>7*0~4l~LLF7$WXT5OSq5QCTbP_l!SN|{R}3D&eWA8~0ltWh1IL+ZBX4rRSt zWF6Om3WDMu4xK^1(BF`2cL}rUCzhHAB`@j5&R-yk_l*t;mPGY|u2^o|myvcOdrg0W z%=lX;f^Vkqfp?u7*4qQq%A3Mpf!xspWBSKS@O%r*TSM}?dl(@*%{0Jm_8;(h{R__M Bt +
+
+
Checking domain {{ domain.name }}
+
+
+ +
+
+
+
+ + + +{% endblock %} +