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
@@ -1,9 +1,11 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"jarl/internal/model"
|
||||
"jarl/internal/namedconf"
|
||||
)
|
||||
|
||||
// checkBlocking checks blocklists and block/allow rules (docs/blocking.md).
|
||||
@@ -67,6 +69,7 @@ func (c *checker) checkBlocking() {
|
||||
seen[key] = r
|
||||
}
|
||||
}
|
||||
c.checkClientGroups(effective)
|
||||
// Jarl writes the response-policy statement where blocking is used;
|
||||
// named allows one per view (or for the server).
|
||||
for vid := range used {
|
||||
@@ -91,3 +94,94 @@ func (c *checker) blockingScope(viewID int64) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// checkClientGroups checks blocking steps and client groups: steps exist,
|
||||
// members are addresses (directly or through ACLs), and no address is in
|
||||
// two groups of the same view.
|
||||
func (c *checker) checkClientGroups(effective func(objType string, id int64, label string, ids []int64) []int64) {
|
||||
for _, b := range c.s.SortedBlocklists() {
|
||||
if _, ok := c.s.StepRank(b.StepID); !ok || b.StepID == model.StepOff {
|
||||
c.errf("blocking_step_missing", "blocklist", b.ID, "blocklist %s is at step %d, which doesn't exist", b.Name, b.StepID)
|
||||
}
|
||||
}
|
||||
// The highest step any group reaches, per view: lists above it block
|
||||
// nothing.
|
||||
top := map[int64]int{}
|
||||
seen := map[string]*model.ClientGroup{} // view|prefix -> first group
|
||||
for _, g := range c.s.SortedClientGroups() {
|
||||
label := "client group " + g.Name
|
||||
rank, ok := c.s.StepRank(g.StepID)
|
||||
if !ok {
|
||||
c.errf("blocking_step_missing", "clientgroup", g.ID, "%s is at step %d, which doesn't exist", label, g.StepID)
|
||||
}
|
||||
if len(g.Members) == 0 {
|
||||
c.warnf("client_group_empty", "clientgroup", g.ID, "%s has no members", label)
|
||||
}
|
||||
for _, m := range g.Members {
|
||||
c.checkMember(g, m, map[string]bool{})
|
||||
}
|
||||
views := effective("clientgroup", g.ID, label, g.ViewIDs)
|
||||
prefixes := c.s.GroupPrefixes(g)
|
||||
for _, vid := range views {
|
||||
if len(prefixes) > 0 {
|
||||
top[vid] = max(top[vid], rank)
|
||||
}
|
||||
for _, p := range prefixes {
|
||||
key := c.blockingScope(vid) + "|" + p.String()
|
||||
if first := seen[key]; first != nil && first.ID != g.ID {
|
||||
c.errf("client_group_overlap", "clientgroup", g.ID, "%s is in client groups %s and %s; a client can only be in one (a more specific prefix in another group is fine)", p, first.Name, g.Name)
|
||||
continue
|
||||
}
|
||||
seen[key] = g
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, b := range c.s.SortedBlocklists() {
|
||||
rank, ok := c.s.StepRank(b.StepID)
|
||||
if !ok || rank == 0 || b.Disabled {
|
||||
continue
|
||||
}
|
||||
reached := false
|
||||
for _, vid := range c.s.BlockingViews(b.ViewIDs) {
|
||||
if top[vid] >= rank {
|
||||
reached = true
|
||||
}
|
||||
}
|
||||
if !reached {
|
||||
c.warnf("blocklist_step_unused", "blocklist", b.ID, "blocklist %s blocks nothing: it is at step %s, and no client group is at that step or above", b.Name, c.s.StepName(b.StepID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkMember checks one member of a client group: an address, a prefix,
|
||||
// or an ACL whose elements are (recursively) addresses and prefixes.
|
||||
func (c *checker) checkMember(g *model.ClientGroup, m string, seen map[string]bool) {
|
||||
if _, err := netip.ParseAddr(m); err == nil {
|
||||
return
|
||||
}
|
||||
if _, err := netip.ParsePrefix(m); err == nil {
|
||||
return
|
||||
}
|
||||
acl := c.s.ACLByName(m)
|
||||
if acl == nil {
|
||||
c.errf("client_group_member_invalid", "clientgroup", g.ID, "client group %s: %q is not an address, a prefix or an ACL", g.Name, m)
|
||||
return
|
||||
}
|
||||
if seen[acl.Name] {
|
||||
return
|
||||
}
|
||||
seen[acl.Name] = true
|
||||
for _, e := range acl.Elements {
|
||||
el, err := namedconf.ParseAMElement(e)
|
||||
if err != nil {
|
||||
continue // reported for the ACL
|
||||
}
|
||||
if el.Negated || el.Kind == namedconf.AMKey || el.Kind == namedconf.AMNested || el.Kind == namedconf.AMName && el.IsBuiltin() {
|
||||
c.errf("client_group_member_invalid", "clientgroup", g.ID, "client group %s uses ACL %s, whose element %q isn't an address or a prefix: blocking matches clients by address only", g.Name, acl.Name, e)
|
||||
continue
|
||||
}
|
||||
if el.Kind == namedconf.AMName {
|
||||
c.checkMember(g, el.Name, seen)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user