95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package resolver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SplitHorizonResult holds the comparison between an authoritative DNS answer
|
|
// and the answer from a local resolver.
|
|
type SplitHorizonResult struct {
|
|
AuthoritativeIPs []string // IPs from the authoritative nameserver
|
|
AuthoritativeSource string // NS hostname that provided the authoritative answer
|
|
LocalIPs []string // IPs from the local resolver (nil if query failed / NXDOMAIN)
|
|
LocalSource string // Local resolver address that was queried (empty if all failed)
|
|
HasConflict bool // True when both sets are non-nil and differ
|
|
}
|
|
|
|
// CheckSplitHorizon queries local resolvers for hostname and compares the result
|
|
// against authoritativeIPs.
|
|
//
|
|
// Local resolvers are tried in priority order; the first successful response is used.
|
|
// If all local resolvers fail or return NXDOMAIN, HasConflict is false (no conflict).
|
|
// w receives Stage 2.5 diagnostic lines (pass io.Discard to suppress).
|
|
func CheckSplitHorizon(ctx context.Context, w io.Writer, localResolvers []string, hostname string, authoritativeIPs []string, authoritativeSource string, timeout time.Duration) SplitHorizonResult {
|
|
result := SplitHorizonResult{
|
|
AuthoritativeIPs: authoritativeIPs,
|
|
AuthoritativeSource: authoritativeSource,
|
|
}
|
|
|
|
if len(localResolvers) > 0 {
|
|
fmt.Fprintf(w, "[dns] Stage 2.5: Split-horizon cross-check against local resolvers [%s]\n",
|
|
strings.Join(localResolvers, ", "))
|
|
}
|
|
|
|
for _, lr := range localResolvers {
|
|
queryCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
ips, err := queryA(queryCtx, lr, hostname, timeout)
|
|
cancel()
|
|
if err != nil {
|
|
continue // timeout, NXDOMAIN, etc. — not a conflict
|
|
}
|
|
if len(ips) == 0 {
|
|
continue
|
|
}
|
|
result.LocalIPs = ips
|
|
result.LocalSource = lr
|
|
result.HasConflict = !ipSetsEqual(authoritativeIPs, ips)
|
|
if result.HasConflict {
|
|
fmt.Fprintf(w, "[dns] %s \u2192 %s (differs from authoritative %s)\n",
|
|
lr, strings.Join(ips, ", "), strings.Join(authoritativeIPs, ", "))
|
|
fmt.Fprintf(w, "[dns] CONFLICT: authoritative and local resolvers disagree\n")
|
|
} else {
|
|
fmt.Fprintf(w, "[dns] %s \u2192 %s (matches authoritative)\n", lr, strings.Join(ips, ", "))
|
|
fmt.Fprintf(w, "[dns] No conflict detected\n")
|
|
}
|
|
return result
|
|
}
|
|
|
|
// All local resolvers failed — no conflict detectable.
|
|
return result
|
|
}
|
|
|
|
// ipSetsEqual reports whether a and b contain the same IPs regardless of order.
|
|
func ipSetsEqual(a, b []string) bool {
|
|
sa := sortedDedup(a)
|
|
sb := sortedDedup(b)
|
|
if len(sa) != len(sb) {
|
|
return false
|
|
}
|
|
for i := range sa {
|
|
if sa[i] != sb[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// sortedDedup returns a sorted, deduplicated copy of ips.
|
|
func sortedDedup(ips []string) []string {
|
|
seen := make(map[string]bool, len(ips))
|
|
deduped := make([]string, 0, len(ips))
|
|
for _, ip := range ips {
|
|
if !seen[ip] {
|
|
seen[ip] = true
|
|
deduped = append(deduped, ip)
|
|
}
|
|
}
|
|
sort.Strings(deduped)
|
|
return deduped
|
|
}
|