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
+144
-1
@@ -1,6 +1,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -17,10 +19,126 @@ type Blocklist struct {
|
||||
// that offers recursion (see BlockingViews).
|
||||
ViewIDs []int64 `json:"view_ids"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Comment string `json:"comment"`
|
||||
// StepID is the blocking step the list starts at: StepNormal for
|
||||
// every client, or a BlockStep for the clients of groups at that step
|
||||
// or above.
|
||||
StepID int64 `json:"step_id"`
|
||||
Comment string `json:"comment"`
|
||||
Meta
|
||||
}
|
||||
|
||||
// Built-in blocking steps (BlockStep IDs are positive).
|
||||
const (
|
||||
StepOff int64 = -1 // no blocklists (rules still apply)
|
||||
StepNormal int64 = 0 // the default: every client not in a group
|
||||
)
|
||||
|
||||
// BlockStep is a blocking step above Normal. Steps are ordered: each
|
||||
// includes the lists of the steps below it, plus its own (docs/blocking.md).
|
||||
type BlockStep struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Position int `json:"position"`
|
||||
Comment string `json:"comment"`
|
||||
Meta
|
||||
}
|
||||
|
||||
// ClientGroup gives the clients with these addresses a blocking step.
|
||||
type ClientGroup struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// Members are addresses, prefixes, or names of ACLs made of them.
|
||||
Members []string `json:"members"`
|
||||
StepID int64 `json:"step_id"` // StepOff, StepNormal or a BlockStep
|
||||
ViewIDs []int64 `json:"view_ids"` // as for Blocklist
|
||||
Comment string `json:"comment"`
|
||||
Meta
|
||||
}
|
||||
|
||||
// OrderedBlockSteps returns the steps above Normal, lowest first.
|
||||
func (s *State) OrderedBlockSteps() []*BlockStep {
|
||||
out := make([]*BlockStep, 0, len(s.BlockSteps))
|
||||
for _, st := range s.BlockSteps {
|
||||
out = append(out, st)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Position != out[j].Position {
|
||||
return out[i].Position < out[j].Position
|
||||
}
|
||||
return out[i].ID < out[j].ID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// StepRank orders steps: -1 for Off, 0 for Normal, 1, 2, ... for the
|
||||
// steps above it. ok is false for a step that doesn't exist.
|
||||
func (s *State) StepRank(id int64) (rank int, ok bool) {
|
||||
switch id {
|
||||
case StepOff:
|
||||
return -1, true
|
||||
case StepNormal:
|
||||
return 0, true
|
||||
}
|
||||
for i, st := range s.OrderedBlockSteps() {
|
||||
if st.ID == id {
|
||||
return i + 1, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// StepName returns a step's name ("off" and "normal" for the built-in
|
||||
// ones).
|
||||
func (s *State) StepName(id int64) string {
|
||||
switch id {
|
||||
case StepOff:
|
||||
return "off"
|
||||
case StepNormal:
|
||||
return "normal"
|
||||
}
|
||||
if st := s.BlockSteps[id]; st != nil {
|
||||
return st.Name
|
||||
}
|
||||
return fmt.Sprintf("step %d", id)
|
||||
}
|
||||
|
||||
// StepByName finds a step by name, the built-in ones included.
|
||||
func (s *State) StepByName(name string) (int64, bool) {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
switch name {
|
||||
case "off":
|
||||
return StepOff, true
|
||||
case "normal":
|
||||
return StepNormal, true
|
||||
}
|
||||
for _, st := range s.BlockSteps {
|
||||
if strings.EqualFold(st.Name, name) {
|
||||
return st.ID, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// ClientGroupByName finds a client group by name.
|
||||
func (s *State) ClientGroupByName(name string) *ClientGroup {
|
||||
for _, g := range s.ClientGroups {
|
||||
if strings.EqualFold(g.Name, name) {
|
||||
return g
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortedClientGroups returns the client groups ordered by name.
|
||||
func (s *State) SortedClientGroups() []*ClientGroup {
|
||||
out := make([]*ClientGroup, 0, len(s.ClientGroups))
|
||||
for _, g := range s.ClientGroups {
|
||||
out = append(out, g)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// Block rule actions.
|
||||
const (
|
||||
BlockActionBlock = "block"
|
||||
@@ -134,3 +252,28 @@ func (s *State) BlockingViews(viewIDs []int64) []int64 {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GroupPrefixes returns the addresses of a client group as prefixes: its
|
||||
// own, and those of the ACLs it names (and the ACLs they name). Elements
|
||||
// that aren't addresses are left out (validation reports them).
|
||||
func (s *State) GroupPrefixes(g *ClientGroup) []netip.Prefix {
|
||||
var out []netip.Prefix
|
||||
seen := map[string]bool{}
|
||||
var add func(elems []string)
|
||||
add = func(elems []string) {
|
||||
for _, e := range elems {
|
||||
e = strings.TrimSpace(e)
|
||||
if a, err := netip.ParseAddr(e); err == nil {
|
||||
a = a.Unmap()
|
||||
out = append(out, netip.PrefixFrom(a, a.BitLen()))
|
||||
} else if p, err := netip.ParsePrefix(e); err == nil {
|
||||
out = append(out, p.Masked())
|
||||
} else if acl := s.ACLByName(e); acl != nil && !seen[acl.Name] {
|
||||
seen[acl.Name] = true
|
||||
add(acl.Elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
add(g.Members)
|
||||
return out
|
||||
}
|
||||
+29
-12
@@ -9,6 +9,7 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -247,8 +248,10 @@ type State struct {
|
||||
Records map[int64]*Record
|
||||
Options map[int64]*Option
|
||||
// Blocking (docs/blocking.md).
|
||||
Blocklists map[int64]*Blocklist
|
||||
BlockRules map[int64]*BlockRule
|
||||
Blocklists map[int64]*Blocklist
|
||||
BlockRules map[int64]*BlockRule
|
||||
BlockSteps map[int64]*BlockStep
|
||||
ClientGroups map[int64]*ClientGroup
|
||||
}
|
||||
|
||||
// NewState returns an empty state with default settings.
|
||||
@@ -262,8 +265,10 @@ func NewState() *State {
|
||||
Records: map[int64]*Record{},
|
||||
Options: map[int64]*Option{},
|
||||
|
||||
Blocklists: map[int64]*Blocklist{},
|
||||
BlockRules: map[int64]*BlockRule{},
|
||||
Blocklists: map[int64]*Blocklist{},
|
||||
BlockRules: map[int64]*BlockRule{},
|
||||
BlockSteps: map[int64]*BlockStep{},
|
||||
ClientGroups: map[int64]*ClientGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,14 +283,16 @@ func (s *State) Clone() *State {
|
||||
Records: make(map[int64]*Record, len(s.Records)),
|
||||
Options: make(map[int64]*Option, len(s.Options)),
|
||||
|
||||
Blocklists: make(map[int64]*Blocklist, len(s.Blocklists)),
|
||||
BlockRules: make(map[int64]*BlockRule, len(s.BlockRules)),
|
||||
Blocklists: make(map[int64]*Blocklist, len(s.Blocklists)),
|
||||
BlockRules: make(map[int64]*BlockRule, len(s.BlockRules)),
|
||||
BlockSteps: make(map[int64]*BlockStep, len(s.BlockSteps)),
|
||||
ClientGroups: make(map[int64]*ClientGroup, len(s.ClientGroups)),
|
||||
}
|
||||
c.Settings.Nameservers = append([]string(nil), s.Settings.Nameservers...)
|
||||
c.Settings.Nameservers = slices.Clone(s.Settings.Nameservers)
|
||||
for id, v := range s.Views {
|
||||
n := *v
|
||||
n.MatchClients = append([]string(nil), v.MatchClients...)
|
||||
n.MatchDestinations = append([]string(nil), v.MatchDestinations...)
|
||||
n.MatchClients = slices.Clone(v.MatchClients)
|
||||
n.MatchDestinations = slices.Clone(v.MatchDestinations)
|
||||
if v.MatchRecursiveOnly != nil {
|
||||
b := *v.MatchRecursiveOnly
|
||||
n.MatchRecursiveOnly = &b
|
||||
@@ -294,7 +301,7 @@ func (s *State) Clone() *State {
|
||||
}
|
||||
for id, a := range s.ACLs {
|
||||
n := *a
|
||||
n.Elements = append([]string(nil), a.Elements...)
|
||||
n.Elements = slices.Clone(a.Elements)
|
||||
c.ACLs[id] = &n
|
||||
}
|
||||
for id, k := range s.Keys {
|
||||
@@ -319,14 +326,24 @@ func (s *State) Clone() *State {
|
||||
}
|
||||
for id, b := range s.Blocklists {
|
||||
n := *b
|
||||
n.ViewIDs = append([]int64(nil), b.ViewIDs...)
|
||||
n.ViewIDs = slices.Clone(b.ViewIDs)
|
||||
c.Blocklists[id] = &n
|
||||
}
|
||||
for id, r := range s.BlockRules {
|
||||
n := *r
|
||||
n.ViewIDs = append([]int64(nil), r.ViewIDs...)
|
||||
n.ViewIDs = slices.Clone(r.ViewIDs)
|
||||
c.BlockRules[id] = &n
|
||||
}
|
||||
for id, st := range s.BlockSteps {
|
||||
n := *st
|
||||
c.BlockSteps[id] = &n
|
||||
}
|
||||
for id, g := range s.ClientGroups {
|
||||
n := *g
|
||||
n.Members = slices.Clone(g.Members)
|
||||
n.ViewIDs = slices.Clone(g.ViewIDs)
|
||||
c.ClientGroups[id] = &n
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user