Files
pangolin/server/internal/httpapi/clientconfig_test.go
T
wangjia bc890974c0 fix(server): 补 #5 国内分流的 DNS 面 + 链路诊断端点(#12)
#5 只分了数据面(geoip/geosite-cn 路由 direct),DNS 仍 final:remote 全量经隧道
解析 → 国内域名解析到非 CN IP、漏过 geoip-cn 又走隧道(白盒实测国内 TLS
1000-1660ms;修后 ~50ms)。

- clientconfig.go: 开分流时 dns.rules 加 {rule_set:[geosite-cn]→local},国内域名
  用 local(223.5.5.5)直连解析 → 拿到真 CN IP → geoip-cn 命中直连
- main.go: PANGOLIN_PUBLIC_URL 缺失时启动告警(空则分流静默跳过,是隐蔽坑)
- nodes.go: connect 渲染加可观测日志(split_cn/rules_base/split_active/bytes)
- diag.go: 新增只读端点 GET /v1/diag/egress?host=X,节点侧量出海段耗时(白名单
  防 SSRF、只回耗时数字),供白盒拆「接入段 vs 出海段」

验证:go test(splitCN 开渲染 dns.rules→local、关无 dns.rules)+ go vet;cara
实测国内 TLS 1000ms+→~50ms、接入段占 TLS 握手 ~98%。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:12:31 +08:00

118 lines
3.5 KiB
Go

package httpapi
import (
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/wangjia/pangolin/server/internal/nodes"
)
func testNode() *nodes.NodeRow {
return &nodes.NodeRow{
UUID: "n1", Endpoint: "1.2.3.4:443",
RealityPBK: "pbk", RealityShortID: "sid", RealitySNI: "www.apple.com",
}
}
func routeOf(t *testing.T, cfg []byte) map[string]any {
t.Helper()
var m map[string]any
if err := json.Unmarshal(cfg, &m); err != nil {
t.Fatalf("unmarshal config: %v", err)
}
return m["route"].(map[string]any)
}
func TestBuildClientConfigSplitCN(t *testing.T) {
// 开启分流 + base → geoip-cn/geosite-cn rule_set + cn 直连规则;URL 自托管。
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k",
ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://node:8080/"})
if err != nil {
t.Fatal(err)
}
route := routeOf(t, cfg)
if rs, ok := route["rule_set"].([]any); !ok || len(rs) != 2 {
t.Fatalf("expected 2 rule_set, got %v", route["rule_set"])
}
s := string(cfg)
for _, want := range []string{
"geoip-cn", "geosite-cn",
"http://node:8080/v1/rules/geoip-cn.srs",
"http://node:8080/v1/rules/geosite-cn.srs",
"download_detour",
} {
if !strings.Contains(s, want) {
t.Errorf("config missing %q", want)
}
}
foundCN := false
for _, r := range route["rules"].([]any) {
rm := r.(map[string]any)
if rm["outbound"] == "direct" && rm["rule_set"] != nil {
foundCN = true
}
}
if !foundCN {
t.Error("missing cn-direct route rule (rule_set→direct)")
}
// DNS 面分流:开分流时国内域名(geosite-cn)用 local 解析,不走 remote(隧道)。
var m map[string]any
_ = json.Unmarshal(cfg, &m)
dnsRules, ok := m["dns"].(map[string]any)["rules"].([]any)
if !ok || len(dnsRules) == 0 {
t.Fatalf("split on should have dns.rules (geosite-cn→local), got %v", m["dns"])
}
dr := dnsRules[0].(map[string]any)
if dr["server"] != "local" || dr["rule_set"] == nil {
t.Errorf("dns rule should route geosite-cn → local, got %v", dr)
}
// 关闭分流 → 无 rule_set,且 DNS 无分流规则(全量 remote)。
cfg2, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
if _, ok := routeOf(t, cfg2)["rule_set"]; ok {
t.Error("split off should have no rule_set")
}
var m2 map[string]any
_ = json.Unmarshal(cfg2, &m2)
if _, ok := m2["dns"].(map[string]any)["rules"]; ok {
t.Error("split off should have no dns.rules")
}
// 开启但缺 base → 静默不分流(避免渲染出无效 rule_set URL)。
cfg3, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{SplitCN: true})
if _, ok := routeOf(t, cfg3)["rule_set"]; ok {
t.Error("split with empty base should have no rule_set")
}
}
func TestRulesHandler(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "geoip-cn.srs"), []byte("SRS"), 0o644); err != nil {
t.Fatal(err)
}
r := chi.NewRouter()
r.Get("/v1/rules/{name}", NewRulesHandler(dir).Serve)
get := func(path string) (int, string) {
rec := httptest.NewRecorder()
r.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
return rec.Code, rec.Body.String()
}
if code, body := get("/v1/rules/geoip-cn.srs"); code != 200 || body != "SRS" {
t.Fatalf("allowed file: code=%d body=%q, want 200/SRS", code, body)
}
if code, _ := get("/v1/rules/passwd"); code != 404 {
t.Errorf("disallowed name: code=%d, want 404", code)
}
if code, _ := get("/v1/rules/geosite-cn.srs"); code != 404 {
t.Errorf("allowed but missing file: code=%d, want 404", code)
}
}