单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。 Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
6 KiB
Go
186 lines
6 KiB
Go
// policy_test.go:SSRF 私网判定 / 重定向重验 / 域名 trie / robots 解析(A6.1 关键路径)。
|
||
// 全部测试不用外部网络(DNS 判定用注入 resolver 或字面量 IP)。
|
||
package policy
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
)
|
||
|
||
// TestCheckURLSchemeProtocols 协议/端口白名单。
|
||
func TestCheckURLSchemePorts(t *testing.T) {
|
||
g := &Guard{}
|
||
cases := []struct {
|
||
url string
|
||
bad bool
|
||
rule string
|
||
}{
|
||
{"https://example.com", false, ""},
|
||
{"http://example.com", false, ""},
|
||
{"http://example.com:80/x", false, ""},
|
||
{"https://example.com:443/x", false, ""},
|
||
{"http://example.com:8080/x", true, "ssrf_port"},
|
||
{"file:///etc/passwd", true, "ssrf_scheme"},
|
||
{"ftp://example.com", true, "ssrf_scheme"},
|
||
{"gopher://example.com", true, "ssrf_scheme"},
|
||
{"http://localhost/x", true, "ssrf_private_ip"},
|
||
{"http://metadata.google.internal/x", true, "ssrf_metadata_host"},
|
||
{"http://foo.internal/x", true, "ssrf_metadata_host"},
|
||
{"http://kubernetes.default.svc/x", true, "ssrf_metadata_host"},
|
||
{"http://127.0.0.1/x", true, "ssrf_private_ip"},
|
||
{"http://10.1.2.3/x", true, "ssrf_private_ip"},
|
||
{"http://192.168.1.1/x", true, "ssrf_private_ip"},
|
||
{"http://172.16.0.9/x", true, "ssrf_private_ip"},
|
||
{"http://169.254.169.254/latest/meta-data/", true, "ssrf_private_ip"},
|
||
{"http://0.0.0.0/x", true, "ssrf_private_ip"},
|
||
{"http://[::1]/x", true, "ssrf_private_ip"},
|
||
{"http://[fe80::1]/x", true, "ssrf_private_ip"},
|
||
{"http://[fc00::1]/x", true, "ssrf_private_ip"},
|
||
}
|
||
for _, c := range cases {
|
||
d := g.CheckURL(c.url)
|
||
if c.bad && d == nil {
|
||
t.Errorf("%s 应被拒", c.url)
|
||
}
|
||
if !c.bad && d != nil {
|
||
t.Errorf("%s 不应被拒: %v", c.url, d)
|
||
}
|
||
if c.bad && d != nil && c.rule != "" && d.RuleID != c.rule {
|
||
t.Errorf("%s rule 期望 %s 得 %s", c.url, c.rule, d.RuleID)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestResolveCheckPrivateDNS 域名解析到私网 IP 必须拦。
|
||
// DNS 通路不依赖外部网络:字面量 IP 路径 + CheckHost 路径在本测试覆盖,
|
||
// fake resolver 的 DNS 通路在集成测试覆盖(此处不留未用变量)。
|
||
func TestResolveCheckPrivateDNS(t *testing.T) {
|
||
g := &Guard{}
|
||
if d := g.CheckHost("192.168.1.61"); d == nil {
|
||
t.Fatal("私网字面量应拒")
|
||
}
|
||
if d := g.CheckHost("example.com"); d != nil {
|
||
t.Fatalf("普通域名不应拒: %v", d)
|
||
}
|
||
}
|
||
|
||
// TestRedirectRecheck 每跳重验(含 URL 与 host 校验)。
|
||
func TestRedirectRecheck(t *testing.T) {
|
||
g := &Guard{}
|
||
if d := g.CheckRedirect(context.Background(), "http://10.0.0.5/next"); d == nil {
|
||
t.Fatal("重定向到私网应拒")
|
||
}
|
||
if d := g.CheckRedirect(context.Background(), "file:///etc/passwd"); d == nil {
|
||
t.Fatal("重定向到 file 应拒")
|
||
}
|
||
if d := g.CheckRedirect(context.Background(), "http://169.254.169.254/x"); d == nil {
|
||
t.Fatal("重定向到 metadata 应拒")
|
||
}
|
||
}
|
||
|
||
// TestTrieLookup 后缀匹配语义。
|
||
func TestTrieLookup(t *testing.T) {
|
||
tr := NewDomainTrie()
|
||
_ = tr.insertStub()
|
||
if a, ok := tr.Lookup("api.example.com"); !ok || a != "direct" {
|
||
t.Fatalf("example.com 应命中 direct: a=%s ok=%v", a, ok)
|
||
}
|
||
if a, ok := tr.Lookup("example.com"); !ok || a != "direct" {
|
||
t.Fatalf("裸域应命中(插入形态不含前导点)")
|
||
}
|
||
if _, ok := tr.Lookup("notexample.com"); ok {
|
||
t.Fatal("非后缀不应命中")
|
||
}
|
||
if a, ok := tr.Lookup("deep.sub.example.com"); !ok || a != "direct" {
|
||
t.Fatalf("深层子域应命中")
|
||
}
|
||
// deny 优先:最长后缀
|
||
tr.Insert(".example.com", "direct")
|
||
tr.Insert("deny.example.com", "deny")
|
||
if a, _ := tr.Lookup("deny.example.com"); a != "deny" {
|
||
t.Fatalf("更长后缀应胜出: %s", a)
|
||
}
|
||
if a, _ := tr.Lookup("other.example.com"); a != "direct" {
|
||
t.Fatalf("未命中 deny 走 direct: %s", a)
|
||
}
|
||
if !tr.IsDenied("a.deny.example.com") {
|
||
t.Fatal("IsDenied 应真")
|
||
}
|
||
}
|
||
|
||
// TestTrieLoadFromStore 从 SQLite 加载。
|
||
func TestTrieLoadFromStore(t *testing.T) {
|
||
db := openPolicyDB(t)
|
||
defer db.Close()
|
||
if err := db.RuleUpsert("suffix", ".internal.example", "deny", 1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := db.RuleUpsert("suffix", "onesvm.com", "direct", 10); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tr := NewDomainTrie()
|
||
n, err := tr.LoadFromStore(db)
|
||
if err != nil || n != 2 {
|
||
t.Fatalf("加载 2 条: n=%d err=%v", n, err)
|
||
}
|
||
gen1 := tr.Generation()
|
||
// 热载
|
||
if err := db.RuleUpsert("suffix", ".blocked.cn", "deny", 2); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := tr.LoadFromStore(db); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if tr.Generation() != gen1+1 {
|
||
t.Fatal("热载应递增代次")
|
||
}
|
||
if !tr.IsDenied("x.blocked.cn") {
|
||
t.Fatal("热载后新规则应生效")
|
||
}
|
||
}
|
||
|
||
// TestRobotsMatch robots 解析:UA 段 + 路径级判定(ITER-3 FIX-4:matchRobots 带 path,
|
||
// 走 AllowedPath 最长匹配语义)。
|
||
func TestRobotsMatch(t *testing.T) {
|
||
body := `User-agent: *
|
||
Disallow: /private/
|
||
Allow: /private/ok/
|
||
Disallow: /
|
||
`
|
||
// 「Disallow: /」全站禁(无更长匹配时)。
|
||
if matchRobots(body, "onesvm-browser-server", "/anything") {
|
||
t.Fatal("命中 Disallow: /(最长匹配)应禁")
|
||
}
|
||
// 路径级:/private/ 禁、/private/ok/ 放行(Allow 更长胜)、/public 放行(更长规则不匹配该前缀)。
|
||
if AllowedPath(body, "/private/secret") {
|
||
t.Fatal("/private/ 前缀应禁")
|
||
}
|
||
if !AllowedPath(body, "/private/ok/page") {
|
||
t.Fatal("Allow 同长/更长应胜")
|
||
}
|
||
if AllowedPath(body, "/public") {
|
||
t.Fatal("/public 命中 Disallow: / 应禁")
|
||
}
|
||
if !AllowedPath(body, "/private/ok/") {
|
||
t.Fatal("/private/ok/ 精确命中 Allow 应放行")
|
||
}
|
||
}
|
||
|
||
// TestRobotsAllowedPathLevel ITER-3 FIX-4 端到端:Allowed(缓存路径)按 path 判定——
|
||
// Disallow: /private/ 时 /private/data 拒、/public 放行。
|
||
func TestRobotsAllowedPathLevel(t *testing.T) {
|
||
body := "User-agent: *\nDisallow: /private/\n"
|
||
if matchRobots(body, "ua", "/private/data") {
|
||
t.Fatal("/private/data 应拒")
|
||
}
|
||
if !matchRobots(body, "ua", "/public") {
|
||
t.Fatal("/public 应放行")
|
||
}
|
||
}
|
||
|
||
// TestRobotsAllowEmpty 空 robots 允许。
|
||
func TestRobotsAllowEmpty(t *testing.T) {
|
||
if !matchRobots("", "ua", "/x") {
|
||
t.Fatal("空 robots 应允许")
|
||
}
|
||
}
|