checker-resolver-propagation/checker/types.go

215 lines
7.4 KiB
Go

package checker
import (
"encoding/json"
"github.com/miekg/dns"
)
// ObservationKeyResolverPropagation is the observation key used to store data
// produced by this checker.
const ObservationKeyResolverPropagation = "resolver_propagation"
// Severity classifies a finding.
type Severity string
const (
SeverityInfo Severity = "info"
SeverityWarn Severity = "warn"
SeverityCrit Severity = "crit"
)
// Finding codes: stable machine-readable identifiers surfaced in the UI.
const (
// Zone-wide.
CodeNoResolvers = "rprop_no_resolvers"
CodeAllResolversDown = "rprop_all_resolvers_down"
CodeSerialDrift = "rprop_serial_drift"
CodeStaleCache = "rprop_stale_cache"
CodeDNSSECFailure = "rprop_dnssec_failure"
CodeDNSSECUnvalidated = "rprop_dnssec_not_validated"
CodeRegionalSplit = "rprop_regional_split"
CodePartialPropagation = "rprop_partial_propagation"
CodeAnswerDrift = "rprop_answer_drift"
CodeUnexpectedNXDOMAIN = "rprop_unexpected_nxdomain"
CodeUnexpectedSERVFAIL = "rprop_unexpected_servfail"
// Per-resolver.
CodeResolverUnreachable = "rprop_resolver_unreachable"
CodeResolverTimeout = "rprop_resolver_timeout"
CodeResolverRewrote = "rprop_resolver_rewrote_answer"
CodeResolverFilteredHit = "rprop_resolver_filtered_hit"
CodeResolverHighLatency = "rprop_resolver_high_latency"
)
// Transport identifies the protocol used to reach a resolver.
type Transport string
const (
TransportUDP Transport = "udp"
TransportTCP Transport = "tcp"
TransportDoT Transport = "dot"
TransportDoH Transport = "doh"
)
// Finding is a single observation produced during collection.
type Finding struct {
Code string `json:"code"`
Severity Severity `json:"severity"`
Message string `json:"message"`
// Resolver is the resolver ID when the finding is scoped to one.
Resolver string `json:"resolver,omitempty"`
// RRset is "name/TYPE" when the finding is scoped to one RR set.
RRset string `json:"rrset,omitempty"`
// Remedy is a short, user-facing sentence describing what to do.
Remedy string `json:"remedy,omitempty"`
}
// RRProbe is the observation for a single (resolver, RRset) pair.
type RRProbe struct {
// Rcode is the response rcode in text form (NOERROR / NXDOMAIN /
// SERVFAIL / REFUSED / …). Empty when the probe failed before a
// response was parsed.
Rcode string `json:"rcode,omitempty"`
// Signature is the sorted, TTL-stripped RDATA joined with "|". Two
// resolvers agree on an answer iff their signatures are equal.
Signature string `json:"signature,omitempty"`
// Records is the list of record RDATA strings as returned by the
// resolver, sorted.
Records []string `json:"records,omitempty"`
// MinTTL is the smallest TTL across the RRset. Useful to spot stale
// caches (TTL close to 0 means the resolver just refreshed).
MinTTL uint32 `json:"min_ttl,omitempty"`
// AD indicates the resolver set the AD bit on the response (DNSSEC
// validated). Only meaningful on AD-capable resolvers.
AD bool `json:"ad,omitempty"`
// LatencyMs is the observed round-trip time in milliseconds.
LatencyMs int64 `json:"latency_ms,omitempty"`
// Transport is the protocol used for this probe.
Transport Transport `json:"transport,omitempty"`
// Error describes a transport/protocol failure. Set means the probe
// did not complete and Rcode/Signature are empty.
Error string `json:"error,omitempty"`
}
// ResolverView aggregates every probe performed against a single resolver.
type ResolverView struct {
ID string `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
Region string `json:"region"`
Filtered bool `json:"filtered,omitempty"`
Transport Transport `json:"transport"`
// Reachable is true when at least one probe against this resolver
// produced a valid response (any rcode, including NXDOMAIN).
Reachable bool `json:"reachable"`
// Probes is one RRProbe per "name/TYPE" string.
Probes map[string]*RRProbe `json:"probes,omitempty"`
}
// RRsetView is the cross-resolver picture of a single (name, type): which
// signatures were seen, which resolvers returned each signature, and which
// one we pick as "consensus". The consensus is the most-returned signature
// from unfiltered, reachable resolvers.
type RRsetView struct {
Name string `json:"name"`
Type string `json:"type"`
// Expected is the signature computed from the user's declared zone. Used
// to distinguish "resolvers disagree with each other" from "resolvers
// agree but are wrong".
Expected string `json:"expected,omitempty"`
ExpectedRecords []string `json:"expected_records,omitempty"`
// Groups buckets resolvers by signature.
Groups []SignatureGroup `json:"groups,omitempty"`
// ConsensusSig is the signature returned by the majority of unfiltered
// reachable resolvers.
ConsensusSig string `json:"consensus_sig,omitempty"`
// Agreeing / Dissenting are resolver IDs relative to the consensus.
Agreeing []string `json:"agreeing,omitempty"`
Dissenting []string `json:"dissenting,omitempty"`
// MatchesExpected is true when the consensus matches the expected
// signature. When Expected is empty we skip this check.
MatchesExpected bool `json:"matches_expected"`
}
// SignatureGroup is one bucket in RRsetView: a signature + its records + the
// resolvers that returned it.
type SignatureGroup struct {
Signature string `json:"signature"`
Records []string `json:"records,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
Rcode string `json:"rcode,omitempty"`
}
// ResolverPropagationData is the top-level observation payload.
type ResolverPropagationData struct {
Zone string `json:"zone"`
// Names lists the owner names probed: apex + user-provided subdomains.
Names []string `json:"names"`
// Types lists the RR types probed (text: "A", "AAAA", "MX", …).
Types []string `json:"types"`
// Resolvers is the per-resolver view, keyed by resolver ID.
Resolvers map[string]*ResolverView `json:"resolvers,omitempty"`
// RRsets is the per-RRset cross-resolver view, keyed by "name/TYPE".
RRsets map[string]*RRsetView `json:"rrsets,omitempty"`
// DeclaredSerial is the SOA serial saved by happyDomain (when available).
DeclaredSerial uint32 `json:"declared_serial,omitempty"`
// RunDurationMs is the wall-clock duration of the probe round.
RunDurationMs int64 `json:"run_duration_ms,omitempty"`
// Stats summarizes the run.
Stats Stats `json:"stats"`
}
// Stats is a rollup of resolver health, useful for the dashboard.
type Stats struct {
TotalResolvers int `json:"total_resolvers"`
ReachableResolvers int `json:"reachable_resolvers"`
UnfilteredProbed int `json:"unfiltered_probed"`
FilteredProbed int `json:"filtered_probed"`
CountriesCovered int `json:"countries_covered"`
UnfilteredAgreeing int `json:"unfiltered_agreeing"`
}
// originService mirrors happyDomain's abstract.Origin payload (same shape as
// checker-propagation). We only need the NS list + SOA to detect "this zone
// is supposed to exist".
type originService struct {
SOA *dns.SOA `json:"soa,omitempty"`
NameServers []*dns.NS `json:"ns"`
}
// serviceMessage mirrors happyDomain's ServiceMessage envelope.
type serviceMessage struct {
Type string `json:"_svctype"`
Domain string `json:"_domain"`
Service json.RawMessage `json:"Service"`
}
// rrsetKey builds the "name/TYPE" identifier used to index RRsets.
func rrsetKey(name, typ string) string {
return dns.Fqdn(name) + "/" + typ
}