package render import ( "strings" "jarl/internal/model" "jarl/internal/namedconf" ) // Generated is a statement Jarl adds to named's configuration by itself, // not from an option you set: why, and what controls it. type Generated struct { Where string `json:"where"` // top level, options, or "view NAME" Text string `json:"text"` // as written in named.conf Why string `json:"why"` // what it is for Setting string `json:"setting"` // what turns it on or off } // GeneratedStatements lists what Render adds to named.conf beyond the // options in the database, so nothing there comes as a surprise. func GeneratedStatements(s *model.State, p Paths, x Extras) []Generated { var out []Generated add := func(where, text, why, setting string) { out = append(out, Generated{Where: where, Text: strings.TrimRight(text, "\n"), Why: why, Setting: setting}) } add("top level", "include "+namedconf.QuoteString(p.KeysConfig())+";", "TSIG keys (and the view keys) live in their own file, which only named and jarld can read", "always") st := x.Stats logResponses := st != nil && st.LogFile != "" pzs := PolicyZones(s, p) if st != nil { _, user := findTop(s, "statistics-channels") why := "named publishes its counters for jarld's query statistics, on a loopback address only" if user { why += " (added to your statistics-channels statement)" } add("top level", formatStmt(namedconf.NewStmt("statistics-channels", namedconf.NewBlock(st.statsInet())), 0), why, "stats.enabled in jarl.yaml") } if logResponses { b := namedconf.NewBlock(st.logChannel()) cat := func(name string, extra ...string) { ch := namedconf.NewBlock(namedconf.NewStmt(StatsChannelName)) for _, e := range extra { ch.Stmts = append(ch.Stmts, namedconf.NewStmt(e)) } b.Stmts = append(b.Stmts, namedconf.NewStmt("category", name, ch)) } why := []string{"responses: every answer, for the statistics' names, clients and views"} cat("responses") if len(pzs) > 0 { cat("rpz") why = append(why, "rpz: what blocking blocked") } if logRateLimit(s, st) { cat("query-errors", "default_syslog") why = append(why, "query-errors: what rate limiting limited, or would in log-only mode, for the Exposure page (also to syslog, as before)") } w := "named logs to a file jarld reads (" + strings.Join(why, "; ") + ")" if _, user := findTop(s, "logging"); user { w += "; merged into your logging statement" } setting := "stats.query_log in jarl.yaml" if logRateLimit(s, st) || hasRateLimit(s) { setting += "; query-errors: stats.rate_limit_log" } add("top level", formatStmt(namedconf.NewStmt("logging", b), 0), w, setting) if s.FindOption(model.ScopeOptions, 0, "responselog") == nil { add("options", "responselog yes;", "named logs answers from the start, for the statistics", "stats.query_log in jarl.yaml") } } views := s.OrderedViews() scopes := []struct { id int64 name string }{{0, "options"}} if len(views) > 0 { scopes = scopes[:0] for _, v := range views { scopes = append(scopes, struct { id int64 name string }{v.ID, "view " + v.Name}) } } for _, sc := range scopes { zs := scopeZones(pzs, sc.id) if len(zs) == 0 { continue } w := newConfWriter() writeResponsePolicy(w, zs, logResponses) writePolicyZones(w, s, zs, &Output{}, 0) add(sc.name, dedent(w.sb.String()), "blocking: your rules and blocklists, as response policy zones named loads", "the Blocking page (jarl block, jarl blocklist)") } if viewKeysOn(s, Extras{ViewKeySeed: x.ViewKeySeed}) { for _, v := range views { mc := viewMatchClients(s, x, v) var keys []string for _, e := range mc { if strings.Contains(e, "key ") { keys = append(keys, e+";") } } add("view "+v.Name, "match-clients { "+strings.Join(keys, " ")+" ... };", "view keys: a query signed with a view's key gets that view, from any address, and never another view (jarl view key)", "always, when there are views") } } return out } func findTop(s *model.State, name string) (*model.Option, bool) { for _, o := range s.ScopedOptions(model.ScopeTopLevel, 0) { if o.Name == name { return o, true } } return nil, false } func hasRateLimit(s *model.State) bool { for _, o := range s.Options { if o.Name == "rate-limit" { return true } } return false } // logRateLimit reports whether rate limiting's log lines go to the // response log (docs/exposure.md). func logRateLimit(s *model.State, st *StatsHooks) bool { return st != nil && st.LogFile != "" && !st.NoRateLimitLog && hasRateLimit(s) } // dedent removes one leading tab from every line. func dedent(s string) string { lines := strings.Split(s, "\n") for i, l := range lines { lines[i] = strings.TrimPrefix(l, "\t") } return strings.Join(lines, "\n") }