checker-resolver-propagation/checker/rules_resolvers.go

139 lines
5 KiB
Go

package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// resolverSelectionRule flags an empty selection (nothing to probe).
type resolverSelectionRule struct{}
func (r *resolverSelectionRule) Name() string { return "resolver_propagation.selection" }
func (r *resolverSelectionRule) Description() string {
return "Checks that the current option set selects at least one public resolver."
}
func (r *resolverSelectionRule) 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 {
return []sdk.CheckState{critState(CodeNoResolvers, data.Zone,
"no resolvers match the current selection (region / filtered / allowlist), loosen the region filter or reset the allowlist")}
}
return []sdk.CheckState{passState("resolver_propagation.selection.ok",
fmt.Sprintf("%d resolver(s) selected for probing", len(data.Resolvers)))}
}
// resolversReachableRule flags the "no resolver answered" case.
type resolversReachableRule struct{}
func (r *resolversReachableRule) Name() string { return "resolver_propagation.reachable" }
func (r *resolversReachableRule) Description() string {
return "Checks that at least one selected resolver answered a query (detects a checker host with no DNS connectivity)."
}
func (r *resolversReachableRule) 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 {
return []sdk.CheckState{{Status: sdk.StatusUnknown, Code: "resolver_propagation.reachable.skipped",
Message: "no resolver in selection"}}
}
for _, rv := range data.Resolvers {
if rv.Reachable {
return []sdk.CheckState{passState("resolver_propagation.reachable.ok",
fmt.Sprintf("%d/%d resolver(s) answered at least one query",
data.Stats.ReachableResolvers, data.Stats.TotalResolvers))}
}
}
return []sdk.CheckState{critState(CodeAllResolversDown, data.Zone,
"no public resolver answered, the checker host may be offline, or DNS traffic is blocked on its network")}
}
// resolverLatencyRule flags resolvers with high average latency.
type resolverLatencyRule struct{}
func (r *resolverLatencyRule) Name() string { return "resolver_propagation.latency" }
func (r *resolverLatencyRule) Description() string {
return "Flags resolvers whose average response time exceeds the configured threshold."
}
func (r *resolverLatencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
data, errSt := loadData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
threshold := int64(sdk.GetIntOption(opts, "latencyThresholdMs", 500))
var states []sdk.CheckState
for _, rv := range data.Resolvers {
if !rv.Reachable {
states = append(states, warnState(CodeResolverUnreachable, rv.ID,
fmt.Sprintf("resolver %s (%s, %s) did not answer any query", rv.Name, rv.IP, rv.Transport)))
continue
}
var total, n int64
for _, p := range rv.Probes {
if p.Error != "" {
continue
}
total += p.LatencyMs
n++
}
if n > 0 {
avg := total / n
if avg > threshold {
states = append(states, infoState(CodeResolverHighLatency, rv.ID,
fmt.Sprintf("%s answered in %d ms on average (threshold %d ms)", rv.Name, avg, threshold)))
}
}
}
if len(states) == 0 {
return []sdk.CheckState{passState("resolver_propagation.latency.ok",
"All reachable resolvers respond within the latency threshold.")}
}
return states
}
// filteredHitRule notes when a filtered resolver returns a different answer
// than the consensus (i.e. a likely blocklist hit).
type filteredHitRule struct{}
func (r *filteredHitRule) Name() string { return "resolver_propagation.filtered_hit" }
func (r *filteredHitRule) Description() string {
return "Reports filtered resolvers returning a different answer than the consensus (typical blocklist behaviour)."
}
func (r *filteredHitRule) 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 _, rv := range data.Resolvers {
if !rv.Filtered {
continue
}
for key, p := range rv.Probes {
if p == nil || p.Error != "" || p.Rcode != "NOERROR" {
continue
}
rv2 := data.RRsets[key]
if rv2 == nil || rv2.ConsensusSig == "" {
continue
}
if p.Signature != rv2.ConsensusSig {
states = append(states, infoState(CodeResolverFilteredHit, rv.ID+" "+key,
fmt.Sprintf("%s (filtered) returned a different answer than the consensus for %s, likely a blocklist hit", rv.Name, key)))
}
}
}
if len(states) == 0 {
return []sdk.CheckState{passState("resolver_propagation.filtered_hit.ok",
"No filtered resolver deviates from the consensus (no blocklist hit detected).")}
}
return states
}