feat(waf): complete composable rule orchestration

Add the React Flow rule editor, ordered graph APIs and runtime DAG execution.\n\nPublish rules only on OpenResty reload and reconcile checksum-driven IP group snapshots in bounded shared memory.
This commit is contained in:
ryan
2026-07-13 14:16:55 +08:00
parent d36409fbf9
commit a1a997bcda
72 changed files with 5897 additions and 3080 deletions
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package protocol
import (
"encoding/json"
"fmt"
)
// MaxWAFIPGroupSnapshotBytes is the maximum serialized size accepted for the
// complete Agent/OpenResty WAF IP group runtime document.
const MaxWAFIPGroupSnapshotBytes = 20 << 20
type wafIPGroupSnapshot struct {
Groups map[string]WAFIPGroup `json:"groups"`
}
// MarshalWAFIPGroupSnapshot serializes the exact document written by the
// Agent to waf_ip_groups.json.
func MarshalWAFIPGroupSnapshot(groups map[string]WAFIPGroup) ([]byte, error) {
if groups == nil {
groups = map[string]WAFIPGroup{}
}
return json.Marshal(wafIPGroupSnapshot{Groups: groups})
}
// ValidateWAFIPGroupSnapshotSize rejects a complete runtime document that
// cannot be published safely to the OpenResty shared-memory snapshot.
func ValidateWAFIPGroupSnapshotSize(groups map[string]WAFIPGroup) error {
data, err := MarshalWAFIPGroupSnapshot(groups)
if err != nil {
return err
}
if len(data) > MaxWAFIPGroupSnapshotBytes {
return fmt.Errorf("WAF IP 组快照大小 %d 字节超过上限 %d 字节", len(data), MaxWAFIPGroupSnapshotBytes)
}
return nil
}
@@ -0,0 +1,57 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package protocol
import (
"strings"
"testing"
)
func TestMarshalWAFIPGroupSnapshotMatchesAgentRuntimeDocument(t *testing.T) {
data, err := MarshalWAFIPGroupSnapshot(map[string]WAFIPGroup{
"7": {ID: 7, Name: "deny", Type: "manual", Enabled: true, IPList: []string{"192.0.2.7"}, Checksum: "sum"},
})
if err != nil {
t.Fatalf("MarshalWAFIPGroupSnapshot failed: %v", err)
}
want := `{"groups":{"7":{"id":7,"name":"deny","type":"manual","enabled":true,"ip_list":["192.0.2.7"],"checksum":"sum"}}}`
if string(data) != want {
t.Fatalf("snapshot = %s, want %s", data, want)
}
}
func TestValidateWAFIPGroupSnapshotSizeBoundary(t *testing.T) {
groups := map[string]WAFIPGroup{
"1": {ID: 1, Type: "manual", Enabled: true, IPList: []string{"192.0.2.1"}, Checksum: strings.Repeat("a", 64)},
}
base, err := MarshalWAFIPGroupSnapshot(groups)
if err != nil {
t.Fatalf("marshal base snapshot: %v", err)
}
groups["1"] = WAFIPGroup{
ID: 1,
Name: strings.Repeat("x", MaxWAFIPGroupSnapshotBytes-len(base)),
Type: "manual",
Enabled: true,
IPList: []string{"192.0.2.1"},
Checksum: strings.Repeat("a", 64),
}
atLimit, err := MarshalWAFIPGroupSnapshot(groups)
if err != nil {
t.Fatalf("marshal boundary snapshot: %v", err)
}
if len(atLimit) != MaxWAFIPGroupSnapshotBytes {
t.Fatalf("boundary snapshot size = %d, want %d", len(atLimit), MaxWAFIPGroupSnapshotBytes)
}
if err := ValidateWAFIPGroupSnapshotSize(groups); err != nil {
t.Fatalf("boundary snapshot rejected: %v", err)
}
group := groups["1"]
group.Name += "x"
groups["1"] = group
if err := ValidateWAFIPGroupSnapshotSize(groups); err == nil {
t.Fatal("oversized snapshot was accepted")
}
}