139 lines
5 KiB
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
|
|
}
|