feat(agent): ACL 渲染放行/拒绝规则对,空白名单仍拒绝
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,3 +141,38 @@ func cleanStrings(in []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// rules 产出 ACL 的 sing-box route 规则:每个 target 一对 —— 先放行白名单、再兜底拒绝。
|
||||
//
|
||||
// 顺序是安全性的一部分,不可重排:
|
||||
// 1. 全部放行规则排在全部拒绝规则之前。不能按 target 交错(放行A/拒绝A/放行B/拒绝B),
|
||||
// 因为 target 之间可能重叠,交错会让 B 的成员被 A 的拒绝规则先命中。
|
||||
// 2. 拒绝规则不带 user 维度 —— 它要对「白名单之外的所有人」生效。
|
||||
// 3. 同一 target 的放行与拒绝,目的地条件由同一个 matchFields() 生成,保证逐字相同。
|
||||
// 任何不对称都会造成「我自己也被拒」或「有人漏网」。
|
||||
//
|
||||
// 白名单为空时只产出拒绝规则(谁都不许进),这是 fail-closed 的核心:空名单的语义是
|
||||
// 「没有人」而非「所有人」。
|
||||
func (ac *ACLConfig) rules() []any {
|
||||
if !ac.active() {
|
||||
return nil
|
||||
}
|
||||
uuids := ac.cleanUUIDs()
|
||||
targets := ac.cleanTargets()
|
||||
|
||||
out := make([]any, 0, len(targets)*2)
|
||||
if len(uuids) > 0 {
|
||||
for _, t := range targets {
|
||||
r := t.matchFields()
|
||||
r["user"] = uuids
|
||||
r["outbound"] = directOutboundTag
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
for _, t := range targets {
|
||||
r := t.matchFields()
|
||||
r["action"] = "reject"
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package agentd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -173,3 +174,85 @@ func TestConfigACLPaths(t *testing.T) {
|
||||
t.Errorf("ACLLastGoodPath() = %q, want %q", c.ACLLastGoodPath(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// rules() 必须产出「先全部放行、再全部拒绝」,且同一 target 两侧目的地条件逐字相同。
|
||||
func TestACLRules_AllowThenDeny(t *testing.T) {
|
||||
ac := &ACLConfig{
|
||||
Enabled: true,
|
||||
AllowDpUUIDs: []string{"uuid-me"},
|
||||
Targets: []ACLTarget{
|
||||
{Domain: []string{"brain.51yanmei.com"}},
|
||||
{IPCIDR: []string{"182.92.213.171/32"}, Port: []int{5001}},
|
||||
},
|
||||
}
|
||||
rules := ac.rules()
|
||||
if len(rules) != 4 {
|
||||
t.Fatalf("规则数 = %d, want 4 (2 target × 放行+拒绝)", len(rules))
|
||||
}
|
||||
|
||||
// 前两条是放行:带 user + outbound,不带 action
|
||||
for i := 0; i < 2; i++ {
|
||||
r := rules[i].(map[string]any)
|
||||
if _, ok := r["user"]; !ok {
|
||||
t.Errorf("rules[%d] 放行规则缺 user", i)
|
||||
}
|
||||
if r["outbound"] != directOutboundTag {
|
||||
t.Errorf("rules[%d] outbound = %v, want %q", i, r["outbound"], directOutboundTag)
|
||||
}
|
||||
if _, ok := r["action"]; ok {
|
||||
t.Errorf("rules[%d] 放行规则不应带 action", i)
|
||||
}
|
||||
}
|
||||
// 后两条是拒绝:带 action=reject,不带 user(对所有人生效)
|
||||
for i := 2; i < 4; i++ {
|
||||
r := rules[i].(map[string]any)
|
||||
if r["action"] != "reject" {
|
||||
t.Errorf("rules[%d] action = %v, want reject", i, r["action"])
|
||||
}
|
||||
if _, ok := r["user"]; ok {
|
||||
t.Errorf("rules[%d] 拒绝规则不应带 user,否则会漏掉名单外的人", i)
|
||||
}
|
||||
}
|
||||
|
||||
// 对称性:target[0] 的放行(rules[0])与拒绝(rules[2])目的地条件必须逐字相同
|
||||
allow0 := rules[0].(map[string]any)
|
||||
deny0 := rules[2].(map[string]any)
|
||||
if fmt.Sprint(allow0["domain"]) != fmt.Sprint(deny0["domain"]) {
|
||||
t.Errorf("target0 放行/拒绝的 domain 不一致: %v vs %v", allow0["domain"], deny0["domain"])
|
||||
}
|
||||
allow1 := rules[1].(map[string]any)
|
||||
deny1 := rules[3].(map[string]any)
|
||||
if fmt.Sprint(allow1["ip_cidr"]) != fmt.Sprint(deny1["ip_cidr"]) ||
|
||||
fmt.Sprint(allow1["port"]) != fmt.Sprint(deny1["port"]) {
|
||||
t.Error("target1 放行/拒绝的 ip_cidr/port 不一致")
|
||||
}
|
||||
}
|
||||
|
||||
// 空白名单 → 不产出放行规则,但拒绝规则照出(fail-closed 的核心断言)。
|
||||
func TestACLRules_EmptyAllowlistStillDenies(t *testing.T) {
|
||||
ac := &ACLConfig{
|
||||
Enabled: true,
|
||||
AllowDpUUIDs: nil,
|
||||
Targets: []ACLTarget{{Domain: []string{"brain.51yanmei.com"}}},
|
||||
}
|
||||
rules := ac.rules()
|
||||
if len(rules) != 1 {
|
||||
t.Fatalf("规则数 = %d, want 1 (仅拒绝)", len(rules))
|
||||
}
|
||||
r := rules[0].(map[string]any)
|
||||
if r["action"] != "reject" {
|
||||
t.Errorf("action = %v, want reject", r["action"])
|
||||
}
|
||||
}
|
||||
|
||||
// 未 active(含 nil / enabled=false)→ 无规则。
|
||||
func TestACLRules_InactiveYieldsNil(t *testing.T) {
|
||||
var nilACL *ACLConfig
|
||||
if got := nilACL.rules(); got != nil {
|
||||
t.Errorf("nil ACL rules() = %v, want nil", got)
|
||||
}
|
||||
off := &ACLConfig{Enabled: false, Targets: []ACLTarget{{Domain: []string{"a.com"}}}}
|
||||
if got := off.rules(); got != nil {
|
||||
t.Errorf("enabled=false rules() = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user