Files
dnshelper/resolver/splithorizon.go
T

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
}