Files
jarl/internal/render/stats.go
T
adamandClaude Opus 5.5 9fc23310bd Add blocking: blocklists and block/allow rules with BIND response policy zones
jarld downloads blocklists (Adblock, hosts or plain domains), checks them
daily and reloads named only when a list changed; your own block and
allow rules take precedence, and the most specific rule wins. Lists and
rules apply per view (default: every view that offers recursion). Web UI
Blocking page with name checks, jarl blocklist and jarl block, the
/blocking API, blocked counts in the statistics. Database schema 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:24:19 +00:00

117 lines
3.8 KiB
Go

package render
import (
"net/netip"
"strconv"
"strings"
"jarl/internal/model"
"jarl/internal/namedconf"
)
// Extras are additions to named's configuration that come from jarld's
// own configuration (jarl.yaml) rather than from the database.
type Extras struct {
// Stats, when set, makes named publish what Jarl's statistics
// collector reads.
Stats *StatsHooks
}
// StatsHooks describe the statistics channel and response log Jarl needs.
type StatsHooks struct {
Channel netip.AddrPort // statistics-channels address (loopback)
LogFile string // response log; "" disables response logging
LogVersions int
LogSize string
}
// StatsChannelName is the logging channel Jarl adds for responses.
const StatsChannelName = "jarl_responses"
const statsComment = "Added by Jarl for query statistics (stats in /etc/jarl/jarl.yaml)."
// statsInet is the statistics-channels clause for the collector.
func (h *StatsHooks) statsInet() *namedconf.Stmt {
addr := h.Channel.Addr().String()
return namedconf.NewStmt("inet", addr, "port", strconv.Itoa(int(h.Channel.Port())),
"allow", namedconf.NewBlock(namedconf.NewStmt(addr)))
}
// logChannel is the logging channel the response log is written to.
func (h *StatsHooks) logChannel() *namedconf.Stmt {
return namedconf.NewStmt("channel", StatsChannelName, namedconf.NewBlock(
namedconf.NewStmt("file", namedconf.Quoted(h.LogFile), "versions", strconv.Itoa(h.LogVersions), "size", h.LogSize),
namedconf.NewStmt("print-time", "yes"),
namedconf.NewStmt("severity", "info"),
))
}
// statsStatement returns the statistics-channels statement, merged into the
// user's own if there is one.
func (h *StatsHooks) statsStatement(user *model.Option) string {
if user != nil {
if st, err := namedconf.ParseOptionValue(user.Name, user.Value); err == nil && st.FirstBlock() != nil {
b := st.FirstBlock()
want := namedconf.CompactStmt(h.statsInet())
for _, s := range b.Stmts {
if namedconf.CompactStmt(s) == want {
return optionText(user, 0)
}
}
b.Stmts = append(b.Stmts, h.statsInet())
return commentLines(user.Comment) + commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
}
// Unparsable: validation reports it; leave it alone.
return optionText(user, 0)
}
st := namedconf.NewStmt("statistics-channels", namedconf.NewBlock(h.statsInet()))
return commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
}
// 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 {
var st *namedconf.Stmt
if user != nil {
var err error
st, err = namedconf.ParseOptionValue(user.Name, user.Value)
if err != nil || st.FirstBlock() == nil {
return optionText(user, 0)
}
} else {
st = namedconf.NewStmt("logging", namedconf.NewBlock())
}
b := st.FirstBlock()
// Channels must be defined before the categories that use them.
b.Stmts = append([]*namedconf.Stmt{h.logChannel()}, b.Stmts...)
categories := []string{"responses"}
if rpz {
categories = append(categories, "rpz")
}
for _, cat := range categories {
merged := false
for _, s := range b.Stmts {
if s.Keyword() == "category" && strings.EqualFold(s.Arg(0), cat) && s.FirstBlock() != nil {
s.FirstBlock().Stmts = append(s.FirstBlock().Stmts, namedconf.NewStmt(StatsChannelName))
merged = true
}
}
if !merged {
b.Stmts = append(b.Stmts, namedconf.NewStmt("category", cat, namedconf.NewBlock(namedconf.NewStmt(StatsChannelName))))
}
}
prefix := ""
if user != nil {
prefix = commentLines(user.Comment)
}
return prefix + commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
}
func commentLines(c string) string {
if c == "" {
return ""
}
return commentBlock("//", c)
}