fix(client-config): TUN 加 route_exclude_address 让私有 LAN 直连
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 27s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 20s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 20s
ci-pangolin / Flutter — analyze + test (push) Failing after 10m20s
ci-pangolin / OpenAPI Sync Check (push) Failing after 10m29s
ci-pangolin / Lint — shellcheck (push) Failing after 10m41s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14m28s
ci-pangolin / Go — build + test (push) Failing after 14m37s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Failing after 14m49s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Failing after 14m57s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 19m19s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 12m37s

隧道开着时连不上局域网/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 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-26 01:53:35 +08:00
parent 5bc5ed1e62
commit a3ad50aa75
2 changed files with 48 additions and 0 deletions
+6
View File
@@ -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 仅在启用时加入(否则不进配置/探测组)。
@@ -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)")
}
}