Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`。
Original file line number Diff line number Diff line change
@@ -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`,普通域名解析正常。
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project are documented in this file.

<!-- release-entries -->

## 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

Expand Down
3 changes: 2 additions & 1 deletion internal/network/container_singbox_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
48 changes: 48 additions & 0 deletions internal/network/container_singbox_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading