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 }