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 }