127 lines
4.8 KiB
Go
127 lines
4.8 KiB
Go
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"`
|
|
}
|