The soa_warning for negative caching fired on the SOA minimum alone and blamed BIND's resolver cap (max-ncache-ttl), which doesn't apply to an authoritative zone. Resolvers cache negative answers for the smaller of the SOA minimum and the SOA record's TTL (RFC 2308 section 5), and the SOA record uses the zone's default TTL. The warning now uses that value, explains the effect, and suggests the fix command (with --view when the zone is in a view). Also: - Problem.String shows zone names without the trailing dot (jarld check). - jarl prints the warnings a change introduces when it is applied or staged, not only on dry runs. - jarl check/resync print only named-checkconf errors, not its "loaded serial" lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
5.1 KiB
Go
186 lines
5.1 KiB
Go
// Package validate checks a complete configuration State for problems
|
|
// before it is rendered and handed to BIND.
|
|
//
|
|
// Rules come from the DNS RFCs (CNAME exclusivity, glue, DNAME, ...), from
|
|
// the checks named itself performs when loading a zone (check-names,
|
|
// check-integrity, record limits), and from operational experience (open
|
|
// resolvers, open zone transfers). named-checkconf and named-checkzone are
|
|
// run as well before anything is applied; this package exists so that
|
|
// problems are found early and explained in terms of Jarl objects.
|
|
package validate
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// Severity of a problem.
|
|
type Severity string
|
|
|
|
const (
|
|
Error Severity = "error" // blocks applying
|
|
Warning Severity = "warning" // shown, does not block
|
|
Info Severity = "info"
|
|
)
|
|
|
|
// Problem is a single validation finding.
|
|
type Problem struct {
|
|
Severity Severity `json:"severity"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
ObjectType string `json:"object_type,omitempty"` // view, zone, record, acl, key, option, settings
|
|
ObjectID int64 `json:"object_id,omitempty"`
|
|
Zone string `json:"zone,omitempty"`
|
|
View string `json:"view,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
}
|
|
|
|
// Key identifies a problem independently of object IDs, so problems can be
|
|
// compared between two states.
|
|
func (p Problem) Key() string {
|
|
return string(p.Severity) + "|" + p.Code + "|" + p.View + "|" + p.Zone + "|" + p.Name + "|" + p.Message
|
|
}
|
|
|
|
func (p Problem) String() string {
|
|
loc := ""
|
|
if p.Zone != "" {
|
|
loc = "zone " + dnsname.Display(p.Zone)
|
|
if p.View != "" {
|
|
loc += " [view " + p.View + "]"
|
|
}
|
|
loc += ": "
|
|
} else if p.View != "" {
|
|
loc = "view " + p.View + ": "
|
|
}
|
|
return string(p.Severity) + ": " + loc + p.Message
|
|
}
|
|
|
|
// Options adjust validation.
|
|
type Options struct {
|
|
// ExternalKeys are TSIG key names defined outside Jarl, in files
|
|
// pulled in by top-level include statements (e.g. rndc-key).
|
|
ExternalKeys []string
|
|
// ExternalKeysUnknown is set when an included file could not be read,
|
|
// so references to unknown keys are not reported.
|
|
ExternalKeysUnknown bool
|
|
}
|
|
|
|
// Result holds the problems found.
|
|
type Result struct {
|
|
Problems []Problem `json:"problems"`
|
|
}
|
|
|
|
// HasErrors reports whether any problem is an error.
|
|
func HasErrors(ps []Problem) bool {
|
|
for _, p := range ps {
|
|
if p.Severity == Error {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Errors returns only the errors.
|
|
func Errors(ps []Problem) []Problem {
|
|
var out []Problem
|
|
for _, p := range ps {
|
|
if p.Severity == Error {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Introduced returns the problems in after that are not in before.
|
|
func Introduced(before, after []Problem) []Problem {
|
|
seen := map[string]bool{}
|
|
for _, p := range before {
|
|
seen[p.Key()] = true
|
|
}
|
|
var out []Problem
|
|
for _, p := range after {
|
|
if !seen[p.Key()] {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type checker struct {
|
|
s *model.State
|
|
opts Options
|
|
problems []Problem
|
|
// indexes
|
|
zoneRecords map[int64][]*model.Record
|
|
keyNames map[string]bool
|
|
}
|
|
|
|
// State validates the complete state.
|
|
func State(s *model.State, opts Options) []Problem {
|
|
c := &checker{s: s, opts: opts, zoneRecords: s.RecordsByZone(), keyNames: map[string]bool{}}
|
|
for _, k := range s.Keys {
|
|
c.keyNames[k.Name] = true
|
|
}
|
|
for _, k := range opts.ExternalKeys {
|
|
c.keyNames[model.NormalizeKeyName(k)] = true
|
|
}
|
|
c.checkSettings()
|
|
c.checkKeys()
|
|
c.checkACLs()
|
|
c.checkViews()
|
|
c.checkZones()
|
|
c.checkOptions()
|
|
c.checkSecurity()
|
|
sortProblems(c.problems)
|
|
return c.problems
|
|
}
|
|
|
|
func (c *checker) add(p Problem) { c.problems = append(c.problems, p) }
|
|
|
|
func (c *checker) errf(code, objType string, id int64, format string, args ...any) *Problem {
|
|
c.add(Problem{Severity: Error, Code: code, ObjectType: objType, ObjectID: id, Message: fmt.Sprintf(format, args...)})
|
|
return &c.problems[len(c.problems)-1]
|
|
}
|
|
|
|
func (c *checker) warnf(code, objType string, id int64, format string, args ...any) *Problem {
|
|
c.add(Problem{Severity: Warning, Code: code, ObjectType: objType, ObjectID: id, Message: fmt.Sprintf(format, args...)})
|
|
return &c.problems[len(c.problems)-1]
|
|
}
|
|
|
|
func (c *checker) infof(code, objType string, id int64, format string, args ...any) *Problem {
|
|
c.add(Problem{Severity: Info, Code: code, ObjectType: objType, ObjectID: id, Message: fmt.Sprintf(format, args...)})
|
|
return &c.problems[len(c.problems)-1]
|
|
}
|
|
|
|
func sortProblems(ps []Problem) {
|
|
rank := map[Severity]int{Error: 0, Warning: 1, Info: 2}
|
|
sort.SliceStable(ps, func(i, j int) bool {
|
|
a, b := ps[i], ps[j]
|
|
if rank[a.Severity] != rank[b.Severity] {
|
|
return rank[a.Severity] < rank[b.Severity]
|
|
}
|
|
if a.View != b.View {
|
|
return a.View < b.View
|
|
}
|
|
if a.Zone != b.Zone {
|
|
return a.Zone < b.Zone
|
|
}
|
|
if a.Name != b.Name {
|
|
return model.CanonicalLess(a.Name, b.Name)
|
|
}
|
|
return a.Message < b.Message
|
|
})
|
|
}
|
|
|
|
// zoneLabel returns "name [view]" for messages.
|
|
func (c *checker) zoneLabel(z *model.Zone) string {
|
|
if v := c.s.ViewName(z); v != "" {
|
|
return strings.TrimSuffix(z.Name, ".") + " [view " + v + "]"
|
|
}
|
|
return strings.TrimSuffix(z.Name, ".")
|
|
}
|