checker-authoritative-consi.../checker/types.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"`
}