Skip to content

Commit 600c0ef

Browse files
committed
feat(rules): add priority field and configurable evaluation mode (#1460)
Add support for explicit rule ordering and configurable evaluation logic: 1. **Priority field**: Rules now have a `priority` field (int, default 0). Lower values = higher priority. Rules are sorted by priority first, then alphabetically by name. This replaces the implicit file naming convention (000-, 001-, etc.) with explicit control. 2. **Evaluation modes**: New config option `Rules.EvaluationMode`: - `deny-priority` (default): Current behavior where deny/reject rules always win over allow rules, regardless of order - `first-match`: RouterOS-style evaluation where the first matching rule wins, giving full control via priority ordering Example config: ```json "Rules": { "Path": "/etc/opensnitchd/rules/", "EnableChecksums": false, "EvaluationMode": "first-match" } ``` Example rule with priority: ```json { "name": "block-malware", "priority": -100, "action": "deny", ... } ``` This provides more intuitive and flexible rule ordering while maintaining backwards compatibility with existing configurations.
1 parent e3cf78c commit 600c0ef

File tree

5 files changed

+60
-5
lines changed

5 files changed

+60
-5
lines changed

daemon/data/default-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
},
3131
"Rules": {
3232
"Path": "/etc/opensnitchd/rules/",
33-
"EnableChecksums": false
33+
"EnableChecksums": false,
34+
"EvaluationMode": "deny-priority"
3435
},
3536
"Ebpf": {
3637
"EventsWorkers": 8,

daemon/rule/loader.go

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Loader struct {
3030
liveReload bool
3131
liveReloadRunning bool
3232
checkSums bool
33+
evaluationMode EvaluationMode
3334
stopLiveReload chan struct{}
3435

3536
sync.RWMutex
@@ -48,10 +49,27 @@ func NewLoader(liveReload bool) (*Loader, error) {
4849
liveReload: liveReload,
4950
watcher: watcher,
5051
liveReloadRunning: false,
52+
evaluationMode: EvalDenyPriority,
5153
stopLiveReload: make(chan struct{}),
5254
}, nil
5355
}
5456

57+
// SetEvaluationMode sets the rule evaluation mode.
58+
// EvalDenyPriority (default): deny/reject rules always win over allow rules.
59+
// EvalFirstMatch: first matching rule wins (RouterOS-style).
60+
func (l *Loader) SetEvaluationMode(mode string) {
61+
l.Lock()
62+
defer l.Unlock()
63+
switch mode {
64+
case string(EvalFirstMatch):
65+
l.evaluationMode = EvalFirstMatch
66+
log.Info("[rules] Evaluation mode: first-match (first matching rule wins)")
67+
default:
68+
l.evaluationMode = EvalDenyPriority
69+
log.Info("[rules] Evaluation mode: deny-priority (deny always wins)")
70+
}
71+
}
72+
5573
// NumRules returns he number of loaded rules.
5674
func (l *Loader) NumRules() int {
5775
l.RLock()
@@ -374,7 +392,16 @@ func (l *Loader) sortRules() {
374392
}
375393
l.activeRules = append(l.activeRules, k)
376394
}
377-
sort.Strings(l.activeRules)
395+
// Sort by priority first (lower = higher priority), then alphabetically by name.
396+
// This allows explicit control over rule evaluation order via the Priority field.
397+
sort.Slice(l.activeRules, func(i, j int) bool {
398+
ri := l.rules[l.activeRules[i]]
399+
rj := l.rules[l.activeRules[j]]
400+
if ri.Priority != rj.Priority {
401+
return ri.Priority < rj.Priority
402+
}
403+
return l.activeRules[i] < l.activeRules[j]
404+
})
378405
}
379406

380407
func (l *Loader) addUserRule(rule *Rule) {
@@ -494,17 +521,25 @@ Exit:
494521
}
495522

496523
// FindFirstMatch will try match the connection against the existing rule set.
524+
// The behavior depends on the evaluation mode:
525+
// - EvalDenyPriority (default): deny/reject rules always win over allow rules.
526+
// - EvalFirstMatch: first matching rule wins (RouterOS-style).
497527
func (l *Loader) FindFirstMatch(con *conman.Connection) (match *Rule) {
498528
l.RLock()
499529
defer l.RUnlock()
500530

501531
for _, idx := range l.activeRules {
502532
rule, _ := l.rules[idx]
503533
if rule.Match(con, l.checkSums) {
504-
// We have a match.
505-
// Save the rule in order to don't ask the user to take action,
506-
// and keep iterating until a Deny or a Priority rule appears.
507534
match = rule
535+
536+
// In first-match mode, return immediately on any match
537+
if l.evaluationMode == EvalFirstMatch {
538+
return rule
539+
}
540+
541+
// In deny-priority mode (default), keep iterating until a
542+
// Deny/Reject or Precedence rule appears
508543
if rule.Action == Reject || rule.Action == Deny || rule.Precedence == true {
509544
return rule
510545
}

daemon/rule/rule.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ const (
3434
Always = Duration("always")
3535
)
3636

37+
// EvaluationMode determines how rules are evaluated when multiple match
38+
type EvaluationMode string
39+
40+
const (
41+
// EvalDenyPriority is the default mode: deny/reject rules always win over allow
42+
EvalDenyPriority = EvaluationMode("deny-priority")
43+
// EvalFirstMatch uses RouterOS-style evaluation: first matching rule wins
44+
EvalFirstMatch = EvaluationMode("first-match")
45+
)
46+
3747
// Rule represents an action on a connection.
3848
// The fields match the ones saved as json to disk.
3949
// If a .json rule file is modified on disk, it's reloaded automatically.
@@ -50,6 +60,10 @@ type Rule struct {
5060
Enabled bool `json:"enabled"`
5161
Precedence bool `json:"precedence"`
5262
Nolog bool `json:"nolog"`
63+
// Priority determines rule evaluation order (lower = higher priority).
64+
// Rules with equal priority are sorted alphabetically by name.
65+
// Default is 0. Use negative values for higher priority rules.
66+
Priority int `json:"priority"`
5367
}
5468

5569
// Create creates a new rule object with the specified parameters.

daemon/ui/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ type (
5252
RulesOptions struct {
5353
Path string `json:"Path"`
5454
EnableChecksums bool `json:"EnableChecksums"`
55+
// EvaluationMode determines how rules are matched:
56+
// - "deny-priority" (default): deny/reject rules always win over allow
57+
// - "first-match": first matching rule wins (RouterOS-style)
58+
EvaluationMode string `json:"EvaluationMode"`
5559
}
5660

5761
// FwOptions struct

daemon/ui/config_utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ func (c *Client) reloadConfiguration(reload bool, newConfig *config.Config) (err
196196

197197
// 1. load rules
198198
c.rules.EnableChecksums(newConfig.Rules.EnableChecksums)
199+
c.rules.SetEvaluationMode(newConfig.Rules.EvaluationMode)
199200
if newConfig.Rules.Path == "" || c.config.Rules.Path != newConfig.Rules.Path {
200201
c.rules.Reload(newConfig.Rules.Path)
201202
log.Debug("[config] reloading config.rules.path, old: <%s> new: <%s>", c.config.Rules.Path, newConfig.Rules.Path)

0 commit comments

Comments
 (0)