eb7c3c1062
节点 sing-box 渲染新增可选 WARP 分流:节点本地 warp.json(默认 <StateDir>/warp.json)配 WARP 凭证 + 域名清单 → 渲染时注入一个 userspace WireGuard(WARP)endpoint + route(sniff 取 SNI/Host → domain_suffix 命中走 warp,其余 final=direct)。sing-box 1.11+ endpoints 语法,system=false 用户态 不依赖内核 wg 模块。 - 运营改域名只需编辑 warp.json + 重启 agent(sing-box 无热重载),即「配置的方式」。 - warp.json 不存在/enabled=false/域名空/凭证缺/坏 JSON → 一律按未启用,配置与旧 节点逐字节一致,坏配置绝不产出无法启动的 sing-box config(渲染读失败仅记日志)。 - WARP 凭证节点私有(wgcf 注册免费匿名账号),不入 git、不经控制面。 - 测试:注入 endpoint+route/无配置无 route/禁用或残缺不注入/坏 JSON 优雅退化。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package agentd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
|
|
)
|
|
|
|
// writeWarp 把 warp.json 写到 cfg 的 WarpConfigPath。
|
|
func writeWarp(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 validWarp = `{
|
|
"enabled": true,
|
|
"private_key": "aW52YWxpZC1rZXk=",
|
|
"peer_public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=",
|
|
"endpoint": "162.159.192.1:2408",
|
|
"address": ["172.16.0.2/32", "2606:4700:110:8abc::/128"],
|
|
"reserved": [1, 2, 3],
|
|
"mtu": 1280,
|
|
"domains": ["reddit.com", "redd.it"]
|
|
}`
|
|
|
|
// 无 warp.json → 配置里既无 endpoints 也无 route(向后兼容,与旧节点逐字节一致)。
|
|
func TestRender_NoWarp_NoRouteSection(t *testing.T) {
|
|
sb := NewSingBox(testConfig(t), nil)
|
|
sb.ApplyConfig(sampleSnapshot(&agentv1.Credential{DpUUID: "aaaa", Protocol: agentv1.ProtocolBoth}), true)
|
|
data, err := sb.RenderConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := cfg["endpoints"]; ok {
|
|
t.Error("no warp.json but endpoints present")
|
|
}
|
|
if _, ok := cfg["route"]; ok {
|
|
t.Error("no warp.json but route present")
|
|
}
|
|
}
|
|
|
|
// 有效 warp.json → 注入 WireGuard endpoint(tag=warp,userspace)+ 域名分流 route。
|
|
func TestRender_Warp_InjectsEndpointAndRoute(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
writeWarp(t, cfg.WarpConfigPath, validWarp)
|
|
sb := NewSingBox(cfg, nil)
|
|
sb.ApplyConfig(sampleSnapshot(&agentv1.Credential{DpUUID: "aaaa", Protocol: agentv1.ProtocolBoth}), true)
|
|
|
|
data, err := sb.RenderConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
t.Fatalf("rendered config invalid JSON: %v", err)
|
|
}
|
|
|
|
eps, ok := m["endpoints"].([]any)
|
|
if !ok || len(eps) != 1 {
|
|
t.Fatalf("want 1 endpoint, got %v", m["endpoints"])
|
|
}
|
|
ep := eps[0].(map[string]any)
|
|
if ep["type"] != "wireguard" || ep["tag"] != "warp" {
|
|
t.Errorf("endpoint type/tag wrong: %v", ep)
|
|
}
|
|
if ep["system"] != false {
|
|
t.Errorf("WARP endpoint must be userspace (system=false), got %v", ep["system"])
|
|
}
|
|
peers := ep["peers"].([]any)
|
|
peer := peers[0].(map[string]any)
|
|
if peer["public_key"] != "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=" {
|
|
t.Errorf("peer public_key wrong: %v", peer["public_key"])
|
|
}
|
|
if peer["address"] != "162.159.192.1" {
|
|
t.Errorf("peer address wrong: %v", peer["address"])
|
|
}
|
|
|
|
route := m["route"].(map[string]any)
|
|
if route["final"] != "direct" {
|
|
t.Errorf("route.final = %v, want direct", route["final"])
|
|
}
|
|
rules := route["rules"].([]any)
|
|
// 首条必须是 sniff(否则客户端发来的已解析 IP 无域名可匹配)。
|
|
if rules[0].(map[string]any)["action"] != "sniff" {
|
|
t.Errorf("first route rule must be sniff, got %v", rules[0])
|
|
}
|
|
last := rules[len(rules)-1].(map[string]any)
|
|
if last["outbound"] != "warp" {
|
|
t.Errorf("domain rule must route to warp, got %v", last)
|
|
}
|
|
if !strings.Contains(string(data), "reddit.com") {
|
|
t.Error("configured domain reddit.com not in route")
|
|
}
|
|
}
|
|
|
|
// enabled=false 或域名为空 → 视为未启用,不注入(坏配置宁可全直连)。
|
|
func TestRender_Warp_DisabledOrIncomplete(t *testing.T) {
|
|
cases := map[string]string{
|
|
"disabled": strings.Replace(validWarp, `"enabled": true`, `"enabled": false`, 1),
|
|
"no-domains": strings.Replace(validWarp, `["reddit.com", "redd.it"]`, `[]`, 1),
|
|
"no-key": strings.Replace(validWarp, `"private_key": "aW52YWxpZC1rZXk=",`, `"private_key": "",`, 1),
|
|
}
|
|
for name, body := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
writeWarp(t, cfg.WarpConfigPath, body)
|
|
sb := NewSingBox(cfg, nil)
|
|
sb.ApplyConfig(sampleSnapshot(&agentv1.Credential{DpUUID: "aaaa", Protocol: agentv1.ProtocolBoth}), true)
|
|
data, err := sb.RenderConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
_ = json.Unmarshal(data, &m)
|
|
if _, ok := m["route"]; ok {
|
|
t.Errorf("%s: route must be absent", name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 坏 JSON → 渲染不报错、按未启用处理(不产出无法启动的配置)。
|
|
func TestRender_Warp_BadJSONDegradesGracefully(t *testing.T) {
|
|
cfg := testConfig(t)
|
|
writeWarp(t, cfg.WarpConfigPath, `{ this is not json `)
|
|
sb := NewSingBox(cfg, nil)
|
|
sb.ApplyConfig(sampleSnapshot(&agentv1.Credential{DpUUID: "aaaa", Protocol: agentv1.ProtocolBoth}), true)
|
|
data, err := sb.RenderConfig()
|
|
if err != nil {
|
|
t.Fatalf("bad warp.json must not fail render: %v", err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := m["route"]; ok {
|
|
t.Error("bad warp.json must degrade to no route")
|
|
}
|
|
}
|