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 }