209 lines
6.5 KiB
Go
209 lines
6.5 KiB
Go
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)
|
|
}
|