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
+59
-7
@@ -150,14 +150,14 @@ func loadState(ctx context.Context, e execer) (*model.State, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err = e.QueryContext(ctx, `SELECT id, name, url, view_ids, disabled, comment, version, created_at, updated_at, created_by, updated_by FROM blocklists`)
|
||||
rows, err = e.QueryContext(ctx, `SELECT id, name, url, view_ids, disabled, step_id, comment, version, created_at, updated_at, created_by, updated_by FROM blocklists`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
b := &model.Blocklist{}
|
||||
var views, ca, ua string
|
||||
if err := rows.Scan(&b.ID, &b.Name, &b.URL, &views, &b.Disabled, &b.Comment, &b.Version, &ca, &ua, &b.CreatedBy, &b.UpdatedBy); err != nil {
|
||||
if err := rows.Scan(&b.ID, &b.Name, &b.URL, &views, &b.Disabled, &b.StepID, &b.Comment, &b.Version, &ca, &ua, &b.CreatedBy, &b.UpdatedBy); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,6 +187,44 @@ func loadState(ctx context.Context, e execer) (*model.State, error) {
|
||||
if err := closeRows(rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err = e.QueryContext(ctx, `SELECT id, name, position, comment, version, created_at, updated_at, created_by, updated_by FROM block_steps`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
s := &model.BlockStep{}
|
||||
var ca, ua string
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Position, &s.Comment, &s.Version, &ca, &ua, &s.CreatedBy, &s.UpdatedBy); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
s.CreatedAt, s.UpdatedAt = parseTS(ca), parseTS(ua)
|
||||
st.BlockSteps[s.ID] = s
|
||||
}
|
||||
if err := closeRows(rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err = e.QueryContext(ctx, `SELECT id, name, members, step_id, view_ids, comment, version, created_at, updated_at, created_by, updated_by FROM client_groups`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
g := &model.ClientGroup{}
|
||||
var members, views, ca, ua string
|
||||
if err := rows.Scan(&g.ID, &g.Name, &members, &g.StepID, &views, &g.Comment, &g.Version, &ca, &ua, &g.CreatedBy, &g.UpdatedBy); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
json.Unmarshal([]byte(members), &g.Members)
|
||||
json.Unmarshal([]byte(views), &g.ViewIDs)
|
||||
g.CreatedAt, g.UpdatedAt = parseTS(ca), parseTS(ua)
|
||||
st.ClientGroups[g.ID] = g
|
||||
}
|
||||
if err := closeRows(rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
@@ -204,7 +242,7 @@ func (t *Tx) SaveState(old, new *model.State) error {
|
||||
// ReplaceState replaces the stored configuration with st (used by import
|
||||
// and tests).
|
||||
func (t *Tx) ReplaceState(st *model.State) error {
|
||||
for _, tbl := range []string{"views", "acls", "tsig_keys", "zones", "records", "options", "blocklists", "block_rules"} {
|
||||
for _, tbl := range []string{"views", "acls", "tsig_keys", "zones", "records", "options", "blocklists", "block_rules", "block_steps", "client_groups"} {
|
||||
if _, err := t.tx.ExecContext(t.ctx, "DELETE FROM "+tbl); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -272,17 +310,31 @@ func saveState(ctx context.Context, e execer, old, new *model.State) error {
|
||||
return err
|
||||
}
|
||||
if err := diffMap(old.Blocklists, new.Blocklists, func(b *model.Blocklist) error {
|
||||
_, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO blocklists (id, name, url, view_ids, disabled, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
b.ID, b.Name, b.URL, jsonText(nonNilIDs(b.ViewIDs)), b.Disabled, b.Comment, b.Version, ts(b.CreatedAt), ts(b.UpdatedAt), b.CreatedBy, b.UpdatedBy)
|
||||
_, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO blocklists (id, name, url, view_ids, disabled, step_id, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
b.ID, b.Name, b.URL, jsonText(nonNilIDs(b.ViewIDs)), b.Disabled, b.StepID, b.Comment, b.Version, ts(b.CreatedAt), ts(b.UpdatedAt), b.CreatedBy, b.UpdatedBy)
|
||||
return err
|
||||
}, func(id int64) error { return del(ctx, e, "blocklists", id) }); err != nil {
|
||||
return err
|
||||
}
|
||||
return diffMap(old.BlockRules, new.BlockRules, func(r *model.BlockRule) error {
|
||||
if err := diffMap(old.BlockRules, new.BlockRules, func(r *model.BlockRule) error {
|
||||
_, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO block_rules (id, name, action, exact, view_ids, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
r.ID, r.Name, r.Action, r.Exact, jsonText(nonNilIDs(r.ViewIDs)), r.Comment, r.Version, ts(r.CreatedAt), ts(r.UpdatedAt), r.CreatedBy, r.UpdatedBy)
|
||||
return err
|
||||
}, func(id int64) error { return del(ctx, e, "block_rules", id) })
|
||||
}, func(id int64) error { return del(ctx, e, "block_rules", id) }); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := diffMap(old.BlockSteps, new.BlockSteps, func(s *model.BlockStep) error {
|
||||
_, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO block_steps (id, name, position, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||
s.ID, s.Name, s.Position, s.Comment, s.Version, ts(s.CreatedAt), ts(s.UpdatedAt), s.CreatedBy, s.UpdatedBy)
|
||||
return err
|
||||
}, func(id int64) error { return del(ctx, e, "block_steps", id) }); err != nil {
|
||||
return err
|
||||
}
|
||||
return diffMap(old.ClientGroups, new.ClientGroups, func(g *model.ClientGroup) error {
|
||||
_, err := e.ExecContext(ctx, `INSERT OR REPLACE INTO client_groups (id, name, members, step_id, view_ids, comment, version, created_at, updated_at, created_by, updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
g.ID, g.Name, jsonText(nonNil(g.Members)), g.StepID, jsonText(nonNilIDs(g.ViewIDs)), g.Comment, g.Version, ts(g.CreatedAt), ts(g.UpdatedAt), g.CreatedBy, g.UpdatedBy)
|
||||
return err
|
||||
}, func(id int64) error { return del(ctx, e, "client_groups", id) })
|
||||
}
|
||||
|
||||
func nonNilIDs(ids []int64) []int64 {
|
||||
|
||||
Reference in new issue
Block a user