e4d014ba99
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
176 lines
4.9 KiB
Go
176 lines
4.9 KiB
Go
package agentd
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// writeACL 把 acl.json 写到指定路径。
|
|
func writeACL(t *testing.T, path, body string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
const validACL = `{
|
|
"enabled": true,
|
|
"allow_dp_uuids": ["uuid-me-1", "uuid-me-sub"],
|
|
"targets": [
|
|
{ "domain": ["brain.51yanmei.com", "git.51yanmei.com"] },
|
|
{ "ip_cidr": ["182.92.213.171/32"], "port": [5001, 3389, 10022, 10023] }
|
|
]
|
|
}`
|
|
|
|
func TestLoadACLConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
t.Run("文件不存在返回 nil,nil(未配置,不是错误)", func(t *testing.T) {
|
|
ac, err := LoadACLConfig(filepath.Join(dir, "missing.json"))
|
|
if err != nil {
|
|
t.Fatalf("want nil error, got %v", err)
|
|
}
|
|
if ac != nil {
|
|
t.Fatalf("want nil config, got %+v", ac)
|
|
}
|
|
})
|
|
|
|
t.Run("坏 JSON 返回 error(绝不静默降级)", func(t *testing.T) {
|
|
p := filepath.Join(dir, "bad.json")
|
|
writeACL(t, p, `{"enabled": true,`)
|
|
if _, err := LoadACLConfig(p); err == nil {
|
|
t.Fatal("want error for malformed JSON, got nil")
|
|
}
|
|
})
|
|
|
|
t.Run("合法配置解析出全部字段", func(t *testing.T) {
|
|
p := filepath.Join(dir, "acl.json")
|
|
writeACL(t, p, validACL)
|
|
ac, err := LoadACLConfig(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ac.Enabled {
|
|
t.Error("Enabled = false, want true")
|
|
}
|
|
if len(ac.AllowDpUUIDs) != 2 {
|
|
t.Errorf("AllowDpUUIDs len = %d, want 2", len(ac.AllowDpUUIDs))
|
|
}
|
|
if len(ac.Targets) != 2 {
|
|
t.Fatalf("Targets len = %d, want 2", len(ac.Targets))
|
|
}
|
|
if len(ac.Targets[0].Domain) != 2 {
|
|
t.Errorf("Targets[0].Domain len = %d, want 2", len(ac.Targets[0].Domain))
|
|
}
|
|
if len(ac.Targets[1].Port) != 4 {
|
|
t.Errorf("Targets[1].Port len = %d, want 4", len(ac.Targets[1].Port))
|
|
}
|
|
})
|
|
}
|
|
|
|
// active() 的语义与 WARP 相反:空白名单不等于「关闭」,而等于「谁都不许进」。
|
|
func TestACLActive_FailClosed(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
ac *ACLConfig
|
|
want bool
|
|
}{
|
|
{"nil 配置 → 未启用", nil, false},
|
|
{"enabled=false → 未启用(唯一的合法关闭途径)", &ACLConfig{
|
|
Enabled: false,
|
|
AllowDpUUIDs: []string{"u"},
|
|
Targets: []ACLTarget{{Domain: []string{"a.com"}}},
|
|
}, false},
|
|
{"无 target → 未启用(无从拒起)", &ACLConfig{
|
|
Enabled: true,
|
|
AllowDpUUIDs: []string{"u"},
|
|
}, false},
|
|
{"target 全为空条件 → 未启用", &ACLConfig{
|
|
Enabled: true,
|
|
Targets: []ACLTarget{{}},
|
|
}, false},
|
|
{"白名单为空但有 target → 仍启用(拒绝所有人)", &ACLConfig{
|
|
Enabled: true,
|
|
AllowDpUUIDs: nil,
|
|
Targets: []ACLTarget{{Domain: []string{"a.com"}}},
|
|
}, true},
|
|
{"完整配置 → 启用", &ACLConfig{
|
|
Enabled: true,
|
|
AllowDpUUIDs: []string{"u"},
|
|
Targets: []ACLTarget{{IPCIDR: []string{"1.2.3.4/32"}, Port: []int{443}}},
|
|
}, true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.ac.active(); got != tc.want {
|
|
t.Errorf("active() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestACLCleanHelpers(t *testing.T) {
|
|
ac := &ACLConfig{
|
|
Enabled: true,
|
|
AllowDpUUIDs: []string{" uuid-a ", "", "uuid-b"},
|
|
Targets: []ACLTarget{
|
|
{Domain: []string{" BRAIN.51yanmei.com ", ""}},
|
|
{},
|
|
{IPCIDR: []string{"1.2.3.4/32"}},
|
|
},
|
|
}
|
|
uuids := ac.cleanUUIDs()
|
|
if len(uuids) != 2 || uuids[0] != "uuid-a" || uuids[1] != "uuid-b" {
|
|
t.Errorf("cleanUUIDs() = %v, want [uuid-a uuid-b]", uuids)
|
|
}
|
|
targets := ac.cleanTargets()
|
|
if len(targets) != 2 {
|
|
t.Fatalf("cleanTargets() len = %d, want 2 (空 target 应被丢弃)", len(targets))
|
|
}
|
|
if targets[0].Domain[0] != "brain.51yanmei.com" {
|
|
t.Errorf("域名未规范化为小写去空白: %q", targets[0].Domain[0])
|
|
}
|
|
}
|
|
|
|
func TestACLTargetMatchFields(t *testing.T) {
|
|
tgt := ACLTarget{
|
|
Domain: []string{"a.com"},
|
|
IPCIDR: []string{"1.2.3.4/32"},
|
|
Port: []int{443, 5001},
|
|
}
|
|
m := tgt.matchFields()
|
|
if _, ok := m["domain"]; !ok {
|
|
t.Error("缺 domain 字段")
|
|
}
|
|
if _, ok := m["ip_cidr"]; !ok {
|
|
t.Error("缺 ip_cidr 字段")
|
|
}
|
|
if _, ok := m["port"]; !ok {
|
|
t.Error("缺 port 字段")
|
|
}
|
|
if _, ok := m["domain_suffix"]; ok {
|
|
t.Error("空的 domain_suffix 不应出现在输出里")
|
|
}
|
|
// matchFields 必须每次返回新 map,否则放行/拒绝两条规则会共享同一对象,
|
|
// 给其中一条加 "user"/"action" 会污染另一条。
|
|
m2 := tgt.matchFields()
|
|
m2["user"] = []string{"x"}
|
|
if _, ok := m["user"]; ok {
|
|
t.Error("matchFields 返回了共享 map,放行与拒绝规则会互相污染")
|
|
}
|
|
}
|
|
|
|
func TestConfigACLPaths(t *testing.T) {
|
|
c := Config{StateDir: "/etc/pangolin-agent"}.withDefaults()
|
|
if want := "/etc/pangolin-agent/acl.json"; c.ACLConfigPath != want {
|
|
t.Errorf("ACLConfigPath = %q, want %q", c.ACLConfigPath, want)
|
|
}
|
|
if want := "/etc/pangolin-agent/acl.last-good.json"; c.ACLLastGoodPath() != want {
|
|
t.Errorf("ACLLastGoodPath() = %q, want %q", c.ACLLastGoodPath(), want)
|
|
}
|
|
}
|