Mail reverse DNS check, what Jarl adds to named.conf, lists one per line
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>
This commit is contained in:
1 parent
d33f573782
commit
c27a6c10bd
30 files changed
+1095
-16
No files matched your search
@@ -0,0 +1,142 @@
|
||||
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")
|
||||
}
|
||||
@@ -274,10 +274,7 @@ func renderConfig(s *model.State, p Paths, x Extras, out *Output) (string, []Spa
|
||||
logRewrites := logResponses && len(pzs) > 0
|
||||
// Rate limiting logs what it limits (or would, in log-only mode) as
|
||||
// query-errors; the statistics count it (docs/exposure.md).
|
||||
logRRL := false
|
||||
for _, o := range s.Options {
|
||||
logRRL = logRRL || logResponses && o.Name == "rate-limit"
|
||||
}
|
||||
logRRL := logRateLimit(s, st)
|
||||
sawStats, sawLogging := false, false
|
||||
if top := s.ScopedOptions(model.ScopeTopLevel, 0); len(top) > 0 {
|
||||
w.write("\n")
|
||||
|
||||
@@ -26,6 +26,9 @@ type StatsHooks struct {
|
||||
LogFile string // response log; "" disables response logging
|
||||
LogVersions int
|
||||
LogSize string
|
||||
// NoRateLimitLog keeps rate limiting's log lines (category
|
||||
// query-errors) out of the response log (stats.rate_limit_log: false).
|
||||
NoRateLimitLog bool
|
||||
}
|
||||
|
||||
// StatsChannelName is the logging channel Jarl adds for responses.
|
||||
|
||||
@@ -82,3 +82,45 @@ func TestStatsHooksMerged(t *testing.T) {
|
||||
func TestStatsHooksWithNamed(t *testing.T) {
|
||||
checkWithNamed(t, sampleState(t), Extras{Stats: statsHooks()})
|
||||
}
|
||||
|
||||
// Rate limiting's log lines go to the response log unless
|
||||
// stats.rate_limit_log is off; the list of what Jarl adds says so.
|
||||
func TestRateLimitLogAndGenerated(t *testing.T) {
|
||||
s := sampleState(t)
|
||||
for id, o := range s.Options {
|
||||
if o.Name == "logging" {
|
||||
delete(s.Options, id)
|
||||
}
|
||||
}
|
||||
s.Options[20000] = &model.Option{ID: 20000, Scope: model.ScopeOptions, Name: "rate-limit", Value: "{ responses-per-second 10; log-only yes; }", Position: 20000}
|
||||
p := Paths{ConfigDir: "/etc/bind/jarl", ZoneDir: "/var/lib/bind/jarl", CacheDir: "/var/cache/bind/jarl"}
|
||||
conf := string(Render(s, p, Extras{Stats: statsHooks()}).File(p.MainConfig()).Content)
|
||||
if !strings.Contains(conf, "category query-errors { jarl_responses; default_syslog; };") {
|
||||
t.Errorf("query-errors not logged:\n%s", conf)
|
||||
}
|
||||
gen := GeneratedStatements(s, p, Extras{Stats: statsHooks()})
|
||||
var logging *Generated
|
||||
for i := range gen {
|
||||
if strings.HasPrefix(gen[i].Text, "logging") {
|
||||
logging = &gen[i]
|
||||
}
|
||||
}
|
||||
if logging == nil || !strings.Contains(logging.Text, "query-errors") || !strings.Contains(logging.Setting, "stats.rate_limit_log") {
|
||||
t.Errorf("generated logging = %+v (all %+v)", logging, gen)
|
||||
}
|
||||
if !strings.HasPrefix(gen[0].Text, "include ") || gen[0].Where != "top level" {
|
||||
t.Errorf("first generated = %+v", gen[0])
|
||||
}
|
||||
|
||||
h := statsHooks()
|
||||
h.NoRateLimitLog = true
|
||||
conf = string(Render(s, p, Extras{Stats: h}).File(p.MainConfig()).Content)
|
||||
if strings.Contains(conf, "query-errors") || !strings.Contains(conf, "category responses") {
|
||||
t.Errorf("rate_limit_log off but query-errors logged:\n%s", conf)
|
||||
}
|
||||
for _, g := range GeneratedStatements(s, p, Extras{Stats: h}) {
|
||||
if strings.Contains(g.Text, "query-errors") {
|
||||
t.Errorf("listed while off: %+v", g)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user