package checker import ( "encoding/json" "fmt" "github.com/miekg/dns" ) // Cap on per-NS error list so a flaky server with many addresses cannot // bloat the JSON observation payload. const maxNSResultErrors = 16 const ObservationKey = "authoritative-consistency" type Severity string const ( SeverityInfo Severity = "info" SeverityWarn Severity = "warn" SeverityCrit Severity = "crit" ) // Stable identifiers; the UI keys translations and remediation docs off them. const ( CodeSerialDrift = "authoritative_consistency_serial_drift" CodeSerialStaleVsSaved = "authoritative_consistency_serial_stale_vs_saved" CodeSerialAheadOfSaved = "authoritative_consistency_serial_ahead_of_saved" CodeNSUnreachable = "authoritative_consistency_ns_unreachable" CodeNSUDPFailed = "authoritative_consistency_ns_udp_failed" CodeNSTCPFailed = "authoritative_consistency_ns_tcp_failed" CodeNSUnresolvable = "authoritative_consistency_ns_unresolvable" CodeLame = "authoritative_consistency_lame" CodeNoSOA = "authoritative_consistency_no_soa" CodeNSRRsetDrift = "authoritative_consistency_ns_rrset_drift" CodeNSRRsetMismatchConfig = "authoritative_consistency_ns_rrset_mismatch_config" CodeParentDrift = "authoritative_consistency_parent_drift" CodeParentQueryFailed = "authoritative_consistency_parent_query_failed" CodeSOAFieldsDrift = "authoritative_consistency_soa_fields_drift" CodeSlowNS = "authoritative_consistency_slow_ns" CodeEDNSUnsupported = "authoritative_consistency_edns_unsupported" CodeTooFewNS = "authoritative_consistency_too_few_ns" CodeNoNS = "authoritative_consistency_no_ns" ) type Finding struct { Code string `json:"code"` Severity Severity `json:"severity"` Message string `json:"message"` // Server is empty for zone-wide findings. Server string `json:"server,omitempty"` // Addr disambiguates IPv4/IPv6 issues on a multi-homed NS. Addr string `json:"addr,omitempty"` } type NSResult struct { // Name is FQDN, lowercase. Name string `json:"name"` Addresses []string `json:"addresses,omitempty"` ResolveError string `json:"resolve_error,omitempty"` UDPReachable bool `json:"udp_reachable"` TCPReachable bool `json:"tcp_reachable"` Authoritative bool `json:"authoritative"` // Zero when not reachable or the answer carries no SOA. Serial uint32 `json:"serial,omitempty"` // Full SOA RR kept for per-field comparison in the report. SOA *dns.SOA `json:"soa,omitempty"` NSRRset []string `json:"ns_rrset,omitempty"` EDNSSupported bool `json:"edns_supported"` // Zero when not reachable. LatencyMs int64 `json:"latency_ms,omitempty"` // Capped at maxNSResultErrors; appendError is the only intended writer. Errors []string `json:"errors,omitempty"` suppressedErrors int } // Dedupes identical messages and caps the list with a sentinel summary. func (n *NSResult) appendError(format string, args ...any) { msg := fmt.Sprintf(format, args...) for _, e := range n.Errors { if e == msg { return } } if len(n.Errors) >= maxNSResultErrors { n.suppressedErrors++ sentinel := fmt.Sprintf("(%d more error(s) suppressed)", n.suppressedErrors) if last := len(n.Errors) - 1; last >= 0 && len(n.Errors[last]) > 0 && n.Errors[last][0] == '(' { n.Errors[last] = sentinel return } n.Errors = append(n.Errors, sentinel) return } n.Errors = append(n.Errors, msg) } type ObservationData struct { Zone string `json:"zone"` // HasSOA distinguishes Origin from NSOnlyOrigin and gates SOA-based rules. HasSOA bool `json:"has_soa"` // Zero when the service is an NSOnlyOrigin. DeclaredSerial uint32 `json:"declared_serial,omitempty"` DeclaredNS []string `json:"declared_ns,omitempty"` // Empty when parent discovery is disabled or failed (see ParentQueryError). ParentNS []string `json:"parent_ns,omitempty"` ParentQueryError string `json:"parent_query_error,omitempty"` // Union of DeclaredNS and ParentNS, de-duplicated. Probed []string `json:"probed,omitempty"` Results map[string]*NSResult `json:"results,omitempty"` Findings []Finding `json:"findings"` } // Local mirror of happyDomain's services/abstract.Origin. Duplicated on // purpose to avoid pulling the entire happyDomain server module just to // decode the payload; miekg/dns marshals dns.SOA/dns.NS in the same shape. type originService struct { SOA *dns.SOA `json:"soa,omitempty"` NameServers []*dns.NS `json:"ns"` } // Local mirror of happyDomain's ServiceMessage envelope. type serviceMessage struct { Type string `json:"_svctype"` Domain string `json:"_domain"` Service json.RawMessage `json:"Service"` }