From a3ad50aa7550ffa69c8805886274784ae56fe6cb Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 26 Jul 2026 01:53:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(client-config):=20TUN=20=E5=8A=A0=20route?= =?UTF-8?q?=5Fexclude=5Faddress=20=E8=AE=A9=E7=A7=81=E6=9C=89=20LAN=20?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 隧道开着时连不上局域网/NAS/家里机器(SSH/gitea 全 reset)。根因:TUN 入站 auto_route+strict_route 在 OS 层把所有流量(含 LAN)强抓进隧道,而 route.rules 里的 ip_cidr(192.168/16…)→direct 在 macOS 被 strict_route 抵消(direct 出站的 包又被捕回隧道)。 修法:TUN 入站加 route_exclude_address=[192.168.0.0/16, 10.0.0.0/8],在 auto_route 层就把这些网段排除出隧道,LAN 走系统直连。**刻意不含 172.16.0.0/12**——隧道自身 地址与 DNS(172.19.x)在此段,排除会断 DNS。route_exclude_address 为 sing-box v1.13 合法 TUN 字段(option/tun.go)。 影响:VPN 不再黑洞局域网——直连 NAS/家里机器/内网 gitea 恢复,CI runner 也不再 需要经 ali 的中继隧道(relay)。客户端需完整重连拉新配置生效。 测试:TestBuildClientConfigLANExclude 断言 tun 含 192.168/16+10/8、不含 172.16/12; go test ./internal/httpapi 全绿。 Co-Authored-By: Claude Fable 5 --- server/internal/httpapi/clientconfig.go | 6 +++ server/internal/httpapi/clientconfig_test.go | 42 ++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/server/internal/httpapi/clientconfig.go b/server/internal/httpapi/clientconfig.go index 426f1e4..af0541d 100644 --- a/server/internal/httpapi/clientconfig.go +++ b/server/internal/httpapi/clientconfig.go @@ -113,6 +113,12 @@ func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts Clien "auto_route": true, "strict_route": true, "stack": "system", + // 私有 LAN 直连:strict_route 会在 OS 层把所有流量(含 LAN)强抓进隧道, + // 光靠 route.rules 的 ip_cidr→direct 在 macOS 不生效(direct 出站的包被 + // strict_route 重新捕回隧道)。route_exclude_address 在 auto_route 层就把这些 + // 网段排除出隧道,LAN 走系统直连(修「隧道开着连不上局域网/NAS/家里机器」)。 + // **不含 172.16.0.0/12**:隧道自身地址与 DNS(172.19.x)在此段,排除会断 DNS。 + "route_exclude_address": []string{"192.168.0.0/16", "10.0.0.0/8"}, } // 代理出站集合:REALITY 必有;Hy2 仅在启用时加入(否则不进配置/探测组)。 diff --git a/server/internal/httpapi/clientconfig_test.go b/server/internal/httpapi/clientconfig_test.go index a144d1f..5d54d81 100644 --- a/server/internal/httpapi/clientconfig_test.go +++ b/server/internal/httpapi/clientconfig_test.go @@ -193,3 +193,45 @@ func TestRulesHandler(t *testing.T) { t.Errorf("allowed but missing file: code=%d, want 404", code) } } + +// TUN 入站必须把私有 LAN 网段从隧道排除(route_exclude_address),否则 strict_route +// 会在 macOS 把 LAN 强抓进隧道 → 隧道开着连不上局域网/NAS。不得含 172.16/12 +// (隧道自身 172.19.x 在此段,排除会断 DNS)。 +func TestBuildClientConfigLANExclude(t *testing.T) { + cfg, err := BuildClientConfig(testNode(), "uuid-1", "k", ClientConfigOpts{}) + if err != nil { + t.Fatalf("build: %v", err) + } + var m map[string]any + if err := json.Unmarshal(cfg, &m); err != nil { + t.Fatalf("unmarshal: %v", err) + } + var tun map[string]any + for _, in := range m["inbounds"].([]any) { + im := in.(map[string]any) + if im["type"] == "tun" { + tun = im + break + } + } + if tun == nil { + t.Fatal("no tun inbound") + } + exRaw, ok := tun["route_exclude_address"] + if !ok { + t.Fatal("tun inbound missing route_exclude_address (LAN would be captured by strict_route)") + } + got := map[string]bool{} + for _, v := range exRaw.([]any) { + got[v.(string)] = true + } + if !got["192.168.0.0/16"] { + t.Error("route_exclude_address must contain 192.168.0.0/16") + } + if !got["10.0.0.0/8"] { + t.Error("route_exclude_address must contain 10.0.0.0/8") + } + if got["172.16.0.0/12"] { + t.Error("route_exclude_address must NOT contain 172.16.0.0/12 (tunnel DNS 172.19.x lives there)") + } +}