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>
225 lines
5.9 KiB
Go
225 lines
5.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"jarl/internal/catalog"
|
|
"jarl/internal/dnsname"
|
|
"jarl/internal/model"
|
|
"jarl/internal/namedconf"
|
|
)
|
|
|
|
type refKind int
|
|
|
|
const (
|
|
refACL refKind = iota
|
|
refKey
|
|
)
|
|
|
|
func sameRef(kind refKind, a, b string) bool {
|
|
if kind == refKey {
|
|
return model.NormalizeKeyName(a) == model.NormalizeKeyName(b)
|
|
}
|
|
return strings.EqualFold(a, b)
|
|
}
|
|
|
|
// rewriteElements renames references in ACL-style element strings. It
|
|
// returns the new list and whether anything matched.
|
|
func rewriteElements(list []string, kind refKind, old, new string) ([]string, bool) {
|
|
changed := false
|
|
var fix func(es []namedconf.AMElement) []namedconf.AMElement
|
|
fix = func(es []namedconf.AMElement) []namedconf.AMElement {
|
|
for i := range es {
|
|
e := &es[i]
|
|
switch {
|
|
case kind == refACL && e.Kind == namedconf.AMName && !e.IsBuiltin() && sameRef(kind, e.Name, old):
|
|
e.Name, changed = new, true
|
|
case kind == refKey && e.Kind == namedconf.AMKey && sameRef(kind, e.Name, old):
|
|
e.Name, changed = new, true
|
|
case e.Kind == namedconf.AMNested:
|
|
e.Nested = fix(e.Nested)
|
|
}
|
|
}
|
|
return es
|
|
}
|
|
out := make([]string, len(list))
|
|
for i, s := range list {
|
|
e, err := namedconf.ParseAMElement(s)
|
|
if err != nil {
|
|
out[i] = s
|
|
continue
|
|
}
|
|
res := fix([]namedconf.AMElement{e})
|
|
out[i] = res[0].String()
|
|
}
|
|
return out, changed
|
|
}
|
|
|
|
// walkRefs calls fn for every reference of the given kind in items. fn may
|
|
// change the value's text.
|
|
func walkRefs(items []namedconf.Item, kind refKind, aml bool, fn func(v *namedconf.Value)) {
|
|
for i := 0; i < len(items); i++ {
|
|
it := items[i]
|
|
if it.Value != nil && !it.Value.Quoted && i+1 < len(items) {
|
|
switch strings.ToLower(it.Value.Text) {
|
|
case "key":
|
|
if kind == refKey && items[i+1].Value != nil {
|
|
fn(items[i+1].Value)
|
|
}
|
|
i++
|
|
continue
|
|
case "keys":
|
|
next := items[i+1]
|
|
if kind == refKey {
|
|
if next.Value != nil {
|
|
fn(next.Value)
|
|
} else if next.Block != nil {
|
|
for _, s := range next.Block.Stmts {
|
|
if len(s.Items) == 1 && s.Items[0].Value != nil {
|
|
fn(s.Items[0].Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
if it.Block == nil {
|
|
continue
|
|
}
|
|
for _, s := range it.Block.Stmts {
|
|
if kind == refACL && aml {
|
|
var v *namedconf.Value
|
|
switch {
|
|
case len(s.Items) == 1 && s.Items[0].Value != nil:
|
|
v = s.Items[0].Value
|
|
case len(s.Items) == 2 && s.Items[0].Bang && s.Items[1].Value != nil:
|
|
v = s.Items[1].Value
|
|
}
|
|
if v != nil && !namedconf.BuiltinACLs[strings.ToLower(v.Text)] {
|
|
fn(v)
|
|
continue
|
|
}
|
|
}
|
|
walkRefs(s.Items, kind, aml, fn)
|
|
}
|
|
}
|
|
}
|
|
|
|
// optionHasAML reports whether the option's grammar contains address match
|
|
// lists (so single words in its blocks are ACL names).
|
|
func optionHasAML(st *model.State, o *model.Option) bool {
|
|
cs := catalogScope(st, o.Scope, o.ScopeID)
|
|
stmt, ok := catalog.Default().Lookup(cs, o.Name)
|
|
return ok && strings.Contains(stmt.Grammar, "<address_match_element>")
|
|
}
|
|
|
|
func optionLabel(st *model.State, o *model.Option) string {
|
|
switch o.Scope {
|
|
case model.ScopeView:
|
|
if v := st.Views[o.ScopeID]; v != nil {
|
|
return fmt.Sprintf("view %s option %s", v.Name, o.Name)
|
|
}
|
|
case model.ScopeZone:
|
|
if z := st.Zones[o.ScopeID]; z != nil {
|
|
label := "zone " + dnsname.Display(z.Name)
|
|
if v := st.ViewName(z); v != "" {
|
|
label += " [view " + v + "]"
|
|
}
|
|
return label + " option " + o.Name
|
|
}
|
|
case model.ScopeTopLevel:
|
|
return "top-level statement " + o.Name
|
|
}
|
|
return "server option " + o.Name
|
|
}
|
|
|
|
// FindRefs lists the objects that refer to an ACL or key by name.
|
|
func FindRefs(st *model.State, kind refKind, name string) []string {
|
|
var out []string
|
|
for _, a := range st.SortedACLs() {
|
|
if _, hit := rewriteElements(a.Elements, kind, name, name); hit {
|
|
out = append(out, "ACL "+a.Name)
|
|
}
|
|
}
|
|
for _, v := range st.OrderedViews() {
|
|
_, h1 := rewriteElements(v.MatchClients, kind, name, name)
|
|
_, h2 := rewriteElements(v.MatchDestinations, kind, name, name)
|
|
if h1 || h2 {
|
|
out = append(out, "view "+v.Name)
|
|
}
|
|
}
|
|
for _, g := range st.SortedClientGroups() {
|
|
if _, hit := rewriteElements(g.Members, kind, name, name); hit {
|
|
out = append(out, "client group "+g.Name)
|
|
}
|
|
}
|
|
var opts []string
|
|
for _, o := range st.Options {
|
|
stmt, err := namedconf.ParseOptionValue(o.Name, o.Value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hit := false
|
|
walkRefs(stmt.Items, kind, optionHasAML(st, o), func(v *namedconf.Value) {
|
|
if sameRef(kind, v.Text, name) {
|
|
hit = true
|
|
}
|
|
})
|
|
if hit {
|
|
opts = append(opts, optionLabel(st, o))
|
|
}
|
|
}
|
|
sort.Strings(opts)
|
|
return append(out, opts...)
|
|
}
|
|
|
|
// renameRefs rewrites references to an ACL or key after a rename.
|
|
func renameRefs(st *model.State, kind refKind, old, new string, c *applyCtx) {
|
|
for _, a := range st.ACLs {
|
|
if l, hit := rewriteElements(a.Elements, kind, old, new); hit {
|
|
a.Elements = l
|
|
a.Touch(c.user(), c.now)
|
|
}
|
|
}
|
|
for _, v := range st.Views {
|
|
l1, h1 := rewriteElements(v.MatchClients, kind, old, new)
|
|
l2, h2 := rewriteElements(v.MatchDestinations, kind, old, new)
|
|
if h1 || h2 {
|
|
v.MatchClients, v.MatchDestinations = l1, l2
|
|
v.Touch(c.user(), c.now)
|
|
}
|
|
}
|
|
for _, g := range st.ClientGroups {
|
|
if l, hit := rewriteElements(g.Members, kind, old, new); hit {
|
|
g.Members = l
|
|
g.Touch(c.user(), c.now)
|
|
}
|
|
}
|
|
for _, o := range st.Options {
|
|
stmt, err := namedconf.ParseOptionValue(o.Name, o.Value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
hit := false
|
|
walkRefs(stmt.Items, kind, optionHasAML(st, o), func(v *namedconf.Value) {
|
|
if sameRef(kind, v.Text, old) {
|
|
v.Text, hit = new, true
|
|
}
|
|
})
|
|
if hit {
|
|
o.Value = namedconf.Compact(stmt.Items[1:])
|
|
o.Touch(c.user(), c.now)
|
|
}
|
|
}
|
|
}
|
|
|
|
// FindACLRefs lists the objects that use the ACL called name.
|
|
func FindACLRefs(st *model.State, name string) []string { return FindRefs(st, refACL, name) }
|
|
|
|
// FindKeyRefs lists the objects that use the TSIG key called name.
|
|
func FindKeyRefs(st *model.State, name string) []string { return FindRefs(st, refKey, name) }
|