feat(server): 私有服务域名分流(PANGOLIN_PRIVATE_SPLIT_DOMAINS)
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Failing after 11s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Failing after 9s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Failing after 9s
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Failing after 11s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Failing after 9s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Failing after 9s
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Has been cancelled
家庭内网穿透域名(nas/git/win.yanmeiai.com)的客户端配置渲染: - DNS 面: 系统解析器(type=local,底层网络)+私有域名规则置顶+reverse_mapping - 路由面: 私有域名→强制走隧道,钉在 LAN 直连之后、国内分流(geoip-cn)之前 在家: 局域网 DNS 覆盖→私网IP→LAN直连零绕行; 在外: 锚点(frps@ali,国内IP) 不再被 smartRoute 分流成直连、避开安全组限源。env 不配置=行为零变化。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,84 @@ func TestBuildClientConfigSplitCN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildClientConfigPrivateSplit(t *testing.T) {
|
||||
domains := []string{"nas.yanmeiai.com", "git.yanmeiai.com", "win.yanmeiai.com"}
|
||||
// 私有分流 + 国内分流同时开:验证规则齐全且顺序正确
|
||||
// (LAN 直连 → 私有域名强制走隧道 → 国内直连;私有规则必须在国内直连之前,
|
||||
// 否则锚点是国内 IP 会被分流成直连、被 frps 侧安全组限源拦截)。
|
||||
cfg, err := BuildClientConfig(testNode(), "uuid-1", "k",
|
||||
ClientConfigOpts{SplitCN: true, RulesBaseURL: "http://node:8080",
|
||||
PrivateSplitDomains: domains})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(cfg, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ① dns.servers 含系统解析器(type=local):在家吃到局域网 DNS 覆盖(私网IP),
|
||||
// 在外用所在网络 DNS 解析出公网锚点。
|
||||
dnsm := m["dns"].(map[string]any)
|
||||
foundSystem := false
|
||||
for _, s := range dnsm["servers"].([]any) {
|
||||
sm := s.(map[string]any)
|
||||
if sm["tag"] == "dns-system" && sm["type"] == "local" {
|
||||
foundSystem = true
|
||||
}
|
||||
}
|
||||
if !foundSystem {
|
||||
t.Error("missing dns-system (type=local) dns server")
|
||||
}
|
||||
|
||||
// ② dns.rules 首条 = 私有域名→dns-system(须排在 geosite-cn→local 之前)。
|
||||
dnsRules := dnsm["rules"].([]any)
|
||||
dr := dnsRules[0].(map[string]any)
|
||||
if dr["server"] != "dns-system" || dr["domain"] == nil {
|
||||
t.Errorf("dns.rules[0] should be private domains → dns-system, got %v", dr)
|
||||
}
|
||||
|
||||
// ③ reverse_mapping 开启:应用自行解析后按 IP 连接,回映射补回域名元数据,
|
||||
// 路由的 domain 规则才有效。
|
||||
if dnsm["reverse_mapping"] != true {
|
||||
t.Error("reverse_mapping should be true when private split is on")
|
||||
}
|
||||
|
||||
// ④ 路由顺序:LAN 直连 < 私有域名→auto < 国内 rule_set→direct。
|
||||
rules := m["route"].(map[string]any)["rules"].([]any)
|
||||
lanIdx, privIdx, cnIdx := -1, -1, -1
|
||||
for i, r := range rules {
|
||||
rm := r.(map[string]any)
|
||||
if rm["ip_cidr"] != nil && rm["outbound"] == "direct" {
|
||||
lanIdx = i
|
||||
}
|
||||
if rm["domain"] != nil && rm["outbound"] == "auto" {
|
||||
privIdx = i
|
||||
}
|
||||
if rm["rule_set"] != nil && rm["outbound"] == "direct" {
|
||||
cnIdx = i
|
||||
}
|
||||
}
|
||||
if lanIdx < 0 || privIdx < 0 || cnIdx < 0 {
|
||||
t.Fatalf("missing rules: lan=%d priv=%d cn=%d", lanIdx, privIdx, cnIdx)
|
||||
}
|
||||
if !(lanIdx < privIdx && privIdx < cnIdx) {
|
||||
t.Errorf("rule order wrong: lan=%d < priv=%d < cn=%d expected", lanIdx, privIdx, cnIdx)
|
||||
}
|
||||
|
||||
// ⑤ 不配置 → 全部不出现(行为与旧版完全一致)。
|
||||
cfg2, _ := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{})
|
||||
var m2 map[string]any
|
||||
_ = json.Unmarshal(cfg2, &m2)
|
||||
dnsm2 := m2["dns"].(map[string]any)
|
||||
if _, ok := dnsm2["reverse_mapping"]; ok {
|
||||
t.Error("private split off: reverse_mapping should be absent")
|
||||
}
|
||||
if strings.Contains(string(cfg2), "dns-system") {
|
||||
t.Error("private split off: dns-system should be absent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesHandler(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "geoip-cn.srs"), []byte("SRS"), 0o644); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user