Compare commits
No commits in common. "master" and "v0.2.0" have entirely different histories.
8 changed files with 59 additions and 139 deletions
|
|
@ -49,7 +49,6 @@ func (p *aliasProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (a
|
||||||
}
|
}
|
||||||
|
|
||||||
observeCoexistence(ctx, data, servers, owner)
|
observeCoexistence(ctx, data, servers, owner)
|
||||||
observeDNAMECoexistence(ctx, data, servers)
|
|
||||||
observeDNSSEC(ctx, data, servers, apex, owner)
|
observeDNSSEC(ctx, data, servers, apex, owner)
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
|
|
@ -401,21 +400,29 @@ func observeApex(ctx context.Context, data *AliasData, servers []string, apex st
|
||||||
|
|
||||||
if (hasA || hasAAAA) && !data.ApexHasCNAME {
|
if (hasA || hasAAAA) && !data.ApexHasCNAME {
|
||||||
data.ApexFlattening = true
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// querySiblings returns RRsets of common types that sit alongside a CNAME or DNAME at owner.
|
func observeCoexistence(ctx context.Context, data *AliasData, servers []string, owner string) {
|
||||||
// Filter on owner+type: a DNAME-synthesized CNAME would otherwise count as a sibling.
|
if !data.OwnerHasCNAME {
|
||||||
func querySiblings(ctx context.Context, servers []string, owner string) []CoexistingRRset {
|
return
|
||||||
candidates := []uint16{
|
}
|
||||||
|
|
||||||
|
siblings := []uint16{
|
||||||
dns.TypeA, dns.TypeAAAA, dns.TypeMX, dns.TypeTXT,
|
dns.TypeA, dns.TypeAAAA, dns.TypeMX, dns.TypeTXT,
|
||||||
dns.TypeNS, dns.TypeSRV, dns.TypeCAA,
|
dns.TypeNS, dns.TypeSRV, dns.TypeCAA,
|
||||||
}
|
}
|
||||||
seen := map[string]uint32{}
|
seen := map[string]uint32{}
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(candidates))
|
wg.Add(len(siblings))
|
||||||
for _, qt := range candidates {
|
for _, qt := range siblings {
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
q := dns.Question{Name: owner, Qtype: qt, Qclass: dns.ClassINET}
|
q := dns.Question{Name: owner, Qtype: qt, Qclass: dns.ClassINET}
|
||||||
|
|
@ -423,6 +430,8 @@ func querySiblings(ctx context.Context, servers []string, owner string) []Coexis
|
||||||
if err != nil || r == nil {
|
if err != nil || r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Filter on owner+type because a DNAME-synthesized CNAME would
|
||||||
|
// otherwise count as a sibling of every queried type.
|
||||||
for _, rr := range r.Answer {
|
for _, rr := range r.Answer {
|
||||||
if rr.Header().Rrtype != qt {
|
if rr.Header().Rrtype != qt {
|
||||||
continue
|
continue
|
||||||
|
|
@ -438,42 +447,9 @@ func querySiblings(ctx context.Context, servers []string, owner string) []Coexis
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
var out []CoexistingRRset
|
|
||||||
for t, ttl := range seen {
|
for t, ttl := range seen {
|
||||||
out = append(out, CoexistingRRset{Type: t, TTL: ttl})
|
data.Coexisting = append(data.Coexisting, CoexistingRRset{Type: t, TTL: ttl})
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func observeCoexistence(ctx context.Context, data *AliasData, servers []string, owner string) {
|
|
||||||
if !data.OwnerHasCNAME {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.Coexisting = querySiblings(ctx, servers, owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
func observeDNAMECoexistence(ctx context.Context, data *AliasData, servers []string) {
|
|
||||||
if len(data.DNAMESubstitutions) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results := make(map[string][]CoexistingRRset, len(data.DNAMESubstitutions))
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(len(data.DNAMESubstitutions))
|
|
||||||
for _, hop := range data.DNAMESubstitutions {
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
siblings := querySiblings(ctx, servers, hop.Owner)
|
|
||||||
if len(siblings) > 0 {
|
|
||||||
mu.Lock()
|
|
||||||
results[hop.Owner] = siblings
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
if len(results) > 0 {
|
|
||||||
data.DNAMECoexistence = results
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ func Definition() *sdk.CheckerDefinition {
|
||||||
cnameAtApexRule{},
|
cnameAtApexRule{},
|
||||||
apexFlatteningRule{},
|
apexFlatteningRule{},
|
||||||
cnameCoexistenceRule{},
|
cnameCoexistenceRule{},
|
||||||
dnameCoexistenceRule{},
|
|
||||||
cnameDnssecRule{},
|
cnameDnssecRule{},
|
||||||
targetResolvableRule{},
|
targetResolvableRule{},
|
||||||
multipleRecordsRule{},
|
multipleRecordsRule{},
|
||||||
|
|
|
||||||
|
|
@ -104,15 +104,7 @@ func buildReportView(data *AliasData, states []sdk.CheckState) *reportView {
|
||||||
v.FinalAddresses = append(v.FinalAddresses, data.FinalA...)
|
v.FinalAddresses = append(v.FinalAddresses, data.FinalA...)
|
||||||
v.FinalAddresses = append(v.FinalAddresses, data.FinalAAAA...)
|
v.FinalAddresses = append(v.FinalAddresses, data.FinalAAAA...)
|
||||||
|
|
||||||
chain := data.Chain
|
for i, h := range data.Chain {
|
||||||
if data.ApexFlattening {
|
|
||||||
chain = append(chain, ChainHop{
|
|
||||||
Owner: data.Apex,
|
|
||||||
Kind: KindALIAS,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, h := range chain {
|
|
||||||
step := chainStep{
|
step := chainStep{
|
||||||
Index: i + 1,
|
Index: i + 1,
|
||||||
Owner: h.Owner,
|
Owner: h.Owner,
|
||||||
|
|
@ -120,7 +112,7 @@ func buildReportView(data *AliasData, states []sdk.CheckState) *reportView {
|
||||||
Target: h.Target,
|
Target: h.Target,
|
||||||
TTL: h.TTL,
|
TTL: h.TTL,
|
||||||
Server: h.Server,
|
Server: h.Server,
|
||||||
IsLast: i == len(chain)-1,
|
IsLast: i == len(data.Chain)-1,
|
||||||
}
|
}
|
||||||
switch h.Kind {
|
switch h.Kind {
|
||||||
case KindCNAME:
|
case KindCNAME:
|
||||||
|
|
|
||||||
|
|
@ -189,10 +189,24 @@ type targetResolvableRule struct{}
|
||||||
|
|
||||||
func (targetResolvableRule) Name() string { return "target_resolvable" }
|
func (targetResolvableRule) Name() string { return "target_resolvable" }
|
||||||
func (targetResolvableRule) Description() string {
|
func (targetResolvableRule) Description() string {
|
||||||
return "Verifies that the final target of the alias chain exists in DNS (returns NOERROR, not NXDOMAIN)."
|
return "Verifies that the final target of the alias chain publishes at least one A or AAAA record."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
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 {
|
||||||
data, errState := loadAlias(ctx, obs)
|
data, errState := loadAlias(ctx, obs)
|
||||||
if errState != nil {
|
if errState != nil {
|
||||||
return errState
|
return errState
|
||||||
|
|
@ -203,14 +217,22 @@ func (targetResolvableRule) Evaluate(ctx context.Context, obs sdk.ObservationGet
|
||||||
if data.ChainTerminated.Reason != TermOK {
|
if data.ChainTerminated.Reason != TermOK {
|
||||||
return skipped("chain did not terminate normally")
|
return skipped("chain did not terminate normally")
|
||||||
}
|
}
|
||||||
if data.FinalRcode != "NXDOMAIN" {
|
if len(data.FinalA) > 0 || len(data.FinalAAAA) > 0 {
|
||||||
return okState(data.FinalTarget, fmt.Sprintf("target %s exists in DNS", data.FinalTarget))
|
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"
|
||||||
}
|
}
|
||||||
return []sdk.CheckState{withHint(sdk.CheckState{
|
return []sdk.CheckState{withHint(sdk.CheckState{
|
||||||
Status: sdk.StatusCrit,
|
Status: status,
|
||||||
Subject: data.FinalTarget,
|
Subject: data.FinalTarget,
|
||||||
Message: fmt.Sprintf("final target %s does not exist (NXDOMAIN)", data.FinalTarget),
|
Message: fmt.Sprintf("final target %s does not resolve to an address (%s)", data.FinalTarget, rcode),
|
||||||
}, "The alias points at a name that does not exist; create the missing record or update the alias target.")}
|
}, "Point the alias at a name that publishes at least one A or AAAA record, or fix the upstream zone.")}
|
||||||
}
|
}
|
||||||
|
|
||||||
type multipleRecordsRule struct{}
|
type multipleRecordsRule struct{}
|
||||||
|
|
|
||||||
|
|
@ -7,41 +7,6 @@ import (
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dnameCoexistenceRule struct{}
|
|
||||||
|
|
||||||
func (dnameCoexistenceRule) Name() string { return "dname_coexistence" }
|
|
||||||
func (dnameCoexistenceRule) Description() string {
|
|
||||||
return "Flags RRsets that sit at the same owner as a DNAME (RFC 6672 §2.3)."
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dnameCoexistenceRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
||||||
data, errState := loadAlias(ctx, obs)
|
|
||||||
if errState != nil {
|
|
||||||
return errState
|
|
||||||
}
|
|
||||||
if !apexKnown(data) {
|
|
||||||
return skipped("apex lookup failed")
|
|
||||||
}
|
|
||||||
if len(data.DNAMESubstitutions) == 0 {
|
|
||||||
return skipped("no DNAME in chain")
|
|
||||||
}
|
|
||||||
if len(data.DNAMECoexistence) == 0 {
|
|
||||||
return okState(data.Owner, "all DNAME nodes have no sibling records")
|
|
||||||
}
|
|
||||||
var out []sdk.CheckState
|
|
||||||
for owner, coexisting := range data.DNAMECoexistence {
|
|
||||||
for _, rr := range coexisting {
|
|
||||||
out = append(out, withHint(sdk.CheckState{
|
|
||||||
Status: sdk.StatusCrit,
|
|
||||||
Subject: owner,
|
|
||||||
Message: fmt.Sprintf("%s and DNAME both exist at %s (RFC 6672 §2.3)", rr.Type, owner),
|
|
||||||
Code: rr.Type,
|
|
||||||
}, "Remove the sibling record or move it under a different label; a DNAME owner must not carry other data."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type cnameCoexistenceRule struct{}
|
type cnameCoexistenceRule struct{}
|
||||||
|
|
||||||
func (cnameCoexistenceRule) Name() string { return "cname_coexistence" }
|
func (cnameCoexistenceRule) Name() string { return "cname_coexistence" }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
const (
|
const (
|
||||||
defaultMaxChainLength = 8
|
defaultMaxChainLength = 8
|
||||||
defaultMinTargetTTL = 60
|
defaultMinTargetTTL = 60
|
||||||
|
defaultRequireResolvableTarget = true
|
||||||
defaultAllowApexCNAME = false
|
defaultAllowApexCNAME = false
|
||||||
defaultRecognizeApexFlattening = true
|
defaultRecognizeApexFlattening = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -266,38 +266,6 @@ func TestCnameCoexistenceRule(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDnameCoexistenceRule(t *testing.T) {
|
|
||||||
t.Run("skip when no DNAME in chain", func(t *testing.T) {
|
|
||||||
d := apexKnownData()
|
|
||||||
assertSkipped(t, run(dnameCoexistenceRule{}, d, nil), "no DNAME in chain")
|
|
||||||
})
|
|
||||||
t.Run("ok when DNAME has no siblings", func(t *testing.T) {
|
|
||||||
d := apexKnownData()
|
|
||||||
d.DNAMESubstitutions = []ChainHop{{Owner: "old.example.com.", Kind: KindDNAME, Target: "new.example.com."}}
|
|
||||||
assertSingle(t, run(dnameCoexistenceRule{}, d, nil), sdk.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("crit when DNAME has siblings", func(t *testing.T) {
|
|
||||||
d := apexKnownData()
|
|
||||||
d.DNAMESubstitutions = []ChainHop{{Owner: "old.example.com.", Kind: KindDNAME, Target: "new.example.com."}}
|
|
||||||
d.DNAMECoexistence = map[string][]CoexistingRRset{
|
|
||||||
"old.example.com.": {{Type: "MX"}, {Type: "A"}},
|
|
||||||
}
|
|
||||||
states := run(dnameCoexistenceRule{}, d, nil)
|
|
||||||
if len(states) != 2 {
|
|
||||||
t.Fatalf("want 2 states, got %d: %+v", len(states), states)
|
|
||||||
}
|
|
||||||
for _, s := range states {
|
|
||||||
if s.Status != sdk.StatusCrit {
|
|
||||||
t.Fatalf("want CRIT, got %v", s.Status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("skip when apex unknown", func(t *testing.T) {
|
|
||||||
d := &AliasData{Owner: "x.", ApexLookupError: "boom"}
|
|
||||||
assertSkipped(t, run(dnameCoexistenceRule{}, d, nil), "apex")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCnameDnssecRule(t *testing.T) {
|
func TestCnameDnssecRule(t *testing.T) {
|
||||||
t.Run("skip unsigned zone", func(t *testing.T) {
|
t.Run("skip unsigned zone", func(t *testing.T) {
|
||||||
d := apexKnownData()
|
d := apexKnownData()
|
||||||
|
|
@ -322,26 +290,25 @@ func TestCnameDnssecRule(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTargetResolvableRule(t *testing.T) {
|
func TestTargetResolvableRule(t *testing.T) {
|
||||||
t.Run("ok when NOERROR with A record", func(t *testing.T) {
|
t.Run("ok", func(t *testing.T) {
|
||||||
d := apexKnownData()
|
d := apexKnownData()
|
||||||
d.ChainTerminated.Reason = TermOK
|
d.ChainTerminated.Reason = TermOK
|
||||||
d.FinalTarget = "target."
|
d.FinalTarget = "target."
|
||||||
d.FinalA = []string{"1.2.3.4"}
|
d.FinalA = []string{"1.2.3.4"}
|
||||||
assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusOK)
|
assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusOK)
|
||||||
})
|
})
|
||||||
t.Run("ok when NOERROR with no A/AAAA (e.g. service label)", func(t *testing.T) {
|
t.Run("crit by default", func(t *testing.T) {
|
||||||
d := apexKnownData()
|
|
||||||
d.ChainTerminated.Reason = TermOK
|
|
||||||
d.FinalTarget = "_2772._tcp.znc.example."
|
|
||||||
assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusOK)
|
|
||||||
})
|
|
||||||
t.Run("crit when NXDOMAIN", func(t *testing.T) {
|
|
||||||
d := apexKnownData()
|
d := apexKnownData()
|
||||||
d.ChainTerminated.Reason = TermOK
|
d.ChainTerminated.Reason = TermOK
|
||||||
d.FinalTarget = "target."
|
d.FinalTarget = "target."
|
||||||
d.FinalRcode = "NXDOMAIN"
|
|
||||||
assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusCrit)
|
assertSingle(t, run(targetResolvableRule{}, d, nil), sdk.StatusCrit)
|
||||||
})
|
})
|
||||||
|
t.Run("warn when requireResolvableTarget=false", func(t *testing.T) {
|
||||||
|
d := apexKnownData()
|
||||||
|
d.ChainTerminated.Reason = TermOK
|
||||||
|
d.FinalTarget = "target."
|
||||||
|
assertSingle(t, run(targetResolvableRule{}, d, sdk.CheckerOptions{"requireResolvableTarget": false}), sdk.StatusWarn)
|
||||||
|
})
|
||||||
t.Run("skip when chain did not terminate normally", func(t *testing.T) {
|
t.Run("skip when chain did not terminate normally", func(t *testing.T) {
|
||||||
d := apexKnownData()
|
d := apexKnownData()
|
||||||
d.ChainTerminated.Reason = TermLoop
|
d.ChainTerminated.Reason = TermLoop
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,6 @@ type AliasData struct {
|
||||||
|
|
||||||
// Coexisting is populated only when Owner has a CNAME.
|
// Coexisting is populated only when Owner has a CNAME.
|
||||||
Coexisting []CoexistingRRset `json:"coexisting,omitempty"`
|
Coexisting []CoexistingRRset `json:"coexisting,omitempty"`
|
||||||
// DNAMECoexistence maps each DNAME owner (from DNAMESubstitutions) to its sibling RRsets.
|
|
||||||
DNAMECoexistence map[string][]CoexistingRRset `json:"dname_coexistence,omitempty"`
|
|
||||||
|
|
||||||
OwnerIsApex bool `json:"owner_is_apex,omitempty"`
|
OwnerIsApex bool `json:"owner_is_apex,omitempty"`
|
||||||
OwnerHasCNAME bool `json:"owner_has_cname,omitempty"`
|
OwnerHasCNAME bool `json:"owner_has_cname,omitempty"`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue