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
This commit is contained in:
713
backend/internal/checker/ns.go
Normal file
713
backend/internal/checker/ns.go
Normal file
@@ -0,0 +1,713 @@
|
||||
package checker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/intodns/backend/internal/resolver"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// checkNS runs the 17 nameserver checks.
|
||||
func checkNS(domain string, r *resolver.Resolver) Category {
|
||||
cat := Category{Name: "nameservers", Title: "Nameservers"}
|
||||
|
||||
domain = dns.Fqdn(domain)
|
||||
|
||||
// Resolve the NS set via a recursive query.
|
||||
resp, err := r.Query(domain, "8.8.8.8", dns.TypeNS)
|
||||
if err != nil || resp == nil {
|
||||
cat.Checks = append(cat.Checks, CheckResult{
|
||||
ID: "ns-count", Title: "NS Count",
|
||||
Status: StatusFail, Message: fmt.Sprintf("Failed to query NS records: %v", err),
|
||||
})
|
||||
return cat
|
||||
}
|
||||
|
||||
var nsNames []string
|
||||
for _, rr := range resp.Answer {
|
||||
if ns, ok := rr.(*dns.NS); ok {
|
||||
nsNames = appendUniqLower(nsNames, ns.Ns)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve all NS to IPs.
|
||||
var nsInfos []nsEntry
|
||||
var allIPs []string
|
||||
for _, ns := range nsNames {
|
||||
ips := resolveNS(ns, r)
|
||||
nsInfos = append(nsInfos, nsEntry{Name: ns, IPs: ips})
|
||||
allIPs = append(allIPs, ips...)
|
||||
}
|
||||
|
||||
// 1. ns-count
|
||||
cat.Checks = append(cat.Checks, checkNSCount(nsNames))
|
||||
|
||||
// 2. ns-reachable — also builds a filtered list of reachable NS
|
||||
reachableCheck, reachableInfos := checkNSReachableFiltered(domain, nsInfos, r)
|
||||
cat.Checks = append(cat.Checks, reachableCheck)
|
||||
|
||||
// Use reachable NS for checks that query NS directly to avoid timeouts.
|
||||
// Checks that only need names/IPs (no direct queries) still use full nsInfos.
|
||||
|
||||
// 3. ns-auth
|
||||
cat.Checks = append(cat.Checks, checkNSAuth(domain, reachableInfos, r))
|
||||
|
||||
// 4. ns-recursion
|
||||
cat.Checks = append(cat.Checks, checkNSRecursion(domain, reachableInfos, r))
|
||||
|
||||
// 5. ns-identical
|
||||
cat.Checks = append(cat.Checks, checkNSIdentical(domain, reachableInfos, r))
|
||||
|
||||
// 6. ns-ip-unique
|
||||
cat.Checks = append(cat.Checks, checkNSIPUnique(nsInfos))
|
||||
|
||||
// 7. ns-subnet-diversity
|
||||
cat.Checks = append(cat.Checks, checkNSSubnetDiversity(allIPs))
|
||||
|
||||
// 8. ns-as-diversity
|
||||
cat.Checks = append(cat.Checks, checkNSASDiversity(allIPs))
|
||||
|
||||
// 9. ns-tcp
|
||||
cat.Checks = append(cat.Checks, checkNSTCP(domain, reachableInfos, r))
|
||||
|
||||
// 10. ns-udp
|
||||
cat.Checks = append(cat.Checks, checkNSUDP(domain, reachableInfos, r))
|
||||
|
||||
// 11. ns-no-cname
|
||||
cat.Checks = append(cat.Checks, checkNSNoCNAME(nsNames, r))
|
||||
|
||||
// 12. ns-a-records
|
||||
cat.Checks = append(cat.Checks, checkNSARecords(nsInfos, r))
|
||||
|
||||
// 13. ns-aaaa-records
|
||||
cat.Checks = append(cat.Checks, checkNSAAAARecords(nsNames, r))
|
||||
|
||||
// 14. ns-version
|
||||
cat.Checks = append(cat.Checks, checkNSVersion(reachableInfos, r))
|
||||
|
||||
// 15. ns-zone-transfer
|
||||
cat.Checks = append(cat.Checks, checkNSZoneTransfer(domain, reachableInfos, r))
|
||||
|
||||
// 16. ns-lame
|
||||
cat.Checks = append(cat.Checks, checkNSLame(domain, reachableInfos, r))
|
||||
|
||||
// 17. ns-response-size
|
||||
cat.Checks = append(cat.Checks, checkNSResponseSize(domain, reachableInfos, r))
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
func checkNSCount(nsNames []string) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-count", Title: "NS Count"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
count := len(nsNames)
|
||||
switch {
|
||||
case count == 0:
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS records found"
|
||||
case count == 1:
|
||||
res.Status = StatusWarn
|
||||
res.Message = "Only 1 nameserver; at least 2 recommended"
|
||||
res.Details = nsNames
|
||||
default:
|
||||
res.Status = StatusPass
|
||||
res.Message = fmt.Sprintf("%d nameservers found", count)
|
||||
res.Details = nsNames
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type nsEntry struct {
|
||||
Name string
|
||||
IPs []string
|
||||
}
|
||||
|
||||
// checkNSReachableFiltered checks reachability and returns both the check result
|
||||
// and a filtered list of only reachable NS entries (with only reachable IPs).
|
||||
func checkNSReachableFiltered(domain string, infos []nsEntry, r *resolver.Resolver) (CheckResult, []nsEntry) {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-reachable", Title: "NS Reachability"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var reachable []nsEntry
|
||||
allOK := true
|
||||
for _, ns := range infos {
|
||||
if len(ns.IPs) == 0 {
|
||||
allOK = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: no IPs resolved", ns.Name))
|
||||
continue
|
||||
}
|
||||
var reachableIPs []string
|
||||
for _, ip := range ns.IPs {
|
||||
_, err := r.QueryNoRecurse(domain, ip, dns.TypeSOA)
|
||||
if err == nil {
|
||||
reachableIPs = append(reachableIPs, ip)
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): OK", ns.Name, ip))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): unreachable", ns.Name, ip))
|
||||
}
|
||||
}
|
||||
if len(reachableIPs) > 0 {
|
||||
reachable = append(reachable, nsEntry{Name: ns.Name, IPs: reachableIPs})
|
||||
} else {
|
||||
allOK = false
|
||||
}
|
||||
}
|
||||
|
||||
if allOK {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers respond to DNS queries"
|
||||
} else {
|
||||
var unreachableNames []string
|
||||
for _, ns := range infos {
|
||||
found := false
|
||||
for _, r := range reachable {
|
||||
if r.Name == ns.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
unreachableNames = append(unreachableNames, ns.Name)
|
||||
}
|
||||
}
|
||||
res.Status = StatusFail
|
||||
res.Message = fmt.Sprintf("The following nameservers are not responding to DNS queries: %s. This means part of your DNS infrastructure is down. Check that these servers are running and accessible on port 53 (UDP/TCP).", strings.Join(unreachableNames, ", "))
|
||||
}
|
||||
return res, reachable
|
||||
}
|
||||
|
||||
func checkNSAuth(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-auth", Title: "NS Authoritative"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
allAuth := true
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
resp, err := r.QueryNoRecurse(domain, ip, dns.TypeSOA)
|
||||
if err != nil {
|
||||
allAuth = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): error %v", ns.Name, ip, err))
|
||||
continue
|
||||
}
|
||||
if resp.Authoritative {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): authoritative (AA=1)", ns.Name, ip))
|
||||
} else {
|
||||
allAuth = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): NOT authoritative (AA=0)", ns.Name, ip))
|
||||
}
|
||||
break // only check first reachable IP per NS
|
||||
}
|
||||
}
|
||||
|
||||
if allAuth {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers are authoritative"
|
||||
} else {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Some nameservers are not authoritative"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSRecursion(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-recursion", Title: "NS Recursion"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
anyRecursive := false
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
resp, err := r.QueryNoRecurse(domain, ip, dns.TypeSOA)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if resp.RecursionAvailable {
|
||||
anyRecursive = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): recursion available (RA=1)", ns.Name, ip))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): no recursion (RA=0)", ns.Name, ip))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if anyRecursive {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "Some nameservers offer recursion; this is a security risk"
|
||||
} else {
|
||||
res.Status = StatusPass
|
||||
res.Message = "No nameservers offer recursion"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSIdentical(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-identical", Title: "NS Identical Sets"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
var sets []string
|
||||
allSame := true
|
||||
var referenceSet string
|
||||
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
names := nsNamesFromAuthNS(domain, ip, r)
|
||||
sorted := sortedStrings(names)
|
||||
setStr := strings.Join(sorted, ",")
|
||||
sets = append(sets, setStr)
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: %s", ns.Name, strings.Join(sorted, ", ")))
|
||||
if referenceSet == "" {
|
||||
referenceSet = setStr
|
||||
} else if setStr != referenceSet {
|
||||
allSame = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allSame {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers return the same NS set"
|
||||
} else {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Nameservers return different NS sets"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSIPUnique(infos []nsEntry) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-ip-unique", Title: "NS IP Uniqueness"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
seen := make(map[string]string) // ip -> ns name
|
||||
duplicates := false
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
if prev, ok := seen[ip]; ok {
|
||||
duplicates = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("Duplicate IP %s shared by %s and %s", ip, prev, ns.Name))
|
||||
} else {
|
||||
seen[ip] = ns.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if duplicates {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "Some nameservers share IP addresses"
|
||||
} else {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameserver IPs are unique"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSSubnetDiversity(allIPs []string) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-subnet-diversity", Title: "NS Subnet Diversity"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(allIPs) == 0 {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "No NS IPs to check"
|
||||
return res
|
||||
}
|
||||
|
||||
subnets := make(map[string]bool)
|
||||
for _, ip := range allIPs {
|
||||
subnets[subnet24(ip)] = true
|
||||
}
|
||||
|
||||
for s := range subnets {
|
||||
res.Details = append(res.Details, s)
|
||||
}
|
||||
|
||||
if len(subnets) >= 2 {
|
||||
res.Status = StatusPass
|
||||
res.Message = fmt.Sprintf("Nameservers span %d /24 subnets", len(subnets))
|
||||
} else {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "All nameservers are in the same /24 subnet"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSASDiversity(allIPs []string) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-as-diversity", Title: "NS AS Diversity"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(allIPs) == 0 {
|
||||
res.Status = StatusInfo
|
||||
res.Message = "No NS IPs to check"
|
||||
return res
|
||||
}
|
||||
|
||||
networks := make(map[string]bool)
|
||||
for _, ip := range allIPs {
|
||||
networks[subnet16(ip)] = true
|
||||
}
|
||||
|
||||
for n := range networks {
|
||||
res.Details = append(res.Details, n)
|
||||
}
|
||||
|
||||
if len(networks) >= 2 {
|
||||
res.Status = StatusInfo
|
||||
res.Message = fmt.Sprintf("Nameservers appear to span %d /16 networks (rough AS diversity proxy)", len(networks))
|
||||
} else {
|
||||
res.Status = StatusInfo
|
||||
res.Message = "All nameservers appear to be in the same /16 network"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSTCP(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-tcp", Title: "NS TCP Support"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
allOK := true
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
_, err := r.QueryTCP(domain, ip, dns.TypeSOA)
|
||||
if err != nil {
|
||||
allOK = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): TCP failed: %v", ns.Name, ip, err))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): TCP OK", ns.Name, ip))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allOK {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers respond over TCP"
|
||||
} else {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Some nameservers do not respond over TCP"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSUDP(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-udp", Title: "NS UDP Support"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
allOK := true
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
_, err := r.QueryNoRecurse(domain, ip, dns.TypeSOA)
|
||||
if err != nil {
|
||||
allOK = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): UDP failed: %v", ns.Name, ip, err))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): UDP OK", ns.Name, ip))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allOK {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers respond over UDP"
|
||||
} else {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Some nameservers do not respond over UDP"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSNoCNAME(nsNames []string, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-no-cname", Title: "NS No CNAME"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
hasCNAME := false
|
||||
for _, ns := range nsNames {
|
||||
resp, err := r.Query(ns, "8.8.8.8", dns.TypeCNAME)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, rr := range resp.Answer {
|
||||
if cname, ok := rr.(*dns.CNAME); ok {
|
||||
hasCNAME = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s is a CNAME to %s", ns, cname.Target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasCNAME {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Some NS names resolve to CNAMEs (RFC violation)"
|
||||
} else {
|
||||
res.Status = StatusPass
|
||||
res.Message = "No NS names resolve to CNAMEs"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSARecords(infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-a-records", Title: "NS A Records"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
allHaveA := true
|
||||
for _, ns := range infos {
|
||||
resp, err := r.Query(ns.Name, "8.8.8.8", dns.TypeA)
|
||||
if err != nil {
|
||||
allHaveA = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: query error %v", ns.Name, err))
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, rr := range resp.Answer {
|
||||
if a, ok := rr.(*dns.A); ok {
|
||||
found = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: %s", ns.Name, a.A.String()))
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
allHaveA = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: no A record", ns.Name))
|
||||
}
|
||||
}
|
||||
|
||||
if allHaveA {
|
||||
res.Status = StatusPass
|
||||
res.Message = "All nameservers have A records"
|
||||
} else {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "Some nameservers lack A records"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSAAAARecords(nsNames []string, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-aaaa-records", Title: "NS AAAA Records"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
anyAAAA := false
|
||||
for _, ns := range nsNames {
|
||||
resp, err := r.Query(ns, "8.8.8.8", dns.TypeAAAA)
|
||||
if err != nil {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: query error", ns))
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, rr := range resp.Answer {
|
||||
if aaaa, ok := rr.(*dns.AAAA); ok {
|
||||
found = true
|
||||
anyAAAA = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: %s", ns, aaaa.AAAA.String()))
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s: no AAAA record", ns))
|
||||
}
|
||||
}
|
||||
|
||||
if anyAAAA {
|
||||
res.Status = StatusInfo
|
||||
res.Message = "Some nameservers have AAAA records (IPv6)"
|
||||
} else {
|
||||
res.Status = StatusInfo
|
||||
res.Message = "No nameservers have AAAA records"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSVersion(infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-version", Title: "NS Version"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
ver, err := r.QueryVersionBind(ip)
|
||||
if err != nil || ver == "" {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): not disclosed", ns.Name, ip))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): %s", ns.Name, ip, ver))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res.Status = StatusInfo
|
||||
res.Message = "Version information (version.bind)"
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSZoneTransfer(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-zone-transfer", Title: "NS Zone Transfer (AXFR)"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
anyAllowed := false
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
allowed, _ := r.QueryAXFR(domain, ip)
|
||||
if allowed {
|
||||
anyAllowed = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): AXFR ALLOWED (dangerous!)", ns.Name, ip))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): AXFR refused (good)", ns.Name, ip))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if anyAllowed {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Zone transfer (AXFR) is allowed on some nameservers"
|
||||
} else {
|
||||
res.Status = StatusPass
|
||||
res.Message = "Zone transfer (AXFR) is refused on all nameservers"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSLame(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-lame", Title: "NS Lame Delegation"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
anyLame := false
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
resp, err := r.QueryNoRecurse(domain, ip, dns.TypeSOA)
|
||||
if err != nil {
|
||||
anyLame = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): lame (unreachable)", ns.Name, ip))
|
||||
break
|
||||
}
|
||||
if !resp.Authoritative {
|
||||
anyLame = true
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): lame (not authoritative)", ns.Name, ip))
|
||||
} else {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): OK (authoritative)", ns.Name, ip))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if anyLame {
|
||||
res.Status = StatusFail
|
||||
res.Message = "Lame delegation detected"
|
||||
} else {
|
||||
res.Status = StatusPass
|
||||
res.Message = "No lame delegations"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkNSResponseSize(domain string, infos []nsEntry, r *resolver.Resolver) CheckResult {
|
||||
start := time.Now()
|
||||
res := CheckResult{ID: "ns-response-size", Title: "NS Response Size"}
|
||||
defer func() { res.DurationMs = measureDuration(start) }()
|
||||
|
||||
if len(infos) == 0 {
|
||||
res.Status = StatusFail
|
||||
res.Message = "No NS to check"
|
||||
return res
|
||||
}
|
||||
|
||||
allOK := true
|
||||
for _, ns := range infos {
|
||||
for _, ip := range ns.IPs {
|
||||
// Try a normal query and check the response size.
|
||||
resp, err := r.QueryNoRecurse(domain, ip, dns.TypeNS)
|
||||
if err != nil {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): error", ns.Name, ip))
|
||||
break
|
||||
}
|
||||
packed, err := resp.Pack()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
size := len(packed)
|
||||
if size <= 512 {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): %d bytes (fits in 512)", ns.Name, ip, size))
|
||||
} else {
|
||||
// Check EDNS support.
|
||||
ednsResp, ednsErr := r.QueryEDNS(domain, ip, dns.TypeNS, 4096)
|
||||
if ednsErr != nil {
|
||||
allOK = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): %d bytes, no EDNS support", ns.Name, ip, size))
|
||||
} else {
|
||||
opt := ednsResp.IsEdns0()
|
||||
if opt != nil {
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): %d bytes, EDNS supported (bufsize %d)", ns.Name, ip, size, opt.UDPSize()))
|
||||
} else {
|
||||
allOK = false
|
||||
res.Details = append(res.Details, fmt.Sprintf("%s (%s): %d bytes, EDNS not in response", ns.Name, ip, size))
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allOK {
|
||||
res.Status = StatusPass
|
||||
res.Message = "Responses fit in 512 bytes or EDNS is supported"
|
||||
} else {
|
||||
res.Status = StatusWarn
|
||||
res.Message = "Some responses exceed 512 bytes without EDNS support"
|
||||
}
|
||||
return res
|
||||
}
|
||||
Reference in New Issue
Block a user