onesvm-browser-server/server/internal/policy/policy_test.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

186 lines
6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 应允许")
}
}