checker-resolver-propagation/checker/rules_soa.go

144 lines
4.6 KiB
Go

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
}