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:
adamandClaude Opus 5.5 committed 2026-10-11 21:33:16 +01:00
1 parent 5b8f681e2a
commit 76aaaf3ef5
33 files changed
+2096 -55

No files matched your search

+14 -4
View File
@@ -43,7 +43,7 @@ named answers "doesn't exist" for the domains on them (docs/blocking.md).
Hosts, plain domain and Adblock (||domain^) lists work. Your own block and
allow rules ('jarl block') take precedence over every list.`}
var views []string
var comment string
var comment, step string
list := &cobra.Command{
Use: "list",
@@ -59,7 +59,7 @@ allow rules ('jarl block') take precedence over every list.`}
return err
}
return output(lists, func(w io.Writer) {
fmt.Fprintln(w, "NAME\tSTATE\tDOMAINS\tVIEWS\tUPDATED\tMEMORY\tURL")
fmt.Fprintln(w, "NAME\tSTATE\tSTEP\tDOMAINS\tVIEWS\tUPDATED\tMEMORY\tURL")
for _, b := range lists {
domains, updated, mem := "-", "-", "-"
if s := b.Status; s != nil && s.UpdatedAt != nil {
@@ -68,7 +68,7 @@ allow rules ('jarl block') take precedence over every list.`}
if b.MemoryMB > 0 {
mem = fmt.Sprintf("~%d MB", b.MemoryMB)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", b.Name, b.State, domains, viewsText(b.Views, b.AutoViews), updated, mem, b.URL)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", b.Name, b.State, b.Step, domains, viewsText(b.Views, b.AutoViews), updated, mem, b.URL)
}
for _, b := range lists {
if b.Status != nil && b.Status.Error != "" {
@@ -95,6 +95,9 @@ allow rules ('jarl block') take precedence over every list.`}
}
body := map[string]any{"name": args[0], "url": args[1]}
viewsBody(body, views)
if step != "" {
body["step"] = step
}
if comment != "" {
body["comment"] = comment
}
@@ -111,6 +114,7 @@ allow rules ('jarl block') take precedence over every list.`}
}
add.Flags().StringArrayVar(&views, "view", nil, "a view it applies to (repeat for several; default: every view that offers recursion)")
add.Flags().StringVar(&comment, "comment", "", "comment")
add.Flags().StringVar(&step, "step", "", "the blocking step it starts at (default: normal; see 'jarl block step')")
var rename, newURL string
update := &cobra.Command{
@@ -136,6 +140,9 @@ allow rules ('jarl block') take precedence over every list.`}
if f.Changed("comment") {
body["comment"] = comment
}
if f.Changed("step") {
body["step"] = step
}
viewsBody(body, views)
if len(body) == 0 {
return fmt.Errorf("nothing to change (see --help)")
@@ -151,6 +158,7 @@ allow rules ('jarl block') take precedence over every list.`}
update.Flags().StringVar(&newURL, "url", "", "new URL")
update.Flags().StringArrayVar(&views, "view", nil, `a view it applies to (repeat for several; "auto": every view that offers recursion)`)
update.Flags().StringVar(&comment, "comment", "", "new comment")
update.Flags().StringVar(&step, "step", "", "the blocking step it starts at (normal, or a step above it)")
toggle := func(disable bool) *cobra.Command {
verb := map[bool]string{true: "disable", false: "enable"}[disable]
@@ -394,6 +402,8 @@ names below it (docs/blocking.md).`}
switch {
case m.Result == "none":
fmt.Fprintf(w, "%snot blocked\n", where)
case m.List != "" && m.Step != "":
fmt.Fprintf(w, "%sblocked for clients at step %s and above, by blocklist %s (entry %s); not for others\n", where, m.Step, m.List, m.Entry)
case m.List != "":
fmt.Fprintf(w, "%sblocked by blocklist %s (entry %s)\n", where, m.List, m.Entry)
default:
@@ -473,7 +483,7 @@ at once. "indefinitely" pauses until 'jarl block resume'.
},
}
cmd.AddCommand(addRule("block"), addRule("allow"), list, del, check, pause, resume)
cmd.AddCommand(addRule("block"), addRule("allow"), list, del, check, pause, resume, blockStepCmd(), blockGroupCmd())
return cmd
}