diff --git a/checker/collect.go b/checker/collect.go index 01077dd..f4fda00 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -58,14 +58,12 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption for _, ps := range parentServers { view := ParentView{Server: ps} - ns, glue, nsTTL, nsTTLKnown, qerr := queryDelegation(ctx, ps, delegatedFQDN) + ns, glue, _, 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 { @@ -91,7 +89,13 @@ 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. - primary := primaryParentView(data.ParentViews) + var primary *ParentView + for i := range data.ParentViews { + if data.ParentViews[i].UDPNSError == "" && len(data.ParentViews[i].NS) > 0 { + primary = &data.ParentViews[i] + break + } + } if primary == nil { return data, nil } @@ -99,11 +103,6 @@ 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. @@ -149,12 +148,22 @@ func (p *delegationProvider) Collect(ctx context.Context, opts sdk.CheckerOption av.ChildGlueAddrs = addrsAt } - 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)) + // 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)) + } } } diff --git a/checker/dns.go b/checker/dns.go index 4a2983c..a5a45a3 100644 --- a/checker/dns.go +++ b/checker/dns.go @@ -108,16 +108,15 @@ 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. -// 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) { +func queryDelegation(ctx context.Context, parentServer, fqdn string) (ns []string, glue map[string][]string, msg *dns.Msg, err error) { q := dns.Question{Name: dns.Fqdn(fqdn), Qtype: dns.TypeNS, Qclass: dns.ClassINET} - msg, merr := dnsExchange(ctx, "", parentServer, q, true) - if merr != nil { - return nil, nil, 0, false, merr + msg, err = dnsExchange(ctx, "", parentServer, q, true) + if err != nil { + return nil, nil, nil, err } if msg.Rcode != dns.RcodeSuccess { - return nil, nil, 0, false, fmt.Errorf("parent answered %s", dns.RcodeToString[msg.Rcode]) + return nil, nil, msg, fmt.Errorf("parent answered %s", dns.RcodeToString[msg.Rcode]) } glue = map[string][]string{} @@ -128,10 +127,6 @@ 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)) @@ -148,20 +143,6 @@ 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} diff --git a/checker/rule.go b/checker/rule.go index cc5cbc7..42f29c2 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -16,22 +16,17 @@ 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{}, @@ -40,9 +35,6 @@ func Rules() []sdk.CheckRule { &childGlueMatchesParentRule{}, &dnskeyQueryRule{}, &dnskeyMatchesDSRule{}, - &dnskeyKSKPresentRule{}, - &dnskeyProtocolRule{}, - &dsCoversAllKSKsRule{}, &nsHasAuthoritativeAnswerRule{}, } } @@ -534,56 +526,6 @@ 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{} @@ -622,44 +564,6 @@ 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" } @@ -1042,306 +946,6 @@ 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 { diff --git a/checker/rule_test.go b/checker/rule_test.go index aee9baf..86327dc 100644 --- a/checker/rule_test.go +++ b/checker/rule_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "strings" "testing" - "time" "github.com/miekg/dns" @@ -438,723 +437,3 @@ 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") - } - }) -} diff --git a/checker/types.go b/checker/types.go index a10e7de..9037cce 100644 --- a/checker/types.go +++ b/checker/types.go @@ -32,8 +32,6 @@ 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"` @@ -42,7 +40,6 @@ 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"` }