package checker import ( "context" "fmt" "sort" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) // serialDriftRule flags disagreement between resolvers on the SOA serial. type serialDriftRule struct{} func (r *serialDriftRule) Name() string { return "resolver_propagation.serial_drift" } func (r *serialDriftRule) Description() string { return "Flags disagreement on the SOA serial across unfiltered resolvers." } func (r *serialDriftRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } soaKey := rrsetKey(data.Zone, "SOA") if data.RRsets[soaKey] == nil { return []sdk.CheckState{{Status: sdk.StatusUnknown, Code: "resolver_propagation.serial_drift.skipped", Message: "SOA was not probed"}} } serials := map[uint32][]string{} for _, rv := range data.Resolvers { if rv.Filtered { continue } p := rv.Probes[soaKey] if p == nil || p.Error != "" || p.Rcode != "NOERROR" { continue } if s := extractSerial(p.Records); s != 0 { serials[s] = append(serials[s], rv.ID) } } if len(serials) < 2 { return []sdk.CheckState{passState("resolver_propagation.serial_drift.ok", "SOA serial is consistent across unfiltered resolvers.")} } var parts []string for s, rs := range serials { sort.Strings(rs) parts = append(parts, fmt.Sprintf("serial %d on %s", s, firstN(rs, 6))) } sort.Strings(parts) return []sdk.CheckState{warnState(CodeSerialDrift, soaKey, "SOA serial differs across resolvers, "+strings.Join(parts, "; "))} } // staleCacheRule flags resolvers still serving a serial below the declared one. type staleCacheRule struct{} func (r *staleCacheRule) Name() string { return "resolver_propagation.stale_cache" } func (r *staleCacheRule) Description() string { return "Flags resolvers still serving an SOA serial below the one saved by happyDomain." } func (r *staleCacheRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } if data.DeclaredSerial == 0 { return []sdk.CheckState{{Status: sdk.StatusUnknown, Code: "resolver_propagation.stale_cache.skipped", Message: "no declared SOA serial available for comparison"}} } soaKey := rrsetKey(data.Zone, "SOA") if data.RRsets[soaKey] == nil { return []sdk.CheckState{{Status: sdk.StatusUnknown, Code: "resolver_propagation.stale_cache.skipped", Message: "SOA was not probed"}} } var below []string for _, rv := range data.Resolvers { if rv.Filtered { continue } p := rv.Probes[soaKey] if p == nil || p.Error != "" || p.Rcode != "NOERROR" { continue } s := extractSerial(p.Records) if s != 0 && s < data.DeclaredSerial { below = append(below, rv.ID) } } if len(below) == 0 { return []sdk.CheckState{passState("resolver_propagation.stale_cache.ok", "No resolver is still serving an outdated SOA serial.")} } sort.Strings(below) return []sdk.CheckState{infoState(CodeStaleCache, soaKey, fmt.Sprintf("%d resolver(s) still return a serial below the declared one (%d): %s", len(below), data.DeclaredSerial, firstN(below, 6)))} } // dnssecRule flags DNSSEC failures (SERVFAIL or missing AD) at the zone apex // on resolvers known to validate. type dnssecRule struct{} func (r *dnssecRule) Name() string { return "resolver_propagation.dnssec" } func (r *dnssecRule) Description() string { return "Checks that validating resolvers successfully validate the zone's DNSSEC chain." } func (r *dnssecRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } soaKey := rrsetKey(data.Zone, "SOA") var states []sdk.CheckState for _, rv := range data.Resolvers { if rv.Filtered || !isValidatingResolver(rv.ID) { continue } soa := rv.Probes[soaKey] if soa == nil || soa.Error != "" { continue } switch soa.Rcode { case "SERVFAIL": states = append(states, critState(CodeDNSSECFailure, rv.ID, fmt.Sprintf("%s returned SERVFAIL for %s, typically a broken DNSSEC chain", rv.Name, data.Zone))) case "NOERROR": if !soa.AD { states = append(states, infoState(CodeDNSSECUnvalidated, rv.ID, fmt.Sprintf("%s did not set AD=1 for %s, zone may not be DNSSEC-signed, or signature is broken", rv.Name, data.Zone))) } } } if len(states) == 0 { return []sdk.CheckState{passState("resolver_propagation.dnssec.ok", "Validating resolvers report no DNSSEC issue.")} } return states }