Files
dnstest/backend/internal/checker/parent.go
robertas_stauskas a70f3262e0 Initial commit: DNS Test - DNS health checking tool
Go backend (miekg/dns) + Nuxt 3 frontend (Tailwind CSS v4).

8 check categories, 52 checks total:
- Overview: @ record, WWW, MX with ASN/provider lookup
- Domain Registration: expiry, registrar (RDAP + whois fallback)
- Parent Delegation: NS records, glue, consistency
- Nameservers: 17 checks (reachability, auth, recursion, TCP/UDP, AXFR, etc.)
- SOA: serial consistency, timing values
- Mail (MX): 11 checks (CNAME, PTR, public IPs, consistency)
- Mail Auth: SPF, DKIM, DMARC
- WWW: A record, CNAME

Features:
- SSE streaming (results appear as each category completes)
- SQLite history (modernc.org/sqlite)
- Rate limiting, CORS, request logging
- Dark mode, responsive design
2026-03-20 13:39:57 +02:00

314 lines
10 KiB
Go

package checker
import (
"fmt"
"sort"
"strings"
"time"
"github.com/intodns/backend/internal/resolver"
"github.com/miekg/dns"
)
// checkParent runs the 5 parent-delegation checks.
func checkParent(domain string, r *resolver.Resolver) Category {
cat := Category{Name: "parent", Title: "Parent Delegation"}
domain = dns.Fqdn(domain)
parts := dns.SplitDomainName(domain)
if len(parts) < 2 {
cat.Checks = append(cat.Checks, CheckResult{
ID: "parent-ns-records", Title: "Parent NS Records",
Status: StatusFail, Message: "Cannot determine parent zone",
})
return cat
}
parent := dns.Fqdn(strings.Join(parts[1:], "."))
// Find parent zone nameservers.
parentNSNames := findNSForZone(parent, r)
parentNSIPs := resolveNames(parentNSNames, r)
// Query a parent NS for delegation NS records.
var delegationNS []string
var glueA []string
var glueAAAA []string
var queryServer string
for _, pip := range parentNSIPs {
resp, err := r.QueryNoRecurse(domain, pip, dns.TypeNS)
if err != nil {
continue
}
for _, rr := range resp.Ns {
if ns, ok := rr.(*dns.NS); ok {
delegationNS = appendUniqLower(delegationNS, ns.Ns)
}
}
for _, rr := range resp.Answer {
if ns, ok := rr.(*dns.NS); ok {
delegationNS = appendUniqLower(delegationNS, ns.Ns)
}
}
// Collect glue records from additional section.
for _, rr := range resp.Extra {
switch v := rr.(type) {
case *dns.A:
glueA = append(glueA, fmt.Sprintf("%s -> %s", v.Hdr.Name, v.A.String()))
case *dns.AAAA:
glueAAAA = append(glueAAAA, fmt.Sprintf("%s -> %s", v.Hdr.Name, v.AAAA.String()))
}
}
if len(delegationNS) > 0 {
queryServer = pip
break
}
}
// 1. parent-ns-records
cat.Checks = append(cat.Checks, checkParentNSRecords(delegationNS, queryServer))
// 2. parent-ns-ips
cat.Checks = append(cat.Checks, checkParentNSIPs(delegationNS, r))
// 3. parent-glue
cat.Checks = append(cat.Checks, checkParentGlue(delegationNS, domain, glueA, glueAAAA, r))
// 4. parent-ns-count
cat.Checks = append(cat.Checks, checkParentNSCount(delegationNS))
// 5. parent-consistency
cat.Checks = append(cat.Checks, checkParentConsistency(domain, delegationNS, r))
return cat
}
func checkParentNSRecords(delegationNS []string, server string) CheckResult {
start := time.Now()
res := CheckResult{ID: "parent-ns-records", Title: "Parent NS Records"}
defer func() { res.DurationMs = measureDuration(start) }()
if len(delegationNS) == 0 {
res.Status = StatusFail
res.Message = "Your domain does not have NS records at the parent zone. This means your domain cannot be resolved. You need to configure NS records at your domain registrar."
return res
}
res.Status = StatusPass
res.Message = fmt.Sprintf("Found %d nameservers at parent zone (queried %s): %s", len(delegationNS), server, strings.Join(delegationNS, ", "))
for _, ns := range delegationNS {
res.Details = append(res.Details, ns)
}
return res
}
func checkParentNSIPs(delegationNS []string, r *resolver.Resolver) CheckResult {
start := time.Now()
res := CheckResult{ID: "parent-ns-ips", Title: "Parent NS IP Addresses"}
defer func() { res.DurationMs = measureDuration(start) }()
if len(delegationNS) == 0 {
res.Status = StatusFail
res.Message = "No parent NS to check"
return res
}
allResolved := true
var unresolvedNS []string
for _, ns := range delegationNS {
ips := resolveNS(ns, r)
if len(ips) == 0 {
allResolved = false
unresolvedNS = append(unresolvedNS, ns)
res.Details = append(res.Details, fmt.Sprintf("%s has no A/AAAA records — cannot be reached", ns))
} else {
res.Details = append(res.Details, fmt.Sprintf("%s resolves to %s", ns, strings.Join(ips, ", ")))
}
}
if allResolved {
res.Status = StatusPass
res.Message = "All nameservers listed at parent have resolvable IP addresses"
} else {
res.Status = StatusFail
res.Message = fmt.Sprintf("The following nameservers cannot be resolved to IP addresses: %s. Make sure these hostnames have A or AAAA records configured.", strings.Join(unresolvedNS, ", "))
}
return res
}
func checkParentGlue(delegationNS []string, domain string, glueA, glueAAAA []string, r *resolver.Resolver) CheckResult {
start := time.Now()
res := CheckResult{ID: "parent-glue", Title: "Glue Records"}
defer func() { res.DurationMs = measureDuration(start) }()
// Check which NS are in-bailiwick.
var inBailiwick []string
for _, ns := range delegationNS {
if isInBailiwick(ns, domain) {
inBailiwick = append(inBailiwick, ns)
}
}
if len(inBailiwick) == 0 {
res.Status = StatusInfo
res.Message = "Your nameservers are not under your domain, so glue records are not required"
return res
}
allGlue := append(glueA, glueAAAA...)
if len(allGlue) == 0 {
res.Status = StatusFail
res.Message = fmt.Sprintf("Your nameservers (%s) are under your own domain (in-bailiwick), which requires glue records at the parent zone. No glue records were found. Contact your registrar to add glue (A) records for your nameservers.", strings.Join(inBailiwick, ", "))
return res
}
// Compare glue IPs with actual IPs from the nameservers themselves.
glueMap := make(map[string][]string) // ns -> glue IPs
for _, g := range glueA {
parts := strings.SplitN(g, " -> ", 2)
if len(parts) == 2 {
glueMap[strings.ToLower(parts[0])] = append(glueMap[strings.ToLower(parts[0])], parts[1])
}
}
var mismatches []string
for _, ns := range inBailiwick {
nsLower := strings.ToLower(ns)
glueIPs := glueMap[nsLower]
actualIPs := resolveNS(ns, r)
if len(glueIPs) > 0 && len(actualIPs) > 0 {
glueSorted := sortedStrings(glueIPs)
// Only compare A records (filter out IPv6 from actual).
var actualV4 []string
for _, ip := range actualIPs {
if !strings.Contains(ip, ":") {
actualV4 = append(actualV4, ip)
}
}
actualSorted := sortedStrings(actualV4)
if strings.Join(glueSorted, ",") != strings.Join(actualSorted, ",") {
mismatches = append(mismatches, fmt.Sprintf("For %s the parent reported: %v and your nameservers reported: %v", ns, glueSorted, actualSorted))
}
}
}
if len(mismatches) > 0 {
res.Status = StatusWarn
res.Message = "The A records (glue) from the parent zone are different from those reported by your nameservers. Make sure your parent zone has the same IP addresses for your nameservers as your actual DNS configuration."
res.Details = mismatches
res.Details = append(res.Details, "")
res.Details = append(res.Details, "Glue records from parent:")
res.Details = append(res.Details, allGlue...)
return res
}
res.Status = StatusPass
res.Message = fmt.Sprintf("Glue records present and consistent for %d in-bailiwick nameservers", len(inBailiwick))
res.Details = allGlue
return res
}
func checkParentNSCount(delegationNS []string) CheckResult {
start := time.Now()
res := CheckResult{ID: "parent-ns-count", Title: "Parent NS Count"}
defer func() { res.DurationMs = measureDuration(start) }()
count := len(delegationNS)
switch {
case count == 0:
res.Status = StatusFail
res.Message = "No nameservers found at parent zone. Your domain will not resolve. Add NS records at your registrar."
case count == 1:
res.Status = StatusWarn
res.Message = fmt.Sprintf("You only have 1 nameserver (%s). RFC 2182 recommends at least 2 nameservers for redundancy. If this server goes down, your domain will be unreachable.", delegationNS[0])
res.Details = delegationNS
default:
res.Status = StatusPass
res.Message = fmt.Sprintf("You have %d nameservers, which is good for redundancy", count)
res.Details = delegationNS
}
return res
}
func checkParentConsistency(domain string, delegationNS []string, r *resolver.Resolver) CheckResult {
start := time.Now()
res := CheckResult{ID: "parent-consistency", Title: "Parent/Auth NS Consistency"}
defer func() { res.DurationMs = measureDuration(start) }()
if len(delegationNS) == 0 {
res.Status = StatusFail
res.Message = "No parent NS to compare"
return res
}
// Get authoritative NS set by querying one of the delegated NS.
var authNS []string
for _, ns := range delegationNS {
ips := resolveNS(ns, r)
for _, ip := range ips {
authNS = nsNamesFromAuthNS(domain, ip, r)
if len(authNS) > 0 {
break
}
}
if len(authNS) > 0 {
break
}
}
if len(authNS) == 0 {
res.Status = StatusWarn
res.Message = "Could not retrieve authoritative NS set for comparison"
return res
}
parentSet := sortedStrings(delegationNS)
authSet := sortedStrings(authNS)
parentStr := strings.Join(parentSet, ",")
authStr := strings.Join(authSet, ",")
if parentStr == authStr {
res.Status = StatusPass
res.Message = "The NS records at the parent zone match the NS records your nameservers return. Everything is consistent."
res.Details = append(res.Details, fmt.Sprintf("NS at parent: %s", strings.Join(parentSet, ", ")))
res.Details = append(res.Details, fmt.Sprintf("NS at auth: %s", strings.Join(authSet, ", ")))
} else {
res.Status = StatusWarn
res.Message = "The NS records at the parent zone do NOT match what your nameservers report. You need to update either your registrar's NS records or your zone's NS records so they are consistent."
onlyParent := diff(parentSet, authSet)
onlyAuth := diff(authSet, parentSet)
if len(onlyParent) > 0 {
for _, ns := range onlyParent {
res.Details = append(res.Details, fmt.Sprintf("%s is listed at the parent zone but NOT in your zone — either add it to your zone or remove it from the parent (registrar)", ns))
}
}
if len(onlyAuth) > 0 {
for _, ns := range onlyAuth {
res.Details = append(res.Details, fmt.Sprintf("%s is in your zone but NOT at the parent — either add it at your registrar or remove it from your zone", ns))
}
}
res.Details = append(res.Details, fmt.Sprintf("Parent reports: %s", strings.Join(parentSet, ", ")))
res.Details = append(res.Details, fmt.Sprintf("Your zone reports: %s", strings.Join(authSet, ", ")))
}
return res
}
func diff(a, b []string) []string {
sort.Strings(a)
sort.Strings(b)
set := make(map[string]bool)
for _, v := range b {
set[v] = true
}
var result []string
for _, v := range a {
if !set[v] {
result = append(result, v)
}
}
return result
}