package checker import ( "context" "fmt" sdk "git.happydns.org/checker-sdk-go/checker" ) type nsResolvableRule struct{} func (r *nsResolvableRule) Name() string { return "authoritative_consistency.ns_resolvable" } func (r *nsResolvableRule) Description() string { return "Verifies that every authoritative name server hostname resolves to at least one A or AAAA address." } func (r *nsResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadObservation(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } var findings []Finding for _, ns := range data.Probed { res := data.Results[ns] if res == nil { continue } if res.ResolveError != "" { findings = append(findings, Finding{ Code: CodeNSUnresolvable, Severity: SeverityCrit, Message: fmt.Sprintf("cannot resolve %s: %s", ns, res.ResolveError), Server: ns, }) } } if len(findings) == 0 { return []sdk.CheckState{passState("authoritative_consistency.ns_resolvable.ok", "Every probed name server resolves to at least one address.")} } return findingsToStates(findings) } type nsReachableRule struct{} func (r *nsReachableRule) Name() string { return "authoritative_consistency.ns_reachable" } func (r *nsReachableRule) Description() string { return "Verifies that every authoritative name server answers over UDP/53 and TCP/53." } func (r *nsReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadObservation(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } requireTCP := sdk.GetBoolOption(opts, "requireTCP", true) var findings []Finding for _, ns := range data.Probed { res := data.Results[ns] if res == nil || res.ResolveError != "" { continue } if !res.UDPReachable { findings = append(findings, Finding{ Code: CodeNSUDPFailed, Severity: SeverityCrit, Message: fmt.Sprintf("%s did not answer any SOA query over UDP/53", ns), Server: ns, }) continue } if !res.TCPReachable { sev := SeverityWarn msg := fmt.Sprintf("%s did not answer over TCP/53", ns) if requireTCP { sev = SeverityCrit msg = fmt.Sprintf("%s did not answer over TCP/53 (required by RFC 7766 and DNSSEC)", ns) } findings = append(findings, Finding{ Code: CodeNSTCPFailed, Severity: sev, Message: msg, Server: ns, }) } } if len(findings) == 0 { return []sdk.CheckState{passState("authoritative_consistency.ns_reachable.ok", "Every probed name server is reachable over UDP/53 and TCP/53.")} } return findingsToStates(findings) } type authoritativeRule struct{} func (r *authoritativeRule) Name() string { return "authoritative_consistency.authoritative" } func (r *authoritativeRule) Description() string { return "Verifies that every reachable name server is authoritative for the zone (no lame delegation) and returns a SOA." } func (r *authoritativeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadObservation(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } var findings []Finding for _, ns := range data.Probed { res := data.Results[ns] if res == nil || !res.UDPReachable { continue } if !res.Authoritative { findings = append(findings, Finding{ Code: CodeLame, Severity: SeverityCrit, Message: fmt.Sprintf("%s is not authoritative for %s (lame delegation)", ns, data.Zone), Server: ns, }) continue } if data.HasSOA && res.SOA == nil { findings = append(findings, Finding{ Code: CodeNoSOA, Severity: SeverityCrit, Message: fmt.Sprintf("%s is authoritative but returned no SOA for %s", ns, data.Zone), Server: ns, }) } } if len(findings) == 0 { return []sdk.CheckState{passState("authoritative_consistency.authoritative.ok", "Every reachable name server is authoritative for the zone.")} } return findingsToStates(findings) } type ednsRule struct{} func (r *ednsRule) Name() string { return "authoritative_consistency.edns" } func (r *ednsRule) Description() string { return "Verifies that every reachable name server correctly handles EDNS0 queries (required by DNSSEC and for large answers)." } func (r *ednsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { if !sdk.GetBoolOption(opts, "checkEDNS", true) { return []sdk.CheckState{notTestedState("authoritative_consistency.edns.skipped", "EDNS0 check disabled by option.")} } data, errSt := loadObservation(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } var findings []Finding for _, ns := range data.Probed { res := data.Results[ns] if res == nil || !res.UDPReachable { continue } if !res.EDNSSupported { findings = append(findings, Finding{ Code: CodeEDNSUnsupported, Severity: SeverityWarn, Message: fmt.Sprintf("%s does not correctly handle EDNS0 (breaks DNSSEC and large answers)", ns), Server: ns, }) } } if len(findings) == 0 { return []sdk.CheckState{passState("authoritative_consistency.edns.ok", "Every reachable name server handles EDNS0 correctly.")} } return findingsToStates(findings) } type latencyRule struct{} func (r *latencyRule) Name() string { return "authoritative_consistency.latency" } func (r *latencyRule) Description() string { return "Flags authoritative name servers whose response latency exceeds the configured threshold." } func (r *latencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { if !sdk.GetBoolOption(opts, "checkLatency", true) { return []sdk.CheckState{notTestedState("authoritative_consistency.latency.skipped", "Latency check disabled by option.")} } data, errSt := loadObservation(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } threshold := int64(sdk.GetIntOption(opts, "latencyThresholdMs", 500)) var findings []Finding for _, ns := range data.Probed { res := data.Results[ns] if res == nil || !res.UDPReachable { continue } if res.LatencyMs > threshold { findings = append(findings, Finding{ Code: CodeSlowNS, Severity: SeverityInfo, Message: fmt.Sprintf("%s responded in %d ms (above %d ms threshold)", ns, res.LatencyMs, threshold), Server: ns, }) } } if len(findings) == 0 { return []sdk.CheckState{passState("authoritative_consistency.latency.ok", "Every reachable name server responded within the configured threshold.")} } return findingsToStates(findings) }