// 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" "net/netip" "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"` // Acknowledged is set when an exposure finding was accepted on // purpose (it is then info, not a warning). Acknowledged *model.Acknowledgement `json:"acknowledged,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 // LocalAddrs are this host's addresses: a server without views is // public when named listens on one that isn't private // (docs/exposure.md). LocalAddrs []netip.Addr } // 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 // served records (not disabled) allRecords 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.ServedRecordsByZone(), allRecords: s.RecordsByZone(), keyNames: map[string]bool{}} for _, k := range s.Keys { c.keyNames[k.Name] = true } // The views' own keys (render/viewkeys.go) may be referred to too. for _, v := range s.Views { c.keyNames[model.NormalizeKeyName(model.ViewKeyName(v.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() c.checkExposure() c.checkDNSSEC() c.checkBlocking() c.checkLinked() c.checkTLSFiles() c.applyAcks() 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.NameLess(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, ".") }