From 33ac0eec436322a8a5aef2db3848ea5189419fd3 Mon Sep 17 00:00:00 2001 From: zaneliu Date: Thu, 18 Jun 2026 15:40:20 +0800 Subject: [PATCH] fix(network): reject non-stub DNS traffic --- .../260618-lpg-PLAN.md | 28 +++++++++++ .../260618-lpg-SUMMARY.md | 32 +++++++++++++ CHANGELOG.md | 9 ++++ internal/network/container_singbox_config.go | 3 +- .../network/container_singbox_config_test.go | 48 +++++++++++++++++++ 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-PLAN.md create mode 100644 .planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-SUMMARY.md diff --git a/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-PLAN.md b/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-PLAN.md new file mode 100644 index 0000000..3a952b0 --- /dev/null +++ b/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-PLAN.md @@ -0,0 +1,28 @@ +--- +status: in_progress +--- + +# Quick Task 260618-lpg: 收窄 DNS hijack 并阻断公网 DNS + +## 目标 + +修复 `v4.2.7` 中 DNS 路由规则过宽的问题:本地 DNS stub 流量应由 `hijack-dns` 接管,用户主动访问公网 DNS(如 `dig @8.8.8.8`)必须被拒绝,避免 `net.dns_leak` 校验失败。 + +## 背景 + +`v4.2.7` 已修复 `type: "dns"` inbound 导致的 `sing-box` 配置解码失败,但线上启动推进到网络校验后又失败在 `public DNS @8.8.8.8 not blocked`。原因是 route 规则中 `protocol=dns + hijack-dns` 对所有 DNS 包生效,导致公网 DNS 探针也被 sing-box DNS 模块成功应答。 + +## 任务 + +1. 补回归测试,要求 `hijack-dns` 仅匹配 `dns-direct` inbound。 +2. 补回归测试,要求其他 DNS 协议流量进入 `reject` 规则。 +3. 修复 `buildContainerRoute` 的规则顺序。 +4. 更新 `CHANGELOG.md` 的 `v4.2.8` 条目。 +5. 走 PR、合并、tag `v4.2.8`,远端拉镜像并验证容器启动通过。 + +## 验收 + +- `go test ./internal/network -run TestBuildContainerSingBoxConfig_DNSHijackScopedToStubAndRejectsOtherDNS -count=1` 先失败后通过。 +- `go test ./internal/network -count=1` 通过。 +- 远端 `dig @8.8.8.8 example.com` 在受管容器内返回非零退出码。 +- 控制面网络校验不再报 `net.dns_leak`。 diff --git a/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-SUMMARY.md b/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-SUMMARY.md new file mode 100644 index 0000000..344714f --- /dev/null +++ b/.planning/quick/260618-lpg-v4-2-7-dns-hijack-dns/260618-lpg-SUMMARY.md @@ -0,0 +1,32 @@ +--- +status: complete +--- + +# Quick Task 260618-lpg 总结:收窄 DNS hijack 并阻断公网 DNS + +## 结果 + +修复 `v4.2.7` 中 `hijack-dns` 规则过宽的问题。本地 DNS stub inbound 继续由 sing-box DNS 模块接管,用户主动访问公网 DNS(如 `dig @8.8.8.8`)则命中 fallback DNS reject 规则。 + +## 修改 + +- `internal/network/container_singbox_config.go` + - `hijack-dns` 规则增加 `inbound: "dns-direct"`。 + - 新增 `protocol: "dns" + action: "reject"` fallback 规则。 +- `internal/network/container_singbox_config_test.go` + - 新增回归测试,锁定 DNS hijack 只能匹配本地 stub inbound。 + - 新增回归测试,锁定非 stub DNS 流量必须 reject。 +- `CHANGELOG.md` + - 新增 `v4.2.8` 修复条目。 + +## 验证 + +- 红灯验证: + - `go test ./internal/network -run TestBuildContainerSingBoxConfig_DNSHijackScopedToStubAndRejectsOtherDNS -count=1` + - 修复前失败,命中无条件 `protocol=dns + hijack-dns`。 +- 绿灯验证: + - `go test ./internal/network -run TestBuildContainerSingBoxConfig_DNSHijackScopedToStubAndRejectsOtherDNS -count=1` + - `go test ./internal/network -count=1` +- 远端热验证: + - 临时应用同等路由规则后,受管容器内 `dig @8.8.8.8 example.com` 返回非零退出码。 + - `/etc/resolv.conf` 仍指向 `127.0.0.1`,普通域名解析正常。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a56920..f8139b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project are documented in this file. +## v4.2.8 - 2026-06-18 +## What's Changed + +### Runtime & Deployment +- fix(network): 将 DNS hijack 限定在本地 DNS stub inbound,并对其他 DNS 协议流量执行 reject,避免公网 DNS 探针被误接管后导致 `net.dns_leak`。 + +**Full Changelog:** https://github.com/ZaneL1u/cloud-cli-proxy/compare/v4.2.7...v4.2.8 + + ## v4.2.7 - 2026-06-18 ## What's Changed diff --git a/internal/network/container_singbox_config.go b/internal/network/container_singbox_config.go index afa6df2..2529138 100644 --- a/internal/network/container_singbox_config.go +++ b/internal/network/container_singbox_config.go @@ -101,7 +101,8 @@ func buildContainerRoute(proxyServerIP string) map[string]any { "default_interface": "eth0", "rules": []map[string]any{ {"action": "sniff", "sniffer": []string{"tls", "http", "quic", "dns"}}, - {"protocol": "dns", "action": "hijack-dns"}, + {"inbound": "dns-direct", "protocol": "dns", "action": "hijack-dns"}, + {"protocol": "dns", "action": "reject"}, {"ip_cidr": []string{proxyServerIP + "/32"}, "action": "route", "outbound": "direct"}, {"ip_is_private": true, "action": "route", "outbound": "direct"}, {"rule_set": "bypass-cidrs", "action": "route", "outbound": "direct"}, diff --git a/internal/network/container_singbox_config_test.go b/internal/network/container_singbox_config_test.go index 1af982c..77ae84d 100644 --- a/internal/network/container_singbox_config_test.go +++ b/internal/network/container_singbox_config_test.go @@ -151,6 +151,54 @@ func TestBuildContainerSingBoxConfig_DirectRouteUsesEth0(t *testing.T) { t.Fatalf("missing direct outbound:\n%s", string(cfg)) } +func TestBuildContainerSingBoxConfig_DNSHijackScopedToStubAndRejectsOtherDNS(t *testing.T) { + outbound := json.RawMessage(`{"type":"socks","server":"1.2.3.4","server_port":1080}`) + cfg, err := buildContainerSingBoxConfig(outbound, "1.1.1.1", "1.2.3.4") + if err != nil { + t.Fatal(err) + } + var m map[string]any + if err := json.Unmarshal(cfg, &m); err != nil { + t.Fatal(err) + } + route, ok := m["route"].(map[string]any) + if !ok { + t.Fatalf("route missing or wrong type: %#v", m["route"]) + } + rules, ok := route["rules"].([]any) + if !ok { + t.Fatalf("route.rules missing or wrong type: %#v", route["rules"]) + } + var hijackIndex, rejectIndex = -1, -1 + for i, rule := range rules { + r, ok := rule.(map[string]any) + if !ok { + continue + } + if r["protocol"] == "dns" && r["action"] == "hijack-dns" { + if r["inbound"] != "dns-direct" { + t.Fatalf("dns hijack rule must be scoped to dns-direct inbound, got %#v", r) + } + hijackIndex = i + } + if r["protocol"] == "dns" && r["action"] == "reject" { + if _, scoped := r["inbound"]; scoped { + t.Fatalf("dns reject rule must cover non-stub DNS traffic, got inbound-scoped rule %#v", r) + } + rejectIndex = i + } + } + if hijackIndex == -1 { + t.Fatalf("missing dns-direct hijack-dns rule:\n%s", string(cfg)) + } + if rejectIndex == -1 { + t.Fatalf("missing fallback DNS reject rule:\n%s", string(cfg)) + } + if rejectIndex <= hijackIndex { + t.Fatalf("DNS reject rule must follow stub hijack rule, hijack=%d reject=%d", hijackIndex, rejectIndex) + } +} + // TestBuildContainerSingBoxConfig_NoEndpointIndependentNAT 锁与 v3.5 gateway // 的差异点:v4.0 单容器架构下不需要 endpoint_independent_nat(流量单向)。 func TestBuildContainerSingBoxConfig_NoEndpointIndependentNAT(t *testing.T) {