From 52a3e56c4f69d44b0b77c2de2597693eaf26b71d Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 15 May 2026 17:30:15 +0800 Subject: [PATCH 1/2] checker: rework target_resolvable to check existence (NOERROR) instead of A/AAAA --- checker/rules_chain.go | 36 +++++++----------------------------- checker/rules_common.go | 7 +++---- checker/rules_test.go | 13 +++++++------ 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/checker/rules_chain.go b/checker/rules_chain.go index 12e91b6..e3da350 100644 --- a/checker/rules_chain.go +++ b/checker/rules_chain.go @@ -189,24 +189,10 @@ type targetResolvableRule struct{} func (targetResolvableRule) Name() string { return "target_resolvable" } func (targetResolvableRule) Description() string { - return "Verifies that the final target of the alias chain publishes at least one A or AAAA record." + return "Verifies that the final target of the alias chain exists in DNS (returns NOERROR, not NXDOMAIN)." } -func (targetResolvableRule) Options() sdk.CheckerOptionsDocumentation { - return sdk.CheckerOptionsDocumentation{ - UserOpts: []sdk.CheckerOptionDocumentation{ - { - Id: "requireResolvableTarget", - Type: "bool", - Label: "Require resolvable target", - Description: "When enabled, a chain whose final target returns no A/AAAA is reported as critical (otherwise a warning).", - Default: defaultRequireResolvableTarget, - }, - }, - } -} - -func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { +func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errState := loadAlias(ctx, obs) if errState != nil { return errState @@ -217,22 +203,14 @@ func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGet if data.ChainTerminated.Reason != TermOK { return skipped("chain did not terminate normally") } - if len(data.FinalA) > 0 || len(data.FinalAAAA) > 0 { - return okState(data.FinalTarget, fmt.Sprintf("target %s resolves to %d address(es)", data.FinalTarget, len(data.FinalA)+len(data.FinalAAAA))) - } - status := sdk.StatusWarn - if sdk.GetBoolOption(opts, "requireResolvableTarget", defaultRequireResolvableTarget) { - status = sdk.StatusCrit - } - rcode := data.FinalRcode - if rcode == "" { - rcode = "no A/AAAA" + if data.FinalRcode != "NXDOMAIN" { + return okState(data.FinalTarget, fmt.Sprintf("target %s exists in DNS", data.FinalTarget)) } return []sdk.CheckState{withHint(sdk.CheckState{ - Status: status, + Status: sdk.StatusCrit, Subject: data.FinalTarget, - Message: fmt.Sprintf("final target %s does not resolve to an address (%s)", data.FinalTarget, rcode), - }, "Point the alias at a name that publishes at least one A or AAAA record, or fix the upstream zone.")} + Message: fmt.Sprintf("final target %s does not exist (NXDOMAIN)", data.FinalTarget), + }, "The alias points at a name that does not exist; create the missing record or update the alias target.")} } type multipleRecordsRule struct{} diff --git a/checker/rules_common.go b/checker/rules_common.go index b267fba..345cfc7 100644 --- a/checker/rules_common.go +++ b/checker/rules_common.go @@ -9,10 +9,9 @@ import ( // Defaults are centralised so Definition's docs and runtime reads cannot drift. const ( - defaultMaxChainLength = 8 - defaultMinTargetTTL = 60 - defaultRequireResolvableTarget = true - defaultAllowApexCNAME = false + defaultMaxChainLength = 8 + defaultMinTargetTTL = 60 + defaultAllowApexCNAME = false defaultRecognizeApexFlattening = true // hintKey is the CheckState.Meta key the HTML report reads to render the diff --git a/checker/rules_test.go b/checker/rules_test.go index 95a1c28..2c8452f 100644 --- a/checker/rules_test.go +++ b/checker/rules_test.go @@ -290,24 +290,25 @@ func TestCnameDnssecRule(t *testing.T) { } func TestTargetResolvableRule(t *testing.T) { - t.Run("ok", func(t *testing.T) { + t.Run("ok when NOERROR with A record", func(t *testing.T) { d := apexKnownData() d.ChainTerminated.Reason = TermOK d.FinalTarget = "target." d.FinalA = []string{"1.2.3.4"} assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusOK) }) - t.Run("crit by default", func(t *testing.T) { + t.Run("ok when NOERROR with no A/AAAA (e.g. service label)", func(t *testing.T) { d := apexKnownData() d.ChainTerminated.Reason = TermOK - d.FinalTarget = "target." - assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusCrit) + d.FinalTarget = "_2772._tcp.znc.example." + assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusOK) }) - t.Run("warn when requireResolvableTarget=false", func(t *testing.T) { + t.Run("crit when NXDOMAIN", func(t *testing.T) { d := apexKnownData() d.ChainTerminated.Reason = TermOK d.FinalTarget = "target." - assertSingle(t, run(targetResolvableRule{}, d, sdk.CheckerOptions{"requireResolvableTarget": false}), sdk.StatusWarn) + d.FinalRcode = "NXDOMAIN" + assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusCrit) }) t.Run("skip when chain did not terminate normally", func(t *testing.T) { d := apexKnownData() From 1493ef4d3f8fb2d5b535f0a0c1887dd7c7b13f31 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 15 May 2026 17:37:10 +0800 Subject: [PATCH 2/2] report: move synthetic ALIAS hop from collector to report view --- checker/collect.go | 6 ------ checker/report.go | 12 ++++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/checker/collect.go b/checker/collect.go index c30da19..5658218 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -400,12 +400,6 @@ func observeApex(ctx context.Context, data *AliasData, servers []string, apex st if (hasA || hasAAAA) && !data.ApexHasCNAME { data.ApexFlattening = true - // Synthesize a pseudo-hop so the report's chain view shows the ALIAS - // indirection that would otherwise be invisible from the wire. - data.Chain = append(data.Chain, ChainHop{ - Owner: lowerFQDN(apex), - Kind: KindALIAS, - }) } } diff --git a/checker/report.go b/checker/report.go index b8c58e7..3f98385 100644 --- a/checker/report.go +++ b/checker/report.go @@ -104,7 +104,15 @@ func buildReportView(data *AliasData, states []sdk.CheckState) *reportView { v.FinalAddresses = append(v.FinalAddresses, data.FinalA...) v.FinalAddresses = append(v.FinalAddresses, data.FinalAAAA...) - for i, h := range data.Chain { + chain := data.Chain + if data.ApexFlattening { + chain = append(chain, ChainHop{ + Owner: data.Apex, + Kind: KindALIAS, + }) + } + + for i, h := range chain { step := chainStep{ Index: i + 1, Owner: h.Owner, @@ -112,7 +120,7 @@ func buildReportView(data *AliasData, states []sdk.CheckState) *reportView { Target: h.Target, TTL: h.TTL, Server: h.Server, - IsLast: i == len(data.Chain)-1, + IsLast: i == len(chain)-1, } switch h.Kind { case KindCNAME: