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-l83: 修复 sing-box DNS inbound 回归

## 目标

修复 `v4.2.6` 中容器 `sing-box` 配置生成错误,避免生成不被当前运行时支持的 `type: "dns"` inbound,恢复主机创建后的出口连通性校验。

## 背景

线上新建主机失败,控制面报 `net.egress_unreachable`。远端容器日志显示 `sing-box` 解码 `/etc/sing-box/config.json` 失败,原因是 `inbounds[1]` 使用了不支持的 `type: "dns"`。

## 任务

1. 补回归测试,要求 `127.0.0.1:53` 使用受支持的 `direct` inbound。
2. 补回归测试,禁止容器配置继续生成 `dns` inbound。
3. 修复容器 `sing-box` 配置生成逻辑。
4. 更新 `CHANGELOG.md` 的 `v4.2.7` 条目。
5. 通过 PR 合并后打 `v4.2.7`,等待镜像发布,再更新远端并验证容器启动。

## 验收

- `go test ./internal/network -run 'TestBuildContainerSingBoxConfig_(DNSStubInboundUsesSupportedDirect|DirectRouteUsesEth0)' -count=1` 先失败后通过。
- `go test ./internal/network -count=1` 通过。
- 发布镜像包含 `org.opencontainers.image.version=4.2.7`。
- 远端控制面和受管容器启动后不再出现 `unknown inbound type: dns`。
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
status: complete
---

# Quick Task 260618-l83 总结:修复 sing-box DNS inbound 回归

## 结果

修复 `v4.2.6` 容器 `sing-box` 配置中生成 `type: "dns"` inbound 的回归。新配置改为用受支持的 `direct` inbound 监听 `127.0.0.1:53`,并通过 `hijack-dns` 路由规则接管 DNS 流量。

## 修改

- `internal/network/container_singbox_config.go`
- `127.0.0.1:53` DNS stub 从 `type: "dns"` 改为 `type: "direct"`。
- DNS stub tag 固定为 `dns-direct`,并开启 `sniff`。
- `route.default_interface` 固定为 `eth0`。
- direct outbound 增加 `bind_interface: "eth0"`。
- `internal/network/container_singbox_config_test.go`
- 增加回归测试,禁止生成不支持的 `dns` inbound。
- 增加回归测试,锁定 direct 路由使用 `eth0`。
- `CHANGELOG.md`
- 新增 `v4.2.7` 修复条目。

## 验证

- 红灯验证:
- `go test ./internal/network -run 'TestBuildContainerSingBoxConfig_(DNSStubInboundUsesSupportedDirect|DirectRouteUsesEth0)' -count=1`
- 修复前失败,分别命中 `unsupported dns inbound` 和缺少 `route.default_interface`。
- 绿灯验证:
- `go test ./internal/network -run 'TestBuildContainerSingBoxConfig_(DNSStubInboundUsesSupportedDirect|DirectRouteUsesEth0)' -count=1`
- `go test ./internal/network -count=1`

## 线上修复路径

1. PR 合并到 `main`。
2. 打 `v4.2.7` 标签触发 release workflow。
3. 等待 `control-plane` 和 `managed-user` 镜像发布。
4. 远端拉取最新镜像并重启控制面。
5. 验证受管容器不再因 `unknown inbound type: dns` 重启,出口连通性校验恢复。
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project are documented in this file.

<!-- release-entries -->

## v4.2.7 - 2026-06-18
## What's Changed

### Runtime & Deployment
- fix(network): 恢复容器 DNS stub 为 sing-box 支持的 direct inbound,避免新建主机因 `unknown inbound type: dns` 进入出口连通性校验失败。
- fix(network): 为容器直连分支显式绑定 `eth0`,确保代理服务器与 bypass 直连流量不被 tun 默认路由回环。

**Full Changelog:** https://github.com/ZaneL1u/cloud-cli-proxy/compare/v4.2.6...v4.2.7


## v4.2.6 - 2026-06-15
## What's Changed

Expand Down Expand Up @@ -761,4 +771,3 @@ All notable changes to this project are documented in this file.
- fix(ci): source pnpm version from root package manager (6cce3f8)

**Full Changelog:** https://github.com/ZaneL1u/cloud-cli-proxy/compare/v1.4.3...v1.4.4

