144 lines
4.6 KiB
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
|
|
}
|