From e157b16c1119be373ebbebee099da41ba88944b0 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 31 Jul 2026 01:15:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20=E8=B5=B0=E9=9A=A7=E9=81=93/?= =?UTF-8?q?=E6=8B=92=E7=BB=9D=E7=9A=84=E5=9F=9F=E5=90=8D=E8=A7=84=E5=88=99?= =?UTF-8?q?=E4=B9=9F=E5=BC=80=20reverse=5Fmapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户规则(可配置分流)里 action==proxy/reject 的域名规则,此前不触发 dns.reverse_mapping(只有 direct 域名规则触发)。而应用自行解析域名后按 IP 发起连接,路由层只剩 IP,无反向映射则 domain 规则永不命中——走隧道/拒绝的 域名规则会静默失效。 translateUserRules 把 hasDomainDirect 扩成 hasDomainRule:任意 action 的 域名类规则(domain/domain_suffix/domain_keyword)都置真、都开 reverse_mapping。 direct 专属的 ip_cidr→route_exclude_address(extraExclude)那条线不变。 这也是把私有服务分流(PANGOLIN_PRIVATE_SPLIT_DOMAINS)改用用户规则表达的 前置修复——否则一条"走隧道"用户规则替代 private-split 会连不上。 回归测试 TestBuildConfigProxyDomainEnablesReverseMapping:唯一一条走隧道/拒绝 域名规则(privateSplit 关、无 direct 域名规则)必须开 reverse_mapping。go test ./... 全绿。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A79VtQA1BwTuQN1ThpvYpo --- server/internal/httpapi/clientconfig.go | 25 ++++++++++--------- server/internal/httpapi/clientconfig_test.go | 26 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/server/internal/httpapi/clientconfig.go b/server/internal/httpapi/clientconfig.go index 7bc0020..abe1d83 100644 --- a/server/internal/httpapi/clientconfig.go +++ b/server/internal/httpapi/clientconfig.go @@ -43,11 +43,14 @@ type ClientConfigOpts struct { // - extraExclude: type==ip_cidr && action==direct 的 value,供调用方并入 // TUN 入站的 route_exclude_address(auto_route 层直连才真正生效, // 见 tunIn 构造处注释)。 -// - hasDomainDirect: 是否存在 action==direct 的域名类规则(domain/ +// - hasDomainRule: 是否存在**任意** action 的域名类规则(domain/ // domain_suffix/domain_keyword),供调用方决定是否开 dns.reverse_mapping。 +// 不限 direct:走隧道/拒绝的域名规则同样需要反向映射——应用自行解析域名后 +// 按 IP 发起连接,路由层只剩 IP,没有 reverse_mapping 则 domain 规则永不命中、 +// 该规则(不论直连/隧道/拒绝)静默失效。 // - geoSets: 规则引用到的 geoip-/geosite- rule_set tag(去重),供 // 调用方在国内分流(splitActive)之外也补上 rule_set 定义。 -func translateUserRules(p *routing.Profile) (rules []any, extraExclude []string, hasDomainDirect bool, geoSets []string) { +func translateUserRules(p *routing.Profile) (rules []any, extraExclude []string, hasDomainRule bool, geoSets []string) { if p == nil || p.Mode != "rule" { return nil, nil, false, nil } @@ -71,9 +74,8 @@ func translateUserRules(p *routing.Profile) (rules []any, extraExclude []string, switch r.Type { case "domain", "domain_suffix", "domain_keyword": rules = append(rules, map[string]any{r.Type: []string{r.Value}, "outbound": outbound}) - if outbound == "direct" { - hasDomainDirect = true - } + // 任意 action 的域名规则都要 reverse_mapping(见返回值注释),不止 direct。 + hasDomainRule = true case "ip_cidr": rules = append(rules, map[string]any{"ip_cidr": []string{r.Value}, "outbound": outbound}) if outbound == "direct" { @@ -88,7 +90,7 @@ func translateUserRules(p *routing.Profile) (rules []any, extraExclude []string, } } } - return rules, extraExclude, hasDomainDirect, geoSets + return rules, extraExclude, hasDomainRule, geoSets } // ruleSetDef 渲染一个自托管 remote rule_set 定义(与既有 geoip-cn/geosite-cn @@ -243,10 +245,10 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien // 直接返回全零值,以下每一步都随之短路,保证 nil-profile 渲染逐字节不变。 var userRules []any var extraExclude []string - var hasDomainDirect bool + var hasDomainRule bool var geoSets []string if opts.Profile != nil { - userRules, extraExclude, hasDomainDirect, geoSets = translateUserRules(opts.Profile) + userRules, extraExclude, hasDomainRule, geoSets = translateUserRules(opts.Profile) if opts.RulesBaseURL == "" && len(geoSets) > 0 { // 没有 base 就没法渲染 remote rule_set 的下载 URL,引用它的用户规则 // 会指向未定义的 tag(sing-box FATAL)。与 splitActive 缺 base 时静默 @@ -380,10 +382,11 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien "domain": opts.PrivateSplitDomains, "server": "dns-system", }) } - if privateSplit || hasDomainDirect { + if privateSplit || hasDomainRule { // 回映射:记住"哪个 IP 是哪个域名解析出来的",给后续按 IP 发起的连接补回 - // 域名元数据——路由层的 domain 规则(私有域名→隧道 / 用户域名直连)靠它 - // 才会命中。hasDomainDirect:用户规则含 action==direct 的域名类规则时同样需要。 + // 域名元数据——路由层的 domain 规则(私有域名→隧道 / 用户域名规则)靠它 + // 才会命中。hasDomainRule:用户规则含**任意** action 的域名类规则(直连/ + // 走隧道/拒绝)时都需要——否则该域名规则永不命中、静默失效。 dns["reverse_mapping"] = true } // 国内分流的 DNS 面(补 #5 数据面之外的 DNS 面):开分流时,命中 geosite-cn 的 diff --git a/server/internal/httpapi/clientconfig_test.go b/server/internal/httpapi/clientconfig_test.go index b4d7563..7852544 100644 --- a/server/internal/httpapi/clientconfig_test.go +++ b/server/internal/httpapi/clientconfig_test.go @@ -358,6 +358,32 @@ func TestBuildConfigUserRules(t *testing.T) { } } +// 回归钉:走隧道/拒绝的域名规则也必须开 reverse_mapping。应用自行解析域名后按 IP +// 发起连接,路由层只剩 IP,无反向映射则 domain 规则永不命中、该规则静默失效。 +// 旧实现只为 action==direct 的域名规则开 reverse_mapping,走隧道/拒绝会漏 → 本测试 +// 用**唯一一条走隧道域名规则**(无任何 direct 域名规则、privateSplit 关)钉死修复。 +func TestBuildConfigProxyDomainEnablesReverseMapping(t *testing.T) { + for _, action := range []string{"proxy", "reject"} { + node := testNode() + p := routing.Default() + p.Rules = []routing.Rule{ + {Type: "domain_suffix", Value: "example.com", Action: action, Enabled: true}, + } + // SplitCN 关 + 不给 PrivateSplitDomains → reverse_mapping 只可能由该域名规则触发。 + raw, err := BuildClientConfig(node, "dp", "k", ClientConfigOpts{Profile: p}) + if err != nil { + t.Fatal(err) + } + var cfg map[string]any + if err := json.Unmarshal(raw, &cfg); err != nil { + t.Fatal(err) + } + if cfg["dns"].(map[string]any)["reverse_mapping"] != true { + t.Fatalf("action=%s domain rule must enable reverse_mapping", action) + } + } +} + func TestBuildConfigGlobalMode(t *testing.T) { p := routing.Default() p.Mode = "global"