Initial commit
This commit is contained in:
commit
2d98ed1b5d
33 changed files with 4644 additions and 0 deletions
215
checker/types.go
Normal file
215
checker/types.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue