Compare commits
5 commits
c884f3f80a
...
70c548284e
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c548284e | |||
| a16e01e1d4 | |||
| 557bed4330 | |||
| 3366cebf7d | |||
| 7e8faa7169 |
5 changed files with 1159 additions and 29 deletions
|
|
@ -58,12 +58,14 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
for _, ps := range parentServers {
|
||||
view := ParentView{Server: ps}
|
||||
|
||||
ns, glue, _, qerr := queryDelegation(ctx, ps, delegatedFQDN)
|
||||
ns, glue, nsTTL, nsTTLKnown, qerr := queryDelegation(ctx, ps, delegatedFQDN)
|
||||
if qerr != nil {
|
||||
view.UDPNSError = qerr.Error()
|
||||
} else {
|
||||
view.NS = ns
|
||||
view.Glue = glue
|
||||
view.NSTTL = nsTTL
|
||||
view.NSTTLKnown = nsTTLKnown
|
||||
}
|
||||
|
||||
if terr := queryDelegationTCP(ctx, ps, delegatedFQDN); terr != nil {
|
||||
|
|
@ -89,13 +91,7 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
}
|
||||
|
||||
// If no parent answered with an NS RRset, skip Phase B; rules flag the gap.
|
||||
var primary *ParentView
|
||||
for i := range data.ParentViews {
|
||||
if data.ParentViews[i].UDPNSError == "" && len(data.ParentViews[i].NS) > 0 {
|
||||
primary = &data.ParentViews[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
primary := primaryParentView(data.ParentViews)
|
||||
if primary == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
|
@ -103,6 +99,11 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
// Phase B: per-child observations, seeded only from parent data.
|
||||
for _, nsName := range primary.NS {
|
||||
child := ChildNSView{NSName: nsName}
|
||||
|
||||
if target, cerr := queryCNAMETarget(ctx, nsName); cerr == nil && target != "" {
|
||||
child.CNAMETarget = target
|
||||
}
|
||||
|
||||
addrs := primary.Glue[nsName]
|
||||
if len(addrs) == 0 {
|
||||
// Out-of-bailiwick: no glue expected, fall back to the system resolver.
|
||||
|
|
@ -148,22 +149,12 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
|||
av.ChildGlueAddrs = addrsAt
|
||||
}
|
||||
|
||||
// DNSKEY is only useful when there's a parent DS to match against.
|
||||
parentHasDS := false
|
||||
for _, pv := range data.ParentViews {
|
||||
if len(pv.DS) > 0 {
|
||||
parentHasDS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if parentHasDS {
|
||||
keys, kerr := queryDNSKEY(ctx, srv, delegatedFQDN)
|
||||
if kerr != nil {
|
||||
av.DNSKEYError = kerr.Error()
|
||||
} else {
|
||||
for _, k := range keys {
|
||||
av.DNSKEYs = append(av.DNSKEYs, NewDNSKEYRecord(k))
|
||||
}
|
||||
keys, kerr := queryDNSKEY(ctx, srv, delegatedFQDN)
|
||||
if kerr != nil {
|
||||
av.DNSKEYError = kerr.Error()
|
||||
} else {
|
||||
for _, k := range keys {
|
||||
av.DNSKEYs = append(av.DNSKEYs, NewDNSKEYRecord(k))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,15 +108,16 @@ func resolveZoneNSAddrs(ctx context.Context, zone string) ([]string, error) {
|
|||
|
||||
// queryDelegation expects a referral response (no RD) and pulls NS + glue
|
||||
// from every section so misconfigured parents (NS in Answer) still parse.
|
||||
func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []string, glue map[string][]string, msg *dns.Msg, err error) {
|
||||
// nsTTL is the TTL of the first matching NS record (all RRset members share it).
|
||||
func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []string, glue map[string][]string, nsTTL uint32, nsTTLKnown bool, err error) {
|
||||
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeNS, Qclass: dns.ClassINET}
|
||||
|
||||
msg, err = dnsExchange(ctx, "", parentServer, q, true)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
msg, merr := dnsExchange(ctx, "", parentServer, q, true)
|
||||
if merr != nil {
|
||||
return nil, nil, 0, false, merr
|
||||
}
|
||||
if msg.Rcode != dns.RcodeSuccess {
|
||||
return nil, nil, msg, fmt.Errorf("parent answered %s", dns.RcodeToString[msg.Rcode])
|
||||
return nil, nil, 0, false, fmt.Errorf("parent answered %s", dns.RcodeToString[msg.Rcode])
|
||||
}
|
||||
|
||||
glue = map[string][]string{}
|
||||
|
|
@ -127,6 +128,10 @@ func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []strin
|
|||
case *dns.NS:
|
||||
if strings.EqualFold(strings.TrimSuffix(t.Header().Name, "."), strings.TrimSuffix(fqdn, ".")) {
|
||||
ns = append(ns, strings.ToLower(dns.Fqdn(t.Ns)))
|
||||
if !nsTTLKnown {
|
||||
nsTTL = t.Header().Ttl
|
||||
nsTTLKnown = true
|
||||
}
|
||||
}
|
||||
case *dns.A:
|
||||
name := strings.ToLower(dns.Fqdn(t.Header().Name))
|
||||
|
|
@ -143,6 +148,20 @@ func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []strin
|
|||
return
|
||||
}
|
||||
|
||||
// queryCNAMETarget returns the CNAME target if host is an alias, or empty
|
||||
// string if it is not. Uses the system resolver, consistent with resolveHost.
|
||||
func queryCNAMETarget(ctx context.Context, host string) (string, error) {
|
||||
var resolver net.Resolver
|
||||
canon, err := resolver.LookupCNAME(ctx, strings.TrimSuffix(host, "."))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.EqualFold(dns.Fqdn(canon), dns.Fqdn(host)) {
|
||||
return "", nil
|
||||
}
|
||||
return strings.TrimSuffix(dns.Fqdn(canon), "."), nil
|
||||
}
|
||||
|
||||
// queryDS uses TCP because DS+RRSIG answers commonly exceed UDP MTU.
|
||||
func queryDS(ctx context.Context, parentServer, fqdn string) (ds []*dns.DS, sigs []*dns.RRSIG, err error) {
|
||||
q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeDS, Qclass: dns.ClassINET}
|
||||
|
|
|
|||
396
checker/rule.go
396
checker/rule.go
|
|
@ -16,17 +16,22 @@ import (
|
|||
func Rules() []sdk.CheckRule {
|
||||
return []sdk.CheckRule{
|
||||
&minNameServersRule{},
|
||||
&duplicateNSRule{},
|
||||
&parentDiscoveredRule{},
|
||||
&parentNSQueryRule{},
|
||||
&parentTCPRule{},
|
||||
&nsMatchesDeclaredRule{},
|
||||
&parentNSConsistencyRule{},
|
||||
&nsTTLConsistencyRule{},
|
||||
&inBailiwickGlueRule{},
|
||||
&unnecessaryGlueRule{},
|
||||
&dsQueryRule{},
|
||||
&dsMatchesDeclaredRule{},
|
||||
&parentDSConsistencyRule{},
|
||||
&dsPresentAtParentRule{},
|
||||
&dsRRSIGValidityRule{},
|
||||
&nsResolvableRule{},
|
||||
&nsTargetNotCNAMERule{},
|
||||
&childReachableRule{},
|
||||
&childAuthoritativeRule{},
|
||||
&childSOASerialDriftRule{},
|
||||
|
|
@ -35,6 +40,9 @@ func Rules() []sdk.CheckRule {
|
|||
&childGlueMatchesParentRule{},
|
||||
&dnskeyQueryRule{},
|
||||
&dnskeyMatchesDSRule{},
|
||||
&dnskeyKSKPresentRule{},
|
||||
&dnskeyProtocolRule{},
|
||||
&dsCoversAllKSKsRule{},
|
||||
&nsHasAuthoritativeAnswerRule{},
|
||||
}
|
||||
}
|
||||
|
|
@ -526,6 +534,56 @@ func rrsigReason(sig DSRRSIGObservation, now time.Time) string {
|
|||
}
|
||||
}
|
||||
|
||||
type nsTTLConsistencyRule struct{}
|
||||
|
||||
func (r *nsTTLConsistencyRule) Name() string { return "delegation_ns_ttl_consistency" }
|
||||
func (r *nsTTLConsistencyRule) Description() string {
|
||||
return "Verifies that all parent authoritative servers serve the NS RRset with the same TTL"
|
||||
}
|
||||
func (r *nsTTLConsistencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_ns_ttl_inconsistent")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
type entry struct {
|
||||
server string
|
||||
ttl uint32
|
||||
}
|
||||
var known []entry
|
||||
for _, v := range data.ParentViews {
|
||||
if v.NSTTLKnown {
|
||||
known = append(known, entry{v.Server, v.NSTTL})
|
||||
}
|
||||
}
|
||||
if len(known) < 2 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_ns_ttl_inconsistent",
|
||||
Message: "fewer than two parent servers returned an NS TTL",
|
||||
}}
|
||||
}
|
||||
ref := known[0]
|
||||
var bad []string
|
||||
for _, e := range known[1:] {
|
||||
if e.ttl != ref.ttl {
|
||||
bad = append(bad, fmt.Sprintf("%s serves TTL %d (reference %d from %s)", e.server, e.ttl, ref.ttl, ref.server))
|
||||
}
|
||||
}
|
||||
if len(bad) > 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusWarn,
|
||||
Code: "delegation_ns_ttl_inconsistent",
|
||||
Message: fmt.Sprintf("NS TTL inconsistency: %s", strings.Join(bad, "; ")),
|
||||
Meta: map[string]any{"reference_ttl": ref.ttl, "reference_server": ref.server},
|
||||
}}
|
||||
}
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusOK,
|
||||
Code: "delegation_ns_ttl_inconsistent",
|
||||
Message: fmt.Sprintf("all parent servers agree on NS TTL (%ds)", ref.ttl),
|
||||
}}
|
||||
}
|
||||
|
||||
// ───────────────────────── child-side rules ─────────────────────────
|
||||
|
||||
type nsResolvableRule struct{}
|
||||
|
|
@ -564,6 +622,44 @@ func (r *nsResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGett
|
|||
return out
|
||||
}
|
||||
|
||||
type nsTargetNotCNAMERule struct{}
|
||||
|
||||
func (r *nsTargetNotCNAMERule) Name() string { return "delegation_ns_not_cname" }
|
||||
func (r *nsTargetNotCNAMERule) Description() string {
|
||||
return "Verifies that NS target names are not CNAME aliases (RFC 2181 §10.3)"
|
||||
}
|
||||
func (r *nsTargetNotCNAMERule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_ns_is_cname")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
if len(data.Children) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_ns_is_cname",
|
||||
Message: "no NS names to check",
|
||||
}}
|
||||
}
|
||||
var out []sdk.CheckState
|
||||
for _, c := range data.Children {
|
||||
st := sdk.CheckState{Code: "delegation_ns_is_cname", Subject: c.NSName}
|
||||
switch {
|
||||
case c.CNAMETarget != "":
|
||||
st.Status = sdk.StatusCrit
|
||||
st.Message = fmt.Sprintf("NS target is a CNAME alias to %s (RFC 2181 §10.3 forbids this)", c.CNAMETarget)
|
||||
st.Meta = map[string]any{"cname_target": c.CNAMETarget}
|
||||
case c.ResolveError != "":
|
||||
st.Status = sdk.StatusUnknown
|
||||
st.Message = fmt.Sprintf("could not verify CNAME status: %s", c.ResolveError)
|
||||
default:
|
||||
st.Status = sdk.StatusOK
|
||||
st.Message = "not a CNAME"
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type childReachableRule struct{}
|
||||
|
||||
func (r *childReachableRule) Name() string { return "delegation_child_reachable" }
|
||||
|
|
@ -946,6 +1042,306 @@ func (r *dnskeyMatchesDSRule) Evaluate(ctx context.Context, obs sdk.ObservationG
|
|||
return out
|
||||
}
|
||||
|
||||
// ------------------------- declaration checks -------------------------
|
||||
|
||||
type duplicateNSRule struct{}
|
||||
|
||||
func (r *duplicateNSRule) Name() string { return "delegation_duplicate_ns" }
|
||||
func (r *duplicateNSRule) Description() string {
|
||||
return "Checks that the declared NS list contains no duplicate name server names"
|
||||
}
|
||||
func (r *duplicateNSRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_duplicate_ns")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
// DeclaredNS is already sorted and lowercased by normalizeNSList.
|
||||
var dups []string
|
||||
for i := 1; i < len(data.DeclaredNS); i++ {
|
||||
if data.DeclaredNS[i] == data.DeclaredNS[i-1] {
|
||||
dups = append(dups, data.DeclaredNS[i])
|
||||
}
|
||||
}
|
||||
if len(dups) > 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusWarn,
|
||||
Code: "delegation_duplicate_ns",
|
||||
Message: fmt.Sprintf("declared NS list contains duplicates: %v", dups),
|
||||
Meta: map[string]any{"duplicates": dups},
|
||||
}}
|
||||
}
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusOK,
|
||||
Code: "delegation_duplicate_ns",
|
||||
Message: "no duplicate name server names in declared list",
|
||||
}}
|
||||
}
|
||||
|
||||
// ------------------------- parent-side consistency -------------------------
|
||||
|
||||
type parentNSConsistencyRule struct{}
|
||||
|
||||
func (r *parentNSConsistencyRule) Name() string { return "delegation_parent_ns_consistency" }
|
||||
func (r *parentNSConsistencyRule) Description() string {
|
||||
return "Verifies that all parent authoritative servers return the same NS RRset for the delegated zone"
|
||||
}
|
||||
func (r *parentNSConsistencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_parent_ns_inconsistent")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
var valid []ParentView
|
||||
for _, v := range data.ParentViews {
|
||||
if v.UDPNSError == "" && len(v.NS) > 0 {
|
||||
valid = append(valid, v)
|
||||
}
|
||||
}
|
||||
if len(valid) < 2 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_parent_ns_inconsistent",
|
||||
Message: "fewer than 2 parent servers returned an NS RRset; cross-server comparison not possible",
|
||||
}}
|
||||
}
|
||||
ref := valid[0]
|
||||
out := make([]sdk.CheckState, 0, len(valid)-1)
|
||||
for _, v := range valid[1:] {
|
||||
missing, extra := diffStringSets(ref.NS, v.NS)
|
||||
st := sdk.CheckState{Code: "delegation_parent_ns_inconsistent", Subject: v.Server}
|
||||
if len(missing) > 0 || len(extra) > 0 {
|
||||
st.Status = sdk.StatusCrit
|
||||
st.Message = fmt.Sprintf("NS RRset differs from %s: missing=%v extra=%v", ref.Server, missing, extra)
|
||||
st.Meta = map[string]any{"reference": ref.Server, "missing": missing, "extra": extra}
|
||||
} else {
|
||||
st.Status = sdk.StatusOK
|
||||
st.Message = fmt.Sprintf("NS RRset matches %s", ref.Server)
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type parentDSConsistencyRule struct{}
|
||||
|
||||
func (r *parentDSConsistencyRule) Name() string { return "delegation_parent_ds_consistency" }
|
||||
func (r *parentDSConsistencyRule) Description() string {
|
||||
return "Verifies that all parent authoritative servers return the same DS RRset for the delegated zone"
|
||||
}
|
||||
func (r *parentDSConsistencyRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_parent_ds_inconsistent")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
var valid []ParentView
|
||||
for _, v := range data.ParentViews {
|
||||
if v.DSQueryError == "" {
|
||||
valid = append(valid, v)
|
||||
}
|
||||
}
|
||||
if len(valid) < 2 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_parent_ds_inconsistent",
|
||||
Message: "fewer than 2 parent servers returned a DS response; cross-server comparison not possible",
|
||||
}}
|
||||
}
|
||||
ref := valid[0]
|
||||
refDS := dsRecordsToMiekg(ref.DS)
|
||||
out := make([]sdk.CheckState, 0, len(valid)-1)
|
||||
for _, v := range valid[1:] {
|
||||
missing, extra := diffDS(refDS, dsRecordsToMiekg(v.DS))
|
||||
st := sdk.CheckState{Code: "delegation_parent_ds_inconsistent", Subject: v.Server}
|
||||
if len(missing) > 0 || len(extra) > 0 {
|
||||
st.Status = sdk.StatusCrit
|
||||
st.Message = fmt.Sprintf("DS RRset differs from %s: missing=%d extra=%d", ref.Server, len(missing), len(extra))
|
||||
st.Meta = map[string]any{"reference": ref.Server, "missing": len(missing), "extra": len(extra)}
|
||||
} else {
|
||||
st.Status = sdk.StatusOK
|
||||
st.Message = fmt.Sprintf("DS RRset matches %s", ref.Server)
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ------------------------- DNSKEY integrity rules -------------------------
|
||||
|
||||
type dnskeyKSKPresentRule struct{}
|
||||
|
||||
func (r *dnskeyKSKPresentRule) Name() string { return "delegation_dnskey_ksk_present" }
|
||||
func (r *dnskeyKSKPresentRule) Description() string {
|
||||
return "Verifies that at least one DNSKEY with the SEP bit set (KSK, flags=257) is present when the parent publishes DS records"
|
||||
}
|
||||
func (r *dnskeyKSKPresentRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_dnskey_no_ksk")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
if !parentHasAnyDS(data.ParentViews) {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_dnskey_no_ksk",
|
||||
Message: "parent has no DS records, KSK check skipped",
|
||||
}}
|
||||
}
|
||||
var out []sdk.CheckState
|
||||
for _, c := range data.Children {
|
||||
var keys []DNSKEYRecord
|
||||
for _, a := range c.Addresses {
|
||||
keys = append(keys, a.DNSKEYs...)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
continue
|
||||
}
|
||||
hasKSK := false
|
||||
for _, k := range keys {
|
||||
if k.Flags&0x0001 != 0 {
|
||||
hasKSK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
st := sdk.CheckState{Code: "delegation_dnskey_no_ksk", Subject: c.NSName}
|
||||
if hasKSK {
|
||||
st.Status = sdk.StatusOK
|
||||
st.Message = "at least one KSK (SEP bit set, flags=257) found"
|
||||
} else {
|
||||
st.Status = sdk.StatusCrit
|
||||
st.Message = "no KSK (SEP bit, flags=257) found among served DNSKEY records"
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_dnskey_no_ksk",
|
||||
Message: "no DNSKEY records observed at any child server",
|
||||
}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type dnskeyProtocolRule struct{}
|
||||
|
||||
func (r *dnskeyProtocolRule) Name() string { return "delegation_dnskey_protocol" }
|
||||
func (r *dnskeyProtocolRule) Description() string {
|
||||
return "Verifies that every DNSKEY record has Protocol=3 as required by RFC 4034 §2.1"
|
||||
}
|
||||
func (r *dnskeyProtocolRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_dnskey_bad_protocol")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
hasSomeKeys := false
|
||||
var out []sdk.CheckState
|
||||
for _, c := range data.Children {
|
||||
// Deduplicate by public key to avoid reporting the same key once per address.
|
||||
seen := map[string]bool{}
|
||||
for _, a := range c.Addresses {
|
||||
for _, k := range a.DNSKEYs {
|
||||
hasSomeKeys = true
|
||||
if seen[k.PublicKey] {
|
||||
continue
|
||||
}
|
||||
seen[k.PublicKey] = true
|
||||
if k.Protocol != 3 {
|
||||
out = append(out, sdk.CheckState{
|
||||
Status: sdk.StatusCrit,
|
||||
Code: "delegation_dnskey_bad_protocol",
|
||||
Subject: fmt.Sprintf("keytag=%d@%s", k.ToMiekg().KeyTag(), c.NSName),
|
||||
Message: fmt.Sprintf("DNSKEY has Protocol=%d, must be 3 (RFC 4034 §2.1)", k.Protocol),
|
||||
Meta: map[string]any{"protocol": k.Protocol},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
if !hasSomeKeys {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_dnskey_bad_protocol",
|
||||
Message: "no DNSKEY records observed",
|
||||
}}
|
||||
}
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusOK,
|
||||
Code: "delegation_dnskey_bad_protocol",
|
||||
Message: "all DNSKEY records have Protocol=3",
|
||||
}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type dsCoversAllKSKsRule struct{}
|
||||
|
||||
func (r *dsCoversAllKSKsRule) Name() string { return "delegation_dnskey_ksk_uncovered" }
|
||||
func (r *dsCoversAllKSKsRule) Description() string {
|
||||
return "Verifies that every KSK (SEP bit set) served by the child has a corresponding DS record at the parent"
|
||||
}
|
||||
func (r *dsCoversAllKSKsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
data, errState := loadData(ctx, obs, "delegation_dnskey_ksk_uncovered")
|
||||
if errState != nil {
|
||||
return errState
|
||||
}
|
||||
if !parentHasAnyDS(data.ParentViews) {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_dnskey_ksk_uncovered",
|
||||
Message: "parent has no DS records, KSK coverage check skipped",
|
||||
}}
|
||||
}
|
||||
var parentDS []*dns.DS
|
||||
for _, v := range data.ParentViews {
|
||||
if len(v.DS) > 0 {
|
||||
parentDS = dsRecordsToMiekg(v.DS)
|
||||
break
|
||||
}
|
||||
}
|
||||
var out []sdk.CheckState
|
||||
for _, c := range data.Children {
|
||||
// Collect unique KSKs across all addresses of this NS, keyed by public key.
|
||||
kskByKey := map[string]*dns.DNSKEY{}
|
||||
for _, a := range c.Addresses {
|
||||
for _, k := range a.DNSKEYs {
|
||||
if k.Flags&0x0001 != 0 {
|
||||
mk := k.ToMiekg()
|
||||
kskByKey[mk.PublicKey] = mk
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, ksk := range kskByKey {
|
||||
covered := false
|
||||
for _, d := range parentDS {
|
||||
expected := ksk.ToDS(d.DigestType)
|
||||
if expected != nil && dsEqual(expected, d) {
|
||||
covered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
st := sdk.CheckState{
|
||||
Code: "delegation_dnskey_ksk_uncovered",
|
||||
Subject: fmt.Sprintf("keytag=%d@%s", ksk.KeyTag(), c.NSName),
|
||||
}
|
||||
if covered {
|
||||
st.Status = sdk.StatusOK
|
||||
st.Message = "KSK has a matching DS record at the parent"
|
||||
} else {
|
||||
st.Status = sdk.StatusCrit
|
||||
st.Message = "KSK has no matching DS record at the parent"
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusUnknown,
|
||||
Code: "delegation_dnskey_ksk_uncovered",
|
||||
Message: "no KSK observed at any child server",
|
||||
}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type nsHasAuthoritativeAnswerRule struct{}
|
||||
|
||||
func (r *nsHasAuthoritativeAnswerRule) Name() string {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
|
|
@ -437,3 +438,723 @@ func TestLoadDataPropagatesError(t *testing.T) {
|
|||
t.Fatalf("want single Error state, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- parentNSQueryRule -------------------------
|
||||
|
||||
func TestParentNSQueryRule(t *testing.T) {
|
||||
r := &parentNSQueryRule{}
|
||||
|
||||
t.Run("no parent views → Unknown", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("UDP error → Crit", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("empty NS RRset → Crit", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", NS: []string{}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("OK when NS records returned", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", NS: []string{"ns1.example.com."}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("per-server subjects", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{
|
||||
{Server: "p1:53", NS: []string{"ns1.example.com."}},
|
||||
{Server: "p2:53", UDPNSError: "refused"},
|
||||
},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 2 {
|
||||
t.Fatalf("want 2 states, got %d", len(states))
|
||||
}
|
||||
idx := statusByCode(t, states)
|
||||
if idx["delegation_parent_query_failed|p1:53"].Status != sdk.StatusOK {
|
||||
t.Errorf("p1: want OK")
|
||||
}
|
||||
if idx["delegation_parent_query_failed|p2:53"].Status != sdk.StatusCrit {
|
||||
t.Errorf("p2: want Crit")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- parentTCPRule -------------------------
|
||||
|
||||
func TestParentTCPRule(t *testing.T) {
|
||||
r := &parentTCPRule{}
|
||||
|
||||
t.Run("no parent views → Unknown", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("TCP error with requireTCP=true → Crit", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", TCPNSError: "refused"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("TCP error with requireTCP=false → Warn", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", TCPNSError: "refused"}},
|
||||
}
|
||||
states := evalRule(t, r, data, sdk.CheckerOptions{"requireTCP": false})
|
||||
if states[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("want Warn, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("no TCP error → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dsQueryRule -------------------------
|
||||
|
||||
func TestDSQueryRule(t *testing.T) {
|
||||
r := &dsQueryRule{}
|
||||
|
||||
t.Run("no parent views → Unknown", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS query error → Warn", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DSQueryError: "timeout"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("want Warn, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS query OK with records", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS query OK with empty answer", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK for NOERROR+empty, got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dsMatchesDeclaredRule -------------------------
|
||||
|
||||
func TestDSMatchesDeclaredRule(t *testing.T) {
|
||||
r := &dsMatchesDeclaredRule{}
|
||||
ds1 := DSRecord{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}
|
||||
ds2 := DSRecord{KeyTag: 2, Algorithm: 8, DigestType: 2, Digest: "BBBB"}
|
||||
|
||||
t.Run("no DS data to compare → Info", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS RRset matches declared → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DeclaredDS: []DSRecord{ds1},
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS missing from parent → Crit when declared non-empty", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DeclaredDS: []DSRecord{ds1, ds2},
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("extra DS at parent with no declared → Warn", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DeclaredDS: []DSRecord{},
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{ds1}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("want Warn, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS query error → view skipped", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DeclaredDS: []DSRecord{ds1},
|
||||
ParentViews: []ParentView{
|
||||
{Server: "p1:53", DSQueryError: "timeout"},
|
||||
{Server: "p2:53", DS: []DSRecord{ds1}},
|
||||
},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
idx := statusByCode(t, states)
|
||||
if _, ok := idx["delegation_ds_mismatch|p1:53"]; ok {
|
||||
t.Error("p1 with DS query error must be skipped")
|
||||
}
|
||||
if idx["delegation_ds_mismatch|p2:53"].Status != sdk.StatusOK {
|
||||
t.Error("p2: want OK")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dsPresentAtParentRule -------------------------
|
||||
|
||||
func TestDSPresentAtParentRule(t *testing.T) {
|
||||
r := &dsPresentAtParentRule{}
|
||||
|
||||
t.Run("no declared DS → Info", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("declared DS and parent serves DS → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DeclaredDS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}},
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1, Algorithm: 8, DigestType: 2, Digest: "AAAA"}}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dsRRSIGValidityRule -------------------------
|
||||
|
||||
func nowUint32() uint32 { return uint32(time.Now().UTC().Unix()) }
|
||||
|
||||
func TestDSRRSIGValidityRule(t *testing.T) {
|
||||
r := &dsRRSIGValidityRule{}
|
||||
|
||||
t.Run("no DS RRSIGs → Info", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DS query error → view skipped", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DSQueryError: "timeout", DSRRSIGs: []DSRRSIGObservation{{
|
||||
Inception: nowUint32() - 3600,
|
||||
Expiration: nowUint32() + 86400,
|
||||
}}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 && states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("view with DS query error must be skipped, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("valid RRSIG → OK", func(t *testing.T) {
|
||||
now := nowUint32()
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
DSRRSIGs: []DSRRSIGObservation{{
|
||||
Inception: now - 3600,
|
||||
Expiration: now + 86400,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("expired RRSIG → Crit", func(t *testing.T) {
|
||||
now := nowUint32()
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
DSRRSIGs: []DSRRSIGObservation{{
|
||||
Inception: now - 172800,
|
||||
Expiration: now - 3600,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit for expired, got %+v", states)
|
||||
}
|
||||
if !strings.Contains(states[0].Message, "expired") {
|
||||
t.Errorf("message should mention 'expired', got %q", states[0].Message)
|
||||
}
|
||||
})
|
||||
t.Run("not-yet-valid RRSIG → Crit", func(t *testing.T) {
|
||||
now := nowUint32()
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
DSRRSIGs: []DSRRSIGObservation{{
|
||||
Inception: now + 3600,
|
||||
Expiration: now + 172800,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit for not-yet-valid, got %+v", states)
|
||||
}
|
||||
if !strings.Contains(states[0].Message, "not yet valid") {
|
||||
t.Errorf("message should mention 'not yet valid', got %q", states[0].Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- nsResolvableRule -------------------------
|
||||
|
||||
func TestNSResolvableRule(t *testing.T) {
|
||||
r := &nsResolvableRule{}
|
||||
|
||||
t.Run("no out-of-bailiwick NS → Info", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.", // in-bailiwick, must be skipped
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("resolve error → Crit", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.elsewhere.net.",
|
||||
ResolveError: "NXDOMAIN",
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("resolved → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.elsewhere.net.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("in-bailiwick skipped even if resolve error set", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
ResolveError: "NXDOMAIN",
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
// In-bailiwick NS must be ignored; only Info state for "no out-of-bailiwick NS".
|
||||
if states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info (in-bailiwick skipped), got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- childReachableRule -------------------------
|
||||
|
||||
func TestChildReachableRule(t *testing.T) {
|
||||
r := &childReachableRule{}
|
||||
|
||||
t.Run("no child addresses → Unknown", func(t *testing.T) {
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("UDP error → Crit", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", UDPError: "timeout"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusCrit {
|
||||
t.Fatalf("want Crit, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("reachable → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", Authoritative: true}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- childNSMatchesParentRule -------------------------
|
||||
|
||||
func TestChildNSMatchesParentRule(t *testing.T) {
|
||||
r := &childNSMatchesParentRule{}
|
||||
|
||||
t.Run("no primary parent view → Unknown", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("child NS matches parent → OK", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.example.com.", "ns2.example.com."},
|
||||
}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{
|
||||
Address: "192.0.2.1",
|
||||
ChildNS: []string{"ns1.example.com.", "ns2.example.com."},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("child NS drift → Warn", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.example.com.", "ns2.example.com."},
|
||||
}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{
|
||||
Address: "192.0.2.1",
|
||||
ChildNS: []string{"ns1.example.com.", "ns3.example.com."},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("want Warn, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("UDP error or NS query error → view skipped", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.example.com."},
|
||||
}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{
|
||||
{Address: "192.0.2.1", UDPError: "timeout"},
|
||||
{Address: "192.0.2.2", ChildNSError: "refused"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
// Both addresses must be skipped → Unknown
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown (all addresses skipped), got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dnskeyQueryRule -------------------------
|
||||
|
||||
func TestDNSKEYQueryRule(t *testing.T) {
|
||||
r := &dnskeyQueryRule{}
|
||||
|
||||
t.Run("no parent DS → Unknown", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53"}},
|
||||
Children: []ChildNSView{{NSName: "ns1.example.com.", Addresses: []ChildAddressView{{Address: "192.0.2.1"}}}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when no parent DS, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DNSKEY query error → Warn", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYError: "timeout"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusWarn {
|
||||
t.Fatalf("want Warn, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("DNSKEY query succeeds → OK", func(t *testing.T) {
|
||||
key := &dns.DNSKEY{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeDNSKEY, Class: dns.ClassINET},
|
||||
Flags: 257, Protocol: 3, Algorithm: dns.RSASHA256,
|
||||
PublicKey: "AwEAAcMnWBKLuvG/LwnPVykcmpvnntwxfshHlHRhlY0F3oz8AMcuF8gw2Ge56vG9oqVxTzHl4Ss2dEqCQOjFlOVo+pa3JwIO1lUzbQ==",
|
||||
}
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYs: []DNSKEYRecord{NewDNSKEYRecord(key)}}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK, got %+v", states)
|
||||
}
|
||||
})
|
||||
t.Run("UDP error → address skipped, Unknown if no usable address", func(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", UDPError: "timeout"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown (UDP error skips address), got %+v", states)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------- dnskeyMatchesDSRule edge cases -------------------------
|
||||
|
||||
func TestDNSKEYMatchesDSRule_NoDSAtParent(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53"}}, // no DS
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, (&dnskeyMatchesDSRule{}), data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when parent has no DS, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSKEYMatchesDSRule_NoKeysObserved(t *testing.T) {
|
||||
data := &DelegationData{
|
||||
ParentViews: []ParentView{{Server: "p:53", DS: []DSRecord{{KeyTag: 1}}}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.example.com.",
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", DNSKEYError: "timeout"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, (&dnskeyMatchesDSRule{}), data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when no DNSKEY observed, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- inBailiwickGlueRule missing-glue branch -------------------------
|
||||
|
||||
func TestInBailiwickGlueRule_MissingGlue(t *testing.T) {
|
||||
r := &inBailiwickGlueRule{}
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.example.com."},
|
||||
Glue: map[string][]string{}, // no glue for in-bailiwick NS
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
var sawCrit bool
|
||||
for _, s := range states {
|
||||
if s.Status == sdk.StatusCrit {
|
||||
sawCrit = true
|
||||
}
|
||||
}
|
||||
if !sawCrit {
|
||||
t.Error("want Crit when in-bailiwick NS has no glue")
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- unnecessaryGlueRule OK/Info branches -------------------------
|
||||
|
||||
func TestUnnecessaryGlueRule_NoExtraGlue(t *testing.T) {
|
||||
r := &unnecessaryGlueRule{}
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.elsewhere.net."},
|
||||
Glue: map[string][]string{}, // out-of-bailiwick with no glue
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusOK {
|
||||
t.Fatalf("want OK for out-of-bailiwick NS without glue, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnnecessaryGlueRule_NoOutOfBailiwickNS(t *testing.T) {
|
||||
r := &unnecessaryGlueRule{}
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.example.com."},
|
||||
Glue: map[string][]string{"ns1.example.com.": {"192.0.2.1"}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if len(states) != 1 || states[0].Status != sdk.StatusInfo {
|
||||
t.Fatalf("want Info when all NS are in-bailiwick, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- childGlueMatchesParentRule edge cases -------------------------
|
||||
|
||||
func TestChildGlueMatchesParentRule_NoPrimary(t *testing.T) {
|
||||
r := &childGlueMatchesParentRule{}
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
ParentViews: []ParentView{{Server: "p:53", UDPNSError: "timeout"}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when no primary parent view, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChildGlueMatchesParentRule_OutOfBailiwickSkipped(t *testing.T) {
|
||||
r := &childGlueMatchesParentRule{}
|
||||
data := &DelegationData{
|
||||
DelegatedFQDN: "example.com.",
|
||||
ParentViews: []ParentView{{
|
||||
Server: "p:53",
|
||||
NS: []string{"ns1.elsewhere.net."},
|
||||
Glue: map[string][]string{},
|
||||
}},
|
||||
Children: []ChildNSView{{
|
||||
NSName: "ns1.elsewhere.net.", // out-of-bailiwick
|
||||
Addresses: []ChildAddressView{{Address: "192.0.2.1", ChildGlueAddrs: []string{"192.0.2.99"}}},
|
||||
}},
|
||||
}
|
||||
states := evalRule(t, r, data, nil)
|
||||
// No in-bailiwick NS → no states at all (rule stays silent).
|
||||
if len(states) != 0 {
|
||||
t.Fatalf("want no states for out-of-bailiwick NS, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- nsHasAuthoritativeAnswerRule no-children branch -------------------------
|
||||
|
||||
func TestNSHasAuthoritativeAnswerRule_NoChildren(t *testing.T) {
|
||||
r := &nsHasAuthoritativeAnswerRule{}
|
||||
states := evalRule(t, r, &DelegationData{}, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when no children, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSHasAuthoritativeAnswerRule_NoAddresses(t *testing.T) {
|
||||
r := &nsHasAuthoritativeAnswerRule{}
|
||||
data := &DelegationData{
|
||||
Children: []ChildNSView{{NSName: "ns1.example.com.", Addresses: nil}},
|
||||
}
|
||||
// NS with no addresses is skipped by the rule.
|
||||
states := evalRule(t, r, data, nil)
|
||||
if states[0].Status != sdk.StatusUnknown {
|
||||
t.Fatalf("want Unknown when NS has no addresses, got %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------- primaryParentView -------------------------
|
||||
|
||||
func TestPrimaryParentView(t *testing.T) {
|
||||
t.Run("nil when all views failed", func(t *testing.T) {
|
||||
views := []ParentView{
|
||||
{Server: "p1:53", UDPNSError: "timeout"},
|
||||
{Server: "p2:53", UDPNSError: "refused"},
|
||||
}
|
||||
if primaryParentView(views) != nil {
|
||||
t.Error("want nil")
|
||||
}
|
||||
})
|
||||
t.Run("nil when NS RRsets are empty", func(t *testing.T) {
|
||||
views := []ParentView{{Server: "p:53", NS: []string{}}}
|
||||
if primaryParentView(views) != nil {
|
||||
t.Error("want nil when NS slice is empty")
|
||||
}
|
||||
})
|
||||
t.Run("returns first successful view", func(t *testing.T) {
|
||||
views := []ParentView{
|
||||
{Server: "p1:53", UDPNSError: "timeout"},
|
||||
{Server: "p2:53", NS: []string{"ns1.example.com."}},
|
||||
{Server: "p3:53", NS: []string{"ns2.example.com."}},
|
||||
}
|
||||
got := primaryParentView(views)
|
||||
if got == nil || got.Server != "p2:53" {
|
||||
t.Errorf("want p2:53, got %v", got)
|
||||
}
|
||||
})
|
||||
t.Run("nil for empty slice", func(t *testing.T) {
|
||||
if primaryParentView(nil) != nil {
|
||||
t.Error("want nil for empty slice")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ type ParentView struct {
|
|||
UDPNSError string `json:"udp_ns_error,omitempty"`
|
||||
TCPNSError string `json:"tcp_ns_error,omitempty"`
|
||||
NS []string `json:"ns,omitempty"`
|
||||
NSTTLKnown bool `json:"ns_ttl_known,omitempty"`
|
||||
NSTTL uint32 `json:"ns_ttl,omitempty"`
|
||||
Glue map[string][]string `json:"glue,omitempty"`
|
||||
DSQueryError string `json:"ds_query_error,omitempty"`
|
||||
DS []DSRecord `json:"ds,omitempty"`
|
||||
|
|
@ -40,6 +42,7 @@ type ParentView struct {
|
|||
|
||||
type ChildNSView struct {
|
||||
NSName string `json:"ns_name"`
|
||||
CNAMETarget string `json:"cname_target,omitempty"`
|
||||
ResolveError string `json:"resolve_error,omitempty"`
|
||||
Addresses []ChildAddressView `json:"addresses,omitempty"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue