jarl block pause|resume (and the Blocking page) lets every name through for a while; jarld resumes on time. The lists stay loaded: the rules zone just passes everything. Statistics gain the clients with the most blocked queries and blocked queries on the queries chart. The view choice in the blocking forms is a dropdown. 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.BlockingPausedUntil != nil && 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.BlockingPausedUntil != nil {
|
|
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
|
|
}
|