21 changes: 13 additions & 8 deletions internal/network/container_singbox_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func buildContainerSingBoxConfig(outboundRaw json.RawMessage, _ /*dnsServer*/, p
if err != nil {
return nil, err
}
dnsIn, err := buildContainerDNSInbound()
dnsIn, err := buildContainerDNSDirectInbound()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -98,6 +98,7 @@ func buildContainerDNS() map[string]any {
func buildContainerRoute(proxyServerIP string) map[string]any {
return map[string]any{
"default_domain_resolver": map[string]any{"server": "dns-local"},
"default_interface": "eth0",
"rules": []map[string]any{
{"action": "sniff", "sniffer": []string{"tls", "http", "quic", "dns"}},
{"protocol": "dns", "action": "hijack-dns"},
Expand All @@ -124,16 +125,19 @@ func buildContainerRoute(proxyServerIP string) map[string]any {
}
}

// buildContainerDNSInbound 渲染 DNS inbound 监听 127.0.0.1:53。
func buildContainerDNSInbound() (json.RawMessage, error) {
// buildContainerDNSDirectInbound 渲染 DNS stub 监听 127.0.0.1:53。
// sing-box 当前运行时不支持 inbound type=dns;这里用 direct inbound 接收
// resolv.conf 指向本地 DNS stub 的流量,再由 route 的 hijack-dns 规则接管。
func buildContainerDNSDirectInbound() (json.RawMessage, error) {
raw, err := json.Marshal(map[string]any{
"type": "dns",
"tag": "dns-in",
"type": "direct",
"tag": "dns-direct",
"listen": "127.0.0.1",
"listen_port": 53,
"sniff": true,
})
if err != nil {
return nil, fmt.Errorf("marshal container dns inbound: %w", err)
return nil, fmt.Errorf("marshal container dns direct inbound: %w", err)
}
return raw, nil
}
Expand Down Expand Up @@ -225,8 +229,9 @@ func isDomain(s string) bool {
// buildGatewayDirectOutbound 渲染 direct outbound。
func buildGatewayDirectOutbound() (json.RawMessage, error) {
raw, err := json.Marshal(map[string]any{
"type": "direct",
"tag": "direct",
"type": "direct",
"tag": "direct",
"bind_interface": "eth0",
})
if err != nil {
return nil, fmt.Errorf("marshal direct outbound: %w", err)
Expand Down
49 changes: 38 additions & 11 deletions internal/network/container_singbox_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestBuildContainerSingBoxConfig_DNSStubServers(t *testing.T) {
}
}

func TestBuildContainerSingBoxConfig_DNSInboundIsNotDirect(t *testing.T) {
func TestBuildContainerSingBoxConfig_DNSStubInboundUsesSupportedDirect(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 {
Expand All @@ -96,32 +96,59 @@ func TestBuildContainerSingBoxConfig_DNSInboundIsNotDirect(t *testing.T) {
if !ok {
continue
}
if in["type"] == "dns" {
t.Fatalf("container sing-box config must not use unsupported dns inbound: %#v", in)
}
if in["listen"] == "127.0.0.1" && in["listen_port"] == float64(53) {
if in["type"] != "dns" {
t.Fatalf("127.0.0.1:53 inbound type = %v, want dns", in["type"])
if in["type"] != "direct" {
t.Fatalf("127.0.0.1:53 inbound type = %v, want direct", in["type"])
}
if in["tag"] != "dns-direct" {
t.Fatalf("127.0.0.1:53 inbound tag = %v, want dns-direct", in["tag"])
}
if in["tag"] == "dns-direct" {
t.Fatalf("dns inbound tag must not collide with dns-direct server tag")
if in["sniff"] != true {
t.Fatalf("127.0.0.1:53 direct inbound must enable sniff, got %#v", in["sniff"])
}
return
}
}
t.Fatalf("missing DNS inbound listening on 127.0.0.1:53:\n%s", string(cfg))
}

func TestBuildContainerSingBoxConfig_NoHardcodedEth0(t *testing.T) {
func TestBuildContainerSingBoxConfig_DirectRouteUsesEth0(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)
}
s := string(cfg)
if strings.Contains(s, `"default_interface": "eth0"`) {
t.Errorf("route must not pin default_interface to eth0:\n%s", s)
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"])
}
if got := route["default_interface"]; got != "eth0" {
t.Fatalf("route.default_interface = %v, want eth0", got)
}
if strings.Contains(s, `"bind_interface": "eth0"`) {
t.Errorf("direct outbound must not bind to eth0:\n%s", s)
outbounds, ok := m["outbounds"].([]any)
if !ok {
t.Fatalf("outbounds missing or wrong type: %#v", m["outbounds"])
}
for _, outbound := range outbounds {
out, ok := outbound.(map[string]any)
if !ok {
continue
}
if out["tag"] == "direct" {
if got := out["bind_interface"]; got != "eth0" {
t.Fatalf("direct outbound bind_interface = %v, want eth0", got)
}
return
}
}
t.Fatalf("missing direct outbound:\n%s", string(cfg))
}

// TestBuildContainerSingBoxConfig_NoEndpointIndependentNAT 锁与 v3.5 gateway
Expand Down
Loading