Different blocking for different devices: steps and client groups
Client groups put devices (addresses, networks, or ACLs of them) at a blocking step: off (only your rules), normal (the default), or steps added above it, each including the lists of the steps below. Lists start at a step. In each view, a gate policy zone in front of a step's lists lets the devices below that step through (rpz-client-ip triggers with rpz-passthru), so every list is still loaded once. The IPv6 part of a gate leaves the IPv4-mapped range out: named matches IPv4 clients as ::ffff:a.b.c.d. Web UI: a Devices panel on the Blocking page and a step on each list. CLI: jarl block step, jarl block group, jarl blocklist --step. API: /blocking/steps, /blocking/groups. Database schema 8. Also: copying the state kept empty lists as empty (not nil), so reverting a changeset no longer adds restore operations for unchanged blocklists, rules and views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b8f681e2a
commit
76aaaf3ef5
33 files changed
+2096
-55
No files matched your search
@@ -2,6 +2,7 @@ package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -21,9 +22,16 @@ type PolicyZone struct {
|
||||
ViewID int64 // 0 without views
|
||||
View string // "" without views
|
||||
File string
|
||||
ListID int64 // the blocklist; 0 for the rules zone
|
||||
ListID int64 // the blocklist; 0 for the rules zone and gates
|
||||
// Gate, for a gate zone: the clients it lets through (past every zone
|
||||
// after it), and the step whose lists it is in front of.
|
||||
Gate []netip.Prefix
|
||||
GateStep string
|
||||
}
|
||||
|
||||
// IsGate reports whether z is a gate zone.
|
||||
func (z PolicyZone) IsGate() bool { return z.GateStep != "" }
|
||||
|
||||
// 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") }
|
||||
@@ -41,7 +49,9 @@ func (p Paths) rulesFile(view string) string {
|
||||
|
||||
// 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.
|
||||
// (rules win over lists), then for each step (normal, then the steps above
|
||||
// it) a gate letting through the clients below that step, when there are
|
||||
// any, and the step's enabled lists by name.
|
||||
func PolicyZones(s *model.State, p Paths) []PolicyZone {
|
||||
var out []PolicyZone
|
||||
rules, lists := s.SortedBlockRules(), s.SortedBlocklists()
|
||||
@@ -65,10 +75,26 @@ func PolicyZones(s *model.State, p Paths) []PolicyZone {
|
||||
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})
|
||||
assigns := ClientAssignments(s, vid)
|
||||
for k, step := range append([]int64{model.StepNormal}, stepIDs(s)...) {
|
||||
var tier []PolicyZone
|
||||
for _, b := range lists {
|
||||
if !b.Disabled && b.StepID == step && slices.Contains(s.BlockingViews(b.ViewIDs), vid) {
|
||||
tier = append(tier, PolicyZone{Name: blocking.ListZone(b.Name), ViewID: vid, View: view, File: p.BlocklistFile(b.ID), ListID: b.ID})
|
||||
}
|
||||
}
|
||||
if len(tier) == 0 {
|
||||
continue
|
||||
}
|
||||
// The clients below this step pass, and never see its lists.
|
||||
if pass := blocking.PassBelow(assigns, k); len(pass) > 0 {
|
||||
name := "off"
|
||||
if k > 0 {
|
||||
name = s.StepName(step)
|
||||
}
|
||||
zones = append(zones, PolicyZone{Name: blocking.GateZone(name), ViewID: vid, View: view, File: p.gateFile(view, name), Gate: pass, GateStep: s.StepName(step)})
|
||||
}
|
||||
zones = append(zones, tier...)
|
||||
}
|
||||
// While paused, every view with blocking gets a rules zone: it
|
||||
// lets everything through.
|
||||
@@ -80,6 +106,39 @@ func PolicyZones(s *model.State, p Paths) []PolicyZone {
|
||||
return out
|
||||
}
|
||||
|
||||
// gateFile returns a view's gate zone file for a step.
|
||||
func (p Paths) gateFile(view, name string) string {
|
||||
return filepath.Join(p.ZoneDir, ViewDir(view), blocking.GateZone(name)+".rpz")
|
||||
}
|
||||
|
||||
// stepIDs returns the IDs of the steps above normal, lowest first.
|
||||
func stepIDs(s *model.State) []int64 {
|
||||
var out []int64
|
||||
for _, st := range s.OrderedBlockSteps() {
|
||||
out = append(out, st.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ClientAssignments returns the step ranks the client groups of a view
|
||||
// give addresses (blocking.PassBelow).
|
||||
func ClientAssignments(s *model.State, viewID int64) []blocking.Assignment {
|
||||
var out []blocking.Assignment
|
||||
for _, g := range s.SortedClientGroups() {
|
||||
if !slices.Contains(s.BlockingViews(g.ViewIDs), viewID) {
|
||||
continue
|
||||
}
|
||||
rank, ok := s.StepRank(g.StepID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, p := range s.GroupPrefixes(g) {
|
||||
out = append(out, blocking.Assignment{Prefix: p, Rank: rank})
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -106,7 +165,9 @@ func writeResponsePolicy(w *confWriter, zones []PolicyZone, logRewrites bool) {
|
||||
// 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 {
|
||||
// A gate lets most queries through: logging that would double
|
||||
// the response log, and it blocks nothing.
|
||||
if !logRewrites || z.IsGate() {
|
||||
opts = append(opts, "log no")
|
||||
}
|
||||
w.write(fmt.Sprintf("\t\tzone %s%s;\n", namedconf.QuoteString(z.Name), prefixSpace(strings.Join(opts, " "))))
|
||||
@@ -132,8 +193,11 @@ func writePolicyZones(w *confWriter, s *model.State, zones []PolicyZone, out *Ou
|
||||
for _, z := range zones {
|
||||
w.write("\n")
|
||||
label := "blocking: your rules"
|
||||
if z.ListID != 0 {
|
||||
switch {
|
||||
case z.ListID != 0:
|
||||
label = "blocking: blocklist " + s.Blocklists[z.ListID].Name
|
||||
case z.IsGate():
|
||||
label = "blocking: clients below step " + z.GateStep
|
||||
}
|
||||
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",
|
||||
@@ -143,6 +207,14 @@ func writePolicyZones(w *confWriter, s *model.State, zones []PolicyZone, out *Ou
|
||||
out.Blocklists = appendUnique(out.Blocklists, z.File)
|
||||
continue
|
||||
}
|
||||
if z.IsGate() {
|
||||
title := "Clients that skip the lists from step " + z.GateStep + " on"
|
||||
if z.View != "" {
|
||||
title += " in view " + z.View
|
||||
}
|
||||
out.Files = append(out.Files, &File{Path: z.File, Content: []byte(blocking.GateZoneFile(title, z.Gate)), Mode: 0o640, Kind: KindPolicy})
|
||||
continue
|
||||
}
|
||||
title := "Blocking rules"
|
||||
if z.View != "" {
|
||||
title += " for view " + z.View
|
||||
|
||||
Reference in new issue
Block a user