jarld checks the reverse DNS of the public zones' MX and SPF addresses as the internet sees it, every 6 hours (Exposure page, jarl exposure mail, jarl_mail_reverse_dns_ok). The Server options page, jarl option generated and GET /options/generated list what Jarl writes into named.conf by itself; stats.rate_limit_log turns off the query-errors logging. Option lists of three or more elements are shown one per line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
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")
|
|
}
|