188 lines
5 KiB
Go
188 lines
5 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// fakeObs round-trips through JSON like the production read path so tests
|
|
// catch any tag drift between DNSSECData fields and rule expectations.
|
|
type fakeObs struct{ data *DNSSECData }
|
|
|
|
func (f fakeObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
|
|
if f.data == nil {
|
|
return nil
|
|
}
|
|
raw, err := json.Marshal(f.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(raw, dest)
|
|
}
|
|
|
|
func (fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func run(r sdk.CheckRule, data *DNSSECData, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
return r.Evaluate(context.Background(), fakeObs{data: data}, opts)
|
|
}
|
|
|
|
func signedZone(denial DenialKind, p *NSEC3ParamObservation) *DNSSECData {
|
|
return &DNSSECData{
|
|
Domain: "example.com",
|
|
Servers: map[string]PerServerView{
|
|
"ns1.example.com.:53": {
|
|
Server: "ns1.example.com.:53",
|
|
DNSKEYs: []DNSKEYRecord{{Flags: 257, Algorithm: 13, KeyTag: 12345, IsKSK: true}},
|
|
NSEC3PARAM: p,
|
|
DenialKind: denial,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func wantStatus(t *testing.T, states []sdk.CheckState, want sdk.Status) {
|
|
t.Helper()
|
|
if len(states) == 0 {
|
|
t.Fatalf("no states returned")
|
|
}
|
|
if states[0].Status != want {
|
|
t.Fatalf("status = %v, want %v: %+v", states[0].Status, want, states[0])
|
|
}
|
|
}
|
|
|
|
func TestDenialUsesNSEC3(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
data *DNSSECData
|
|
want sdk.Status
|
|
}{
|
|
{
|
|
name: "NSEC zone is walkable -> WARN",
|
|
data: signedZone(DenialNSEC, nil),
|
|
want: sdk.StatusWarn,
|
|
},
|
|
{
|
|
name: "NSEC3 zone -> OK",
|
|
data: signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0}),
|
|
want: sdk.StatusOK,
|
|
},
|
|
{
|
|
name: "OPT-OUT zone -> OK",
|
|
data: signedZone(DenialOptOut, &NSEC3ParamObservation{Iterations: 0, Flags: 1}),
|
|
want: sdk.StatusOK,
|
|
},
|
|
{
|
|
name: "Unsigned zone -> INFO",
|
|
data: &DNSSECData{Domain: "x", Servers: map[string]PerServerView{
|
|
"ns1:53": {Server: "ns1:53", DenialKind: DenialNone},
|
|
}},
|
|
want: sdk.StatusInfo,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
wantStatus(t, run(denialUsesNSEC3Rule{}, tc.data, nil), tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNSEC3Iterations(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
iter uint16
|
|
opts sdk.CheckerOptions
|
|
want sdk.Status
|
|
}{
|
|
{"iter=0 -> OK", 0, nil, sdk.StatusOK},
|
|
{"iter=1 default ceiling 0 -> WARN", 1, nil, sdk.StatusWarn},
|
|
{"iter=10 default ceiling 0 -> WARN", 10, nil, sdk.StatusWarn},
|
|
{"iter=10 ceiling 100 -> OK", 10, sdk.CheckerOptions{"nsec3IterationsMax": float64(100)}, sdk.StatusOK},
|
|
{"iter=10 severity=crit -> CRIT", 10, sdk.CheckerOptions{"nsec3IterationsSeverity": "crit"}, sdk.StatusCrit},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: tc.iter})
|
|
wantStatus(t, run(nsec3IterationsRule{}, data, tc.opts), tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNSEC3SaltEmpty(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
saltLength uint8
|
|
want sdk.Status
|
|
}{
|
|
{"empty salt -> OK", 0, sdk.StatusOK},
|
|
{"non-empty salt -> WARN", 8, sdk.StatusWarn},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{
|
|
Iterations: 0, SaltLength: tc.saltLength, Salt: strings.Repeat("ab", int(tc.saltLength)),
|
|
})
|
|
wantStatus(t, run(nsec3SaltEmptyRule{}, data, nil), tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNSEC3OptOut(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
flags uint8
|
|
want sdk.Status
|
|
}{
|
|
{"OPT-OUT off -> OK", 0, sdk.StatusOK},
|
|
{"OPT-OUT on -> INFO", 1, sdk.StatusInfo},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
data := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, Flags: tc.flags})
|
|
wantStatus(t, run(nsec3OptOutRule{}, data, nil), tc.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDenialConsistent(t *testing.T) {
|
|
consistent := &DNSSECData{
|
|
Domain: "x",
|
|
Servers: map[string]PerServerView{
|
|
"ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC3},
|
|
"ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3},
|
|
},
|
|
}
|
|
wantStatus(t, run(denialConsistentRule{}, consistent, nil), sdk.StatusOK)
|
|
|
|
drifting := &DNSSECData{
|
|
Domain: "x",
|
|
Servers: map[string]PerServerView{
|
|
"ns1:53": {Server: "ns1:53", DenialKind: DenialNSEC},
|
|
"ns2:53": {Server: "ns2:53", DenialKind: DenialNSEC3},
|
|
},
|
|
}
|
|
wantStatus(t, run(denialConsistentRule{}, drifting, nil), sdk.StatusWarn)
|
|
}
|
|
|
|
func TestRoundTripJSON(t *testing.T) {
|
|
d := signedZone(DenialNSEC3, &NSEC3ParamObservation{Iterations: 0, SaltLength: 0})
|
|
raw, err := json.Marshal(d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var back DNSSECData
|
|
if err := json.Unmarshal(raw, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back.Domain != d.Domain {
|
|
t.Fatalf("domain round-trip lost: %q vs %q", back.Domain, d.Domain)
|
|
}
|
|
if got := back.Servers["ns1.example.com.:53"].DenialKind; got != DenialNSEC3 {
|
|
t.Fatalf("denial round-trip lost: %v", got)
|
|
}
|
|
}
|