Internet exposure checks and response rate limiting

Jarl works out which views are reachable from the internet (match-clients
ending in a catch-all, not private-looking addresses: an internal view of a
global IPv6 prefix is still yours; a server without views: open allow-query
and a non-private listen address), with a per-view override (schema 9), and
checks public ones: rate limiting, open resolver, minimal-any, cache
snooping, open NOTIFY and transfers, version and host name, management
interfaces. Fixes are staged like any change, including rate limiting from
ISC's example in log-only mode with the secondaries exempt, then enforcing.
named logs what rate limiting limits (or would, in log-only mode, where its
counters don't move) as query-errors; Jarl routes those lines to its log
and counts them (rrl breakdown, ratelimited top lists, Prometheus
jarl_ratelimit_logged_total), shown on the new Exposure page.

A change that newly opens a public view up must be confirmed (accept_risk,
--accept-risk, a checkbox on the Changes page; 409 confirm_risk
otherwise). Findings can be acknowledged with a reason and stay visible as
info. GET /exposure, POST /exposure/fix|ack|unack, jarl exposure,
docs/exposure.md.

Also fixes query statistics skipping TSIG-signed queries ("IP#port/key
NAME" in named's log).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 10:38:56 +01:00
1 parent cf550bd512
commit bdd7acfd3b
50 files changed
+2019 -52

No files matched your search

+8 -2
View File
@@ -272,6 +272,12 @@ func renderConfig(s *model.State, p Paths, x Extras, out *Output) (string, []Spa
pzs := PolicyZones(s, p)
out.Policy = pzs
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"
}
sawStats, sawLogging := false, false
if top := s.ScopedOptions(model.ScopeTopLevel, 0); len(top) > 0 {
w.write("\n")
@@ -282,7 +288,7 @@ func renderConfig(s *model.State, p Paths, x Extras, out *Output) (string, []Spa
w.write(st.statsStatement(o))
sawStats = true
case logResponses && o.Name == "logging" && !sawLogging:
w.write(st.loggingStatement(o, logRewrites))
w.write(st.loggingStatement(o, logRewrites, logRRL))
sawLogging = true
default:
w.write(optionText(o, 0))
@@ -297,7 +303,7 @@ func renderConfig(s *model.State, p Paths, x Extras, out *Output) (string, []Spa
w.write(st.statsStatement(nil))
}
if logResponses && !sawLogging {
w.write(st.loggingStatement(nil, logRewrites))
w.write(st.loggingStatement(nil, logRewrites, logRRL))
}
w.end()
}
+14 -4
View File
@@ -72,9 +72,10 @@ func (h *StatsHooks) statsStatement(user *model.Option) string {
}
// loggingStatement returns the logging statement with Jarl's channel and
// the responses category (and rpz, for blocking's rewrites), merged into
// the user's own if there is one.
func (h *StatsHooks) loggingStatement(user *model.Option, rpz bool) string {
// the responses category (rpz too, for blocking's rewrites, and
// query-errors for rate limiting), merged into the user's own if there is
// one.
func (h *StatsHooks) loggingStatement(user *model.Option, rpz, rrl bool) string {
var st *namedconf.Stmt
if user != nil {
var err error
@@ -92,6 +93,9 @@ func (h *StatsHooks) loggingStatement(user *model.Option, rpz bool) string {
if rpz {
categories = append(categories, "rpz")
}
if rrl {
categories = append(categories, "query-errors")
}
for _, cat := range categories {
merged := false
for _, s := range b.Stmts {
@@ -101,7 +105,13 @@ func (h *StatsHooks) loggingStatement(user *model.Option, rpz bool) string {
}
}
if !merged {
b.Stmts = append(b.Stmts, namedconf.NewStmt("category", cat, namedconf.NewBlock(namedconf.NewStmt(StatsChannelName))))
channels := namedconf.NewBlock(namedconf.NewStmt(StatsChannelName))
if cat == "query-errors" {
// Naming the category takes it away from named's
// default destination: keep that too.
channels.Stmts = append(channels.Stmts, namedconf.NewStmt("default_syslog"))
}
b.Stmts = append(b.Stmts, namedconf.NewStmt("category", cat, channels))
}
}
prefix := ""