273 lines
9 KiB
Go
273 lines
9 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
type chainLoopRule struct{}
|
|
|
|
func (chainLoopRule) Name() string { return "chain_loop" }
|
|
func (chainLoopRule) Description() string {
|
|
return "Detects CNAME/DNAME cycles in the resolution chain."
|
|
}
|
|
|
|
func (chainLoopRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
if data.ChainTerminated.Reason != TermLoop {
|
|
return okState(data.Owner, "no loop in the alias chain")
|
|
}
|
|
return []sdk.CheckState{withHint(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: data.ChainTerminated.Subject,
|
|
Message: fmt.Sprintf("chain loops back to %s", data.ChainTerminated.Subject),
|
|
}, "Break the loop by pointing the last CNAME at an A/AAAA-bearing name.")}
|
|
}
|
|
|
|
type chainLengthRule struct{}
|
|
|
|
func (chainLengthRule) Name() string { return "chain_length" }
|
|
func (chainLengthRule) Description() string {
|
|
return "Flags alias chains longer than the configured maximum (most resolvers give up around 8-16 hops)."
|
|
}
|
|
|
|
func (chainLengthRule) Options() sdk.CheckerOptionsDocumentation {
|
|
return sdk.CheckerOptionsDocumentation{
|
|
UserOpts: []sdk.CheckerOptionDocumentation{
|
|
{
|
|
Id: "maxChainLength",
|
|
Type: "uint",
|
|
Label: "Maximum chain length",
|
|
Description: "Above this number of hops the chain is reported as critical.",
|
|
Default: float64(defaultMaxChainLength),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (chainLengthRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
maxLen := sdk.GetIntOption(opts, "maxChainLength", defaultMaxChainLength)
|
|
if data.ChainTerminated.Reason != TermTooLong {
|
|
return okState(data.Owner, fmt.Sprintf("chain has %d hop(s), within limit of %d", len(data.Chain), maxLen))
|
|
}
|
|
return []sdk.CheckState{withHint(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: data.ChainTerminated.Subject,
|
|
Message: fmt.Sprintf("chain exceeds %d hops; many resolvers will give up", maxLen),
|
|
}, "Flatten intermediate CNAMEs so that the chain is at most a few hops long.")}
|
|
}
|
|
|
|
type chainQueryErrorRule struct{}
|
|
|
|
func (chainQueryErrorRule) Name() string { return "chain_query_error" }
|
|
func (chainQueryErrorRule) Description() string {
|
|
return "Flags DNS query failures encountered while walking the alias chain."
|
|
}
|
|
|
|
func (chainQueryErrorRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
if data.ChainTerminated.Reason != TermQueryErr {
|
|
return okState(data.Owner, "all chain queries succeeded")
|
|
}
|
|
return []sdk.CheckState{withHint(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: data.ChainTerminated.Subject,
|
|
Message: fmt.Sprintf("CNAME query for %s failed: %s", data.ChainTerminated.Subject, data.ChainTerminated.Detail),
|
|
}, "Check authoritative-server reachability and firewall rules; the alias is unusable while queries fail.")}
|
|
}
|
|
|
|
type chainRcodeRule struct{}
|
|
|
|
func (chainRcodeRule) Name() string { return "chain_rcode" }
|
|
func (chainRcodeRule) Description() string {
|
|
return "Flags NXDOMAIN/SERVFAIL/other rcodes encountered mid-chain or on the final target lookup."
|
|
}
|
|
|
|
func (chainRcodeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
var out []sdk.CheckState
|
|
if data.ChainTerminated.Reason == TermRcode {
|
|
out = append(out, withHint(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: data.ChainTerminated.Subject,
|
|
Message: fmt.Sprintf("server answered %s mid-chain", data.ChainTerminated.Rcode),
|
|
}, "Ensure the zone publishes the expected record; NXDOMAIN/SERVFAIL mid-chain breaks the alias."))
|
|
}
|
|
if data.FinalRcode != "" && data.FinalRcode != "NOERROR" {
|
|
out = append(out, withHint(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: data.FinalTarget,
|
|
Message: fmt.Sprintf("final A lookup for %s returned %s", data.FinalTarget, data.FinalRcode),
|
|
}, "Check the upstream zone's A/AAAA publication."))
|
|
}
|
|
if len(out) == 0 {
|
|
return okState(data.Owner, "all chain and final lookups returned NOERROR")
|
|
}
|
|
return out
|
|
}
|
|
|
|
type hopTTLRule struct{}
|
|
|
|
func (hopTTLRule) Name() string { return "hop_ttl" }
|
|
func (hopTTLRule) Description() string {
|
|
return "Flags chain hops whose TTL is below the configured minimum."
|
|
}
|
|
|
|
func (hopTTLRule) Options() sdk.CheckerOptionsDocumentation {
|
|
return sdk.CheckerOptionsDocumentation{
|
|
UserOpts: []sdk.CheckerOptionDocumentation{
|
|
{
|
|
Id: "minTargetTTL",
|
|
Type: "uint",
|
|
Label: "Minimum TTL (seconds)",
|
|
Description: "Hops with a TTL below this threshold are flagged as a warning.",
|
|
Default: float64(defaultMinTargetTTL),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (hopTTLRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
if len(data.Chain) == 0 {
|
|
return skipped("chain is empty")
|
|
}
|
|
minTTL := uint32(sdk.GetIntOption(opts, "minTargetTTL", defaultMinTargetTTL))
|
|
var out []sdk.CheckState
|
|
for _, h := range data.Chain {
|
|
if h.Kind == KindTarget || h.TTL == 0 {
|
|
continue
|
|
}
|
|
if h.TTL < minTTL {
|
|
out = append(out, withHint(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: h.Owner,
|
|
Message: fmt.Sprintf("hop %s → %s has TTL %ds (< %d)", h.Owner, h.Target, h.TTL, minTTL),
|
|
}, "Raise the CNAME TTL to improve cache efficiency (5-15 minutes is a common floor)."))
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return okState(data.Owner, fmt.Sprintf("all chain hops have TTL ≥ %ds", minTTL))
|
|
}
|
|
return out
|
|
}
|
|
|
|
type targetResolvableRule struct{}
|
|
|
|
func (targetResolvableRule) Name() string { return "target_resolvable" }
|
|
func (targetResolvableRule) Description() string {
|
|
return "Verifies that the final target of the alias chain publishes at least one A or AAAA record."
|
|
}
|
|
|
|
func (targetResolvableRule) Options() sdk.CheckerOptionsDocumentation {
|
|
return sdk.CheckerOptionsDocumentation{
|
|
UserOpts: []sdk.CheckerOptionDocumentation{
|
|
{
|
|
Id: "requireResolvableTarget",
|
|
Type: "bool",
|
|
Label: "Require resolvable target",
|
|
Description: "When enabled, a chain whose final target returns no A/AAAA is reported as critical (otherwise a warning).",
|
|
Default: defaultRequireResolvableTarget,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
if data.ChainTerminated.Reason != TermOK {
|
|
return skipped("chain did not terminate normally")
|
|
}
|
|
if len(data.FinalA) > 0 || len(data.FinalAAAA) > 0 {
|
|
return okState(data.FinalTarget, fmt.Sprintf("target %s resolves to %d address(es)", data.FinalTarget, len(data.FinalA)+len(data.FinalAAAA)))
|
|
}
|
|
status := sdk.StatusWarn
|
|
if sdk.GetBoolOption(opts, "requireResolvableTarget", defaultRequireResolvableTarget) {
|
|
status = sdk.StatusCrit
|
|
}
|
|
rcode := data.FinalRcode
|
|
if rcode == "" {
|
|
rcode = "no A/AAAA"
|
|
}
|
|
return []sdk.CheckState{withHint(sdk.CheckState{
|
|
Status: status,
|
|
Subject: data.FinalTarget,
|
|
Message: fmt.Sprintf("final target %s does not resolve to an address (%s)", data.FinalTarget, rcode),
|
|
}, "Point the alias at a name that publishes at least one A or AAAA record, or fix the upstream zone.")}
|
|
}
|
|
|
|
type multipleRecordsRule struct{}
|
|
|
|
func (multipleRecordsRule) Name() string { return "multiple_records" }
|
|
func (multipleRecordsRule) Description() string {
|
|
return "Flags owners that carry more than one CNAME/DNAME record; only one is legal per owner."
|
|
}
|
|
|
|
func (multipleRecordsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadAlias(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !apexKnown(data) {
|
|
return skipped("apex lookup failed")
|
|
}
|
|
seen := map[string]int{}
|
|
for _, h := range data.Chain {
|
|
if h.Kind == KindCNAME || h.Kind == KindDNAME {
|
|
seen[h.Owner]++
|
|
}
|
|
}
|
|
var out []sdk.CheckState
|
|
for owner, n := range seen {
|
|
if n > 1 {
|
|
out = append(out, withHint(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: owner,
|
|
Message: fmt.Sprintf("%s carries %d CNAME/DNAME records in the chain", owner, n),
|
|
}, "Keep a single CNAME per name; remove duplicates at the authoritative zone."))
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return okState(data.Owner, "every chain owner carries a single CNAME/DNAME")
|
|
}
|
|
return out
|
|
}
|