Added:
- Server statistics: named's memory and CPU (from /proc), cache size,
sockets and client counts against their limits (statistics channel,
rndc status), host memory and load, and free disk space. Memory and
CPU are kept over time as gauges (sum and sample count, averaged in
timelines). Statistics page Server section, jarl stats server,
GET /stats/server.
- jarl record add|set|delete|disable|enable --view a,b | all: the same
change in each view, applied as one changeset.
- Blocking can be paused until resumed (jarl block pause indefinitely,
"Until I resume", {"indefinitely": true}).
Changed:
- List options (allow-transfer, forwarders, ...) get their braces added
when given without them.
- The dashboard lists named-checkconf's warnings about the running
configuration; the CLI prints them after an apply.
- Stats page: tile and card rows fill their width, and the queries and
failed answers charts share margins so their time axes line up.
- Dashboard: options have their own tile.
- 24-hour times in the web UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
5.5 KiB
Go
168 lines
5.5 KiB
Go
package render
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"jarl/internal/blocking"
|
|
"jarl/internal/model"
|
|
"jarl/internal/namedconf"
|
|
)
|
|
|
|
// KindPolicy is the kind of a rendered rules policy zone file.
|
|
const KindPolicy = "policy"
|
|
|
|
// PolicyZone is a response policy zone in named's configuration
|
|
// (docs/blocking.md).
|
|
type PolicyZone struct {
|
|
Name string // without the final dot
|
|
ViewID int64 // 0 without views
|
|
View string // "" without views
|
|
File string
|
|
ListID int64 // the blocklist; 0 for the rules zone
|
|
}
|
|
|
|
// BlocklistsDir holds the zone files of downloaded blocklists, one per
|
|
// list, shared by the views that use it.
|
|
func (p Paths) BlocklistsDir() string { return filepath.Join(p.CacheDir, "blocklists") }
|
|
|
|
// BlocklistFile returns the zone file of a blocklist. It is named after
|
|
// the list's ID, so renaming the list doesn't move it.
|
|
func (p Paths) BlocklistFile(id int64) string {
|
|
return filepath.Join(p.BlocklistsDir(), fmt.Sprintf("list-%d.rpz", id))
|
|
}
|
|
|
|
// rulesFile returns a view's rules zone file.
|
|
func (p Paths) rulesFile(view string) string {
|
|
return filepath.Join(p.ZoneDir, ViewDir(view), blocking.RulesZone+".rpz")
|
|
}
|
|
|
|
// PolicyZones returns the policy zones named loads, per view in view
|
|
// order, each view's in the order named checks them: the rules zone first
|
|
// (rules win over lists), then the enabled lists by name.
|
|
func PolicyZones(s *model.State, p Paths) []PolicyZone {
|
|
var out []PolicyZone
|
|
rules, lists := s.SortedBlockRules(), s.SortedBlocklists()
|
|
scopes := []int64{0}
|
|
if len(s.Views) > 0 {
|
|
scopes = nil
|
|
for _, v := range s.OrderedViews() {
|
|
scopes = append(scopes, v.ID)
|
|
}
|
|
}
|
|
for _, vid := range scopes {
|
|
view := ""
|
|
if v := s.Views[vid]; v != nil {
|
|
view = v.Name
|
|
}
|
|
var zones []PolicyZone
|
|
hasRules := false
|
|
for _, r := range rules {
|
|
if slices.Contains(s.BlockingViews(r.ViewIDs), vid) {
|
|
hasRules = true
|
|
break
|
|
}
|
|
}
|
|
for _, b := range lists {
|
|
if !b.Disabled && slices.Contains(s.BlockingViews(b.ViewIDs), vid) {
|
|
zones = append(zones, PolicyZone{Name: blocking.ListZone(b.Name), ViewID: vid, View: view, File: p.BlocklistFile(b.ID), ListID: b.ID})
|
|
}
|
|
}
|
|
// While paused, every view with blocking gets a rules zone: it
|
|
// lets everything through.
|
|
if hasRules || s.Settings.BlockingPaused() && len(zones) > 0 {
|
|
out = append(out, PolicyZone{Name: blocking.RulesZone, ViewID: vid, View: view, File: p.rulesFile(view)})
|
|
}
|
|
out = append(out, zones...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ViewRules returns the rules that apply in a view (0 without views).
|
|
func ViewRules(s *model.State, viewID int64) []blocking.Rule {
|
|
var out []blocking.Rule
|
|
for _, r := range s.SortedBlockRules() {
|
|
if slices.Contains(s.BlockingViews(r.ViewIDs), viewID) {
|
|
out = append(out, blocking.Rule{Name: r.Name, Action: r.Action, Exact: r.Exact})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// writeResponsePolicy writes the response-policy statement for a view's
|
|
// policy zones (or the server's, without views). Rewrites are logged to
|
|
// Jarl's response log when statistics read it, and not at all otherwise
|
|
// (they would go to named's default log).
|
|
func writeResponsePolicy(w *confWriter, zones []PolicyZone, logRewrites bool) {
|
|
if len(zones) == 0 {
|
|
return
|
|
}
|
|
w.begin("blocking", 0, "blocking (rules and blocklists)")
|
|
w.write("\t// Blocking (docs/blocking.md): rules first, then blocklists.\n\tresponse-policy {\n")
|
|
for _, z := range zones {
|
|
// Apply a new version at once: Jarl reloads a list only when it
|
|
// changed (at most every blocking.refresh), so named's default
|
|
// delay for batching frequent updates only adds a wait.
|
|
opts := []string{"min-update-interval 0"}
|
|
if !logRewrites {
|
|
opts = append(opts, "log no")
|
|
}
|
|
w.write(fmt.Sprintf("\t\tzone %s%s;\n", namedconf.QuoteString(z.Name), prefixSpace(strings.Join(opts, " "))))
|
|
}
|
|
// Block without waiting for the real answer, and also for clients
|
|
// that ask for DNSSEC data.
|
|
w.write("\t} qname-wait-recurse no break-dnssec yes;\n")
|
|
w.end()
|
|
}
|
|
|
|
func prefixSpace(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " " + s
|
|
}
|
|
|
|
// writePolicyZones writes the zone statements of policy zones and adds
|
|
// their files to the output: rules zone files are rendered, blocklist
|
|
// files only listed (jarld downloads them; see Output.Blocklists).
|
|
func writePolicyZones(w *confWriter, s *model.State, zones []PolicyZone, out *Output, indent int) {
|
|
tabs := strings.Repeat("\t", indent)
|
|
for _, z := range zones {
|
|
w.write("\n")
|
|
label := "blocking: your rules"
|
|
if z.ListID != 0 {
|
|
label = "blocking: blocklist " + s.Blocklists[z.ListID].Name
|
|
}
|
|
w.begin("blocking", z.ListID, label)
|
|
w.write(fmt.Sprintf("%szone %s {\n%s\ttype primary;\n%s\tfile %s;\n%s\tallow-query { none; };\n%s\tallow-transfer { none; };\n%s\tnotify no;\n%s};\n",
|
|
tabs, namedconf.QuoteString(z.Name), tabs, tabs, namedconf.QuoteString(z.File), tabs, tabs, tabs, tabs))
|
|
w.end()
|
|
if z.ListID != 0 {
|
|
out.Blocklists = appendUnique(out.Blocklists, z.File)
|
|
continue
|
|
}
|
|
title := "Blocking rules"
|
|
if z.View != "" {
|
|
title += " for view " + z.View
|
|
}
|
|
content := blocking.RulesZoneFile(title, ViewRules(s, z.ViewID))
|
|
if s.Settings.BlockingPaused() {
|
|
content = blocking.PausedZoneFile(title)
|
|
}
|
|
out.Files = append(out.Files, &File{Path: z.File, Content: []byte(content), Mode: 0o640, Kind: KindPolicy})
|
|
}
|
|
}
|
|
|
|
// scopeZones returns the policy zones of one view (0 without views).
|
|
func scopeZones(all []PolicyZone, viewID int64) []PolicyZone {
|
|
var out []PolicyZone
|
|
for _, z := range all {
|
|
if z.ViewID == viewID {
|
|
out = append(out, z)
|
|
}
|
|
}
|
|
return out
|
|
}
|