Initial commit
This commit is contained in:
commit
2d98ed1b5d
33 changed files with 4644 additions and 0 deletions
245
checker/rules_consensus.go
Normal file
245
checker/rules_consensus.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// consensusRule emits one state per RRset summarising how much of the probed
|
||||
// resolver set agrees on its answer. It covers the "partial propagation"
|
||||
// case (several distinct NOERROR signatures observed).
|
||||
type consensusRule struct{}
|
||||
|
||||
func (r *consensusRule) Name() string { return "resolver_propagation.consensus" }
|
||||
func (r *consensusRule) Description() string {
|
||||
return "Checks that public resolvers agree on a single answer for each probed RRset."
|
||||
}
|
||||
func (r *consensusRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
if len(data.Resolvers) == 0 || len(data.RRsets) == 0 {
|
||||
return []sdk.CheckState{{Status: sdk.StatusUnknown,
|
||||
Code: "resolver_propagation.consensus.skipped",
|
||||
Message: "no resolver probes available"}}
|
||||
}
|
||||
|
||||
keys := sortedRRsetKeys(data)
|
||||
var states []sdk.CheckState
|
||||
for _, key := range keys {
|
||||
v := data.RRsets[key]
|
||||
|
||||
unfilteredNOERRORSigs := map[string]bool{}
|
||||
for _, g := range v.Groups {
|
||||
if g.Rcode != "NOERROR" {
|
||||
continue
|
||||
}
|
||||
for _, rid := range g.Resolvers {
|
||||
rv := data.Resolvers[rid]
|
||||
if rv != nil && !rv.Filtered {
|
||||
unfilteredNOERRORSigs[g.Signature] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case v.ConsensusSig == "" && len(v.Groups) == 0:
|
||||
states = append(states, infoState("resolver_propagation.consensus.no_data", key,
|
||||
fmt.Sprintf("no resolver returned a usable answer for %s", key)))
|
||||
case len(unfilteredNOERRORSigs) > 1:
|
||||
states = append(states, warnState(CodePartialPropagation, key,
|
||||
fmt.Sprintf("%d distinct answers seen across public resolvers for %s, change is mid-propagation",
|
||||
len(unfilteredNOERRORSigs), key)))
|
||||
default:
|
||||
states = append(states, sdk.CheckState{
|
||||
Status: sdk.StatusOK,
|
||||
Code: "resolver_propagation.consensus.ok",
|
||||
Subject: key,
|
||||
Message: fmt.Sprintf("all %d probed resolver(s) agree on %s", len(v.Agreeing), key),
|
||||
})
|
||||
}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// authoritativeMatchRule checks the consensus against the answer served by
|
||||
// the zone's own authoritative servers.
|
||||
type authoritativeMatchRule struct{}
|
||||
|
||||
func (r *authoritativeMatchRule) Name() string { return "resolver_propagation.matches_authoritative" }
|
||||
func (r *authoritativeMatchRule) Description() string {
|
||||
return "Checks that the public consensus matches the answer served by the zone's authoritative nameservers."
|
||||
}
|
||||
func (r *authoritativeMatchRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
var states []sdk.CheckState
|
||||
anyExpected := false
|
||||
for _, key := range sortedRRsetKeys(data) {
|
||||
v := data.RRsets[key]
|
||||
if v.Expected == "" {
|
||||
continue
|
||||
}
|
||||
anyExpected = true
|
||||
switch {
|
||||
case v.ConsensusSig == "":
|
||||
states = append(states, critState("resolver_propagation.matches_authoritative.no_consensus", key,
|
||||
fmt.Sprintf("no public resolver returned a usable answer for %s (authoritative answer is known)", key)))
|
||||
case !v.MatchesExpected:
|
||||
states = append(states, critState(CodeAnswerDrift, key,
|
||||
fmt.Sprintf("consensus of public resolvers for %s differs from the authoritative answer, wait for TTL expiry or force a flush", key)))
|
||||
default:
|
||||
states = append(states, sdk.CheckState{
|
||||
Status: sdk.StatusOK, Code: "resolver_propagation.matches_authoritative.ok", Subject: key,
|
||||
Message: fmt.Sprintf("public consensus for %s matches the authoritative answer", key),
|
||||
})
|
||||
}
|
||||
}
|
||||
if !anyExpected {
|
||||
return []sdk.CheckState{{Status: sdk.StatusUnknown,
|
||||
Code: "resolver_propagation.matches_authoritative.skipped",
|
||||
Message: "authoritative nameservers were unreachable; cannot compare consensus to ground truth"}}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// nxdomainRule flags RRsets returning NXDOMAIN on some (but not all) resolvers.
|
||||
type nxdomainRule struct{}
|
||||
|
||||
func (r *nxdomainRule) Name() string { return "resolver_propagation.nxdomain" }
|
||||
func (r *nxdomainRule) Description() string {
|
||||
return "Flags RRsets for which some resolvers return NXDOMAIN while others return NOERROR."
|
||||
}
|
||||
func (r *nxdomainRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
var states []sdk.CheckState
|
||||
for _, key := range sortedRRsetKeys(data) {
|
||||
v := data.RRsets[key]
|
||||
var nxList []string
|
||||
for _, g := range v.Groups {
|
||||
if g.Rcode == "NXDOMAIN" {
|
||||
nxList = append(nxList, g.Resolvers...)
|
||||
}
|
||||
}
|
||||
if len(nxList) > 0 && len(nxList) < len(data.Resolvers) {
|
||||
states = append(states, critState(CodeUnexpectedNXDOMAIN, key,
|
||||
fmt.Sprintf("%s resolved as NXDOMAIN on %d resolver(s): %s", key, len(nxList), firstN(nxList, 6))))
|
||||
}
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return []sdk.CheckState{passState("resolver_propagation.nxdomain.ok",
|
||||
"No resolver unexpectedly returns NXDOMAIN.")}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// servfailRule flags RRsets returning SERVFAIL on any resolver.
|
||||
type servfailRule struct{}
|
||||
|
||||
func (r *servfailRule) Name() string { return "resolver_propagation.servfail" }
|
||||
func (r *servfailRule) Description() string {
|
||||
return "Flags RRsets for which any resolver returns SERVFAIL (usually DNSSEC or reachability failure)."
|
||||
}
|
||||
func (r *servfailRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
var states []sdk.CheckState
|
||||
for _, key := range sortedRRsetKeys(data) {
|
||||
v := data.RRsets[key]
|
||||
var sfList []string
|
||||
for _, g := range v.Groups {
|
||||
if g.Rcode == "SERVFAIL" {
|
||||
sfList = append(sfList, g.Resolvers...)
|
||||
}
|
||||
}
|
||||
if len(sfList) > 0 {
|
||||
states = append(states, critState(CodeUnexpectedSERVFAIL, key,
|
||||
fmt.Sprintf("%s returned SERVFAIL on %d resolver(s): %s", key, len(sfList), firstN(sfList, 6))))
|
||||
}
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return []sdk.CheckState{passState("resolver_propagation.servfail.ok",
|
||||
"No resolver returns SERVFAIL.")}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// regionalSplitRule flags regions in which all resolvers agree on an answer
|
||||
// that diverges from the global consensus.
|
||||
type regionalSplitRule struct{}
|
||||
|
||||
func (r *regionalSplitRule) Name() string { return "resolver_propagation.regional_split" }
|
||||
func (r *regionalSplitRule) Description() string {
|
||||
return "Flags regions in which every resolver agrees on an answer that differs from the global consensus."
|
||||
}
|
||||
func (r *regionalSplitRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errSt := loadData(ctx, obs)
|
||||
if errSt != nil {
|
||||
return []sdk.CheckState{*errSt}
|
||||
}
|
||||
|
||||
var states []sdk.CheckState
|
||||
for _, key := range sortedRRsetKeys(data) {
|
||||
v := data.RRsets[key]
|
||||
region2sig := map[string]map[string]int{}
|
||||
for _, g := range v.Groups {
|
||||
for _, rid := range g.Resolvers {
|
||||
rv := data.Resolvers[rid]
|
||||
if rv == nil || rv.Filtered {
|
||||
continue
|
||||
}
|
||||
if region2sig[rv.Region] == nil {
|
||||
region2sig[rv.Region] = map[string]int{}
|
||||
}
|
||||
region2sig[rv.Region][g.Signature]++
|
||||
}
|
||||
}
|
||||
regions := make([]string, 0, len(region2sig))
|
||||
for r := range region2sig {
|
||||
regions = append(regions, r)
|
||||
}
|
||||
sort.Strings(regions)
|
||||
for _, region := range regions {
|
||||
sigs := region2sig[region]
|
||||
if len(sigs) != 1 {
|
||||
continue
|
||||
}
|
||||
var only string
|
||||
for s := range sigs {
|
||||
only = s
|
||||
}
|
||||
if only != "" && only != v.ConsensusSig {
|
||||
states = append(states, warnState(CodeRegionalSplit, region+" "+key,
|
||||
fmt.Sprintf("all %s resolvers agree on an answer that differs from the global consensus for %s",
|
||||
regionLabel(region), key)))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(states) == 0 {
|
||||
return []sdk.CheckState{passState("resolver_propagation.regional_split.ok",
|
||||
"No region is split from the global consensus.")}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func sortedRRsetKeys(data *ResolverPropagationData) []string {
|
||||
keys := make([]string, 0, len(data.RRsets))
|
||||
for k := range data.RRsets {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue