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
714 lines
19 KiB
Go
714 lines
19 KiB
Go
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
|
|
}
|