package validate import ( "regexp" "strconv" "strings" "jarl/internal/model" "jarl/internal/namedconf" ) // defaultMaxZoneTTL is max-zone-ttl of BIND's built-in policies (and of // custom ones that don't set it): named refuses to load a signed zone // with a longer TTL. const defaultMaxZoneTTL = 86400 // Policies returns the custom dnssec-policy statements defined at the top // level, by name. func Policies(s *model.State) map[string]*namedconf.Stmt { out := map[string]*namedconf.Stmt{} for _, o := range s.ScopedOptions(model.ScopeTopLevel, 0) { if strings.ToLower(o.Name) != "dnssec-policy" { continue } stmt, err := namedconf.ParseOptionValue(o.Name, o.Value) if err != nil || stmt.Arg(0) == "" { continue } out[stmt.Arg(0)] = stmt } return out } func (c *checker) checkDNSSEC() { policies := Policies(c.s) // A custom policy without keys crashes named 9.20 when it loads the // configuration (an assertion in kaspconf.c), although named-checkconf // accepts it. BIND's built-in "insecure" policy is the way to go // unsigned. for _, o := range c.s.ScopedOptions(model.ScopeTopLevel, 0) { if strings.ToLower(o.Name) != "dnssec-policy" { continue } stmt, err := namedconf.ParseOptionValue(o.Name, o.Value) if err != nil { continue // reported by checkOptions } if b := stmt.FirstBlock(); b != nil { if keys := namedconf.FindOne(b.Stmts, "keys"); keys != nil { if kb := keys.FirstBlock(); kb != nil && len(kb.Stmts) == 0 { c.errf("dnssec_policy_no_keys", "option", o.ID, "dnssec-policy %s has an empty keys list, which makes named 9.20 crash when it loads the configuration; to remove signing from a zone, use BIND's built-in insecure policy (jarl zone dnssec disable)", stmt.Arg(0)) } } } } for _, z := range c.s.SortedZones() { if z.DNSSECPolicy == "" { continue } if z.Type != model.ZonePrimary { c.zoneProblem(z, Error, "dnssec_zone_type", "only primary zones can be signed by Jarl; a %s zone gets its signatures from its primary", z.Type) continue } maxTTL := uint32(defaultMaxZoneTTL) switch z.DNSSECPolicy { case model.DNSSECDefault, model.DNSSECInsecure: default: stmt := policies[z.DNSSECPolicy] if stmt == nil { c.zoneProblem(z, Error, "dnssec_policy_unknown", "dnssec-policy %q doesn't exist: use default (BIND's built-in policy), or define it as a top-level dnssec-policy statement", z.DNSSECPolicy) continue } if b := stmt.FirstBlock(); b != nil { if m := namedconf.FindOne(b.Stmts, "max-zone-ttl"); m != nil { if v, ok := parseDuration(m.Arg(0)); ok && v > 0 { maxTTL = v } } } } if o := c.s.FindOption(model.ScopeZone, z.ID, "inline-signing"); o != nil && isNo(o.Value) { c.zoneProblem(z, Error, "dnssec_inline_signing", "a signed zone needs inline signing: named keeps its signatures in a separate copy of the zone, because Jarl rewrites the zone file (remove the inline-signing option)") } // named refuses to load a signed zone with a TTL above the // policy's max-zone-ttl, and keeps serving the old version. if z.DefaultTTL > maxTTL { c.zoneProblem(z, Error, "dnssec_ttl_too_long", "the default TTL (%d) is longer than the DNSSEC policy's max-zone-ttl (%d), so named would refuse to load the signed zone; lower it to %d or less", z.DefaultTTL, maxTTL, maxTTL) } for _, r := range c.zoneRecords[z.ID] { if r.TTL != nil && *r.TTL > maxTTL { p := c.zoneProblem(z, Error, "dnssec_ttl_too_long", "%s %s has TTL %d, longer than the DNSSEC policy's max-zone-ttl (%d), so named would refuse to load the signed zone; lower it to %d or less", strings.TrimSuffix(r.Name, "."), r.Type, *r.TTL, maxTTL, maxTTL) p.ObjectType, p.ObjectID, p.Name = "record", r.ID, r.Name } } } } func isNo(v string) bool { switch strings.ToLower(strings.Trim(strings.TrimSpace(v), `";`)) { case "no", "false", "0": return true } return false } var ( isoDuration = regexp.MustCompile(`^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$`) ttlDuration = regexp.MustCompile(`^(?:\d+[wdhms])+$`) ttlPart = regexp.MustCompile(`(\d+)([wdhms])`) ) // parseDuration parses a BIND duration: ISO 8601 (P1D, PT12H), seconds, or // TTL style (1d, 12h30m). func parseDuration(s string) (uint32, bool) { s = strings.ToUpper(strings.TrimSpace(s)) if n, err := strconv.ParseUint(s, 10, 32); err == nil { return uint32(n), true } if m := isoDuration.FindStringSubmatch(s); m != nil && s != "P" && s != "PT" { units := []uint64{31536000, 2592000, 604800, 86400, 3600, 60, 1} var total uint64 for i, u := range units { if m[i+1] != "" { n, _ := strconv.ParseUint(m[i+1], 10, 32) total += n * u } } return uint32(min(total, 1<<32-1)), true } l := strings.ToLower(s) if ttlDuration.MatchString(l) { units := map[string]uint64{"w": 604800, "d": 86400, "h": 3600, "m": 60, "s": 1} var total uint64 for _, m := range ttlPart.FindAllStringSubmatch(l, -1) { n, _ := strconv.ParseUint(m[1], 10, 32) total += n * units[m[2]] } return uint32(min(total, 1<<32-1)), true } return 0, false }