checker-authoritative-consi.../checker/types.go

208 lines
8 KiB
Go

package checker
import (
"encoding/json"
"fmt"
"github.com/miekg/dns"
)
// maxNSResultErrors caps the per-NS error list so a flaky server with many
// addresses cannot bloat the JSON observation payload. Once the cap is
// reached, further errors are dropped and a single sentinel entry records the
// number of suppressed messages.
const maxNSResultErrors = 16
// ObservationKey is the observation key for observation data.
const ObservationKey = "authoritative-consistency"
// Severity classifies a finding emitted by the authoritative-consistency checker.
type Severity string
const (
SeverityInfo Severity = "info"
SeverityWarn Severity = "warn"
SeverityCrit Severity = "crit"
)
// Finding codes: stable machine-readable identifiers used by the UI to
// localize and link to remediation docs.
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"
)
// Finding describes a single observation produced while running the
// checker testsuite.
type Finding struct {
// Code is a stable machine-readable identifier (e.g.
// "authoritative_consistency_serial_drift").
Code string `json:"code"`
// Severity grades the finding.
Severity Severity `json:"severity"`
// Message is a human-readable explanation.
Message string `json:"message"`
// Server is the name server the finding applies to, when the issue is
// scoped to a specific NS. Empty for zone-wide findings.
Server string `json:"server,omitempty"`
// Addr is the IP:port actually queried when the issue was raised. Useful
// to distinguish IPv4/IPv6 problems on the same NS name.
Addr string `json:"addr,omitempty"`
}
// NSResult is the per-name-server view of the zone, populated during Collect.
// It carries every signal the evaluator needs to decide whether the zone is
// propagated correctly, plus what the UI needs to render an actionable
// report.
type NSResult struct {
// Name is the NS hostname (FQDN, lowercase).
Name string `json:"name"`
// Addresses is the list of A/AAAA addresses tried for this NS.
Addresses []string `json:"addresses,omitempty"`
// ResolveError is set when no address could be resolved for this NS.
ResolveError string `json:"resolve_error,omitempty"`
// UDPReachable is true when the NS answered at least once over UDP/53.
UDPReachable bool `json:"udp_reachable"`
// TCPReachable is true when the NS answered at least once over TCP/53.
TCPReachable bool `json:"tcp_reachable"`
// Authoritative is true when at least one authoritative (AA=1) answer
// was received for the zone.
Authoritative bool `json:"authoritative"`
// Serial is the SOA serial returned by this NS (0 when not reachable or
// the answer does not carry a SOA).
Serial uint32 `json:"serial,omitempty"`
// SOA is the full SOA RR returned by this NS, useful for per-field
// comparison in the report.
SOA *dns.SOA `json:"soa,omitempty"`
// NSRRset is the NS RRset this server returns for the zone (lowercase
// FQDNs).
NSRRset []string `json:"ns_rrset,omitempty"`
// EDNSSupported is true when the NS answered correctly to an EDNS0
// query.
EDNSSupported bool `json:"edns_supported"`
// LatencyMs is the duration (milliseconds) of the SOA query used for
// the reachability test. 0 when not reachable.
LatencyMs int64 `json:"latency_ms,omitempty"`
// Errors collects low-level query errors encountered while probing this
// NS. Exposed to help operators debug network/firewall issues. Capped
// at maxNSResultErrors entries; appendError is the only intended writer.
Errors []string `json:"errors,omitempty"`
// suppressedErrors counts the messages that were dropped after the cap
// was reached. Reflected back into Errors as a sentinel line so the
// operator knows the list is truncated.
suppressedErrors int
}
// appendError records a probe error on the NS result, deduplicating identical
// messages and capping the total to maxNSResultErrors. Suppressed entries are
// summarised in a trailing sentinel.
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)
// Replace the previous sentinel in place when present.
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)
}
// ObservationData is the observation payload stored by the checker. It
// carries every finding emitted by the testsuite plus the raw observed state
// from each authoritative server.
type ObservationData struct {
// Zone is the FQDN of the zone under test.
Zone string `json:"zone"`
// HasSOA indicates whether the service declares a SOA record (Origin
// versus NSOnlyOrigin). Drives which tests run.
HasSOA bool `json:"has_soa"`
// DeclaredSerial is the SOA serial saved in happyDomain for this zone.
// Zero when the service is an NSOnlyOrigin.
DeclaredSerial uint32 `json:"declared_serial,omitempty"`
// DeclaredNS is the list of NS hostnames declared by the service,
// lowercased and FQDN-normalized.
DeclaredNS []string `json:"declared_ns,omitempty"`
// ParentNS is the list of NS hostnames returned by the parent zone's
// referral, when parent discovery is enabled. Empty when the parent
// query is disabled or failed (see ParentQueryError).
ParentNS []string `json:"parent_ns,omitempty"`
// ParentQueryError is set when the parent referral query failed.
ParentQueryError string `json:"parent_query_error,omitempty"`
// Probed is the final list of NS names that were actually probed
// (union of DeclaredNS and ParentNS, de-duplicated).
Probed []string `json:"probed,omitempty"`
// Results holds the per-NS probe results, keyed by NS hostname.
Results map[string]*NSResult `json:"results,omitempty"`
// Findings is the list of issues / observations produced by the run,
// ordered by (severity desc, code asc, server asc).
Findings []Finding `json:"findings"`
}
// originService is the minimal local mirror of happyDomain's
// `services/abstract.Origin` type. It is duplicated on purpose so that this
// checker does not have to import the (heavy) happyDomain server module
// just to decode the service payload. github.com/miekg/dns marshals
// dns.SOA / dns.NS to JSON in the same shape happyDomain uses.
type originService struct {
SOA *dns.SOA `json:"soa,omitempty"`
NameServers []*dns.NS `json:"ns"`
}
// serviceMessage is the minimal local mirror of happyDomain's ServiceMessage
// envelope. We only need the embedded service JSON and the type tag; the
// rest of the meta fields are ignored.
type serviceMessage struct {
Type string `json:"_svctype"`
Domain string `json:"_domain"`
Service json.RawMessage `json:"Service"`
}