215 lines
7.4 KiB
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
|
|
}
|