透明代理为何 SOCKS 能用、普通访问却超时?ROS 回包环路与 sing-box DNS 循环复盘

2026 年 9 月 23 日,家里云一台 Linux 主机出现外网访问异常。排查最终发现两个独立故障:sing-box 的 DNS 引导形成循环依赖,RouterOS 的策略路由又把透明代理回包送回了旁路由。

修复 DNS 后,显式 SOCKS 代理已经能访问 Google,但普通 HTTPS 请求仍然超时。直到修正 ROS 的回包路由条件,透明访问才恢复。这也是本次排查最有价值的对照实验。

本文省略真实公网节点、业务域名和所有认证信息,保留私网地址用于说明路径。配置片段针对本次环境,不是可直接覆盖现网的完整模板。

现网职责:ROS 分流,sing-box 提供代理出口

客户端地址为 192.168.10.32,默认网关是 ROS 192.168.10.254,DNS 指向 192.168.100.253。ROS 同时拥有代理网段的网关 192.168.100.254;sing-box 运行在 192.168.100.253,通过 nftables TProxy 接管被转发过来的 TCP/UDP。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
客户端 192.168.10.32
ROS 192.168.10.254
        ├─ 命中国内目的地址列表 CNIP → 正常 WAN 出口
        └─ 需要代理 → cross 路由表 → 192.168.100.253
                                 sing-box TProxy
                                 VLESS REALITY 节点

这套设计的国内外分流发生在 ROS。进入 sing-box 的业务流量默认交给代理出口,因此不能仅凭 sing-box 没有中国大陆直连规则,就认定国内流量分流缺失。必须同时检查 ROS 的地址列表、mangle 和路由表。

第一个故障:DNS 要先连代理,代理又要先查 DNS

故障时 sing-box 的业务 DNS 是 1.1.1.1 DoH,通过 vless-reality 出口访问。问题在于:代理服务器使用域名,而它的域名解析器也指向了同一个 remote-dns

1
2
3
4
5
解析网站域名
    → 经 VLESS 访问 remote-dns
    → 建立 VLESS 连接前,解析代理节点域名
    → 再次使用 remote-dns
    → 循环依赖

日志给出了直接证据,以下节点域名已替换为示例:

1
lookup proxy.example.com: DNS query loopback in transport[remote-dns]

由于客户端只使用 192.168.100.253 作为 DNS,循环导致国内外网站都可能卡在解析阶段。此时 sing-box 服务仍显示 active,所以“进程运行正常”并不能说明代理可用。

修复方式是添加一个不依赖代理的引导解析器,并明确让代理节点域名使用它。本次采用直连阿里 DNS 的 HTTPS 服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "type": "https",
  "tag": "bootstrap-dns",
  "server": "223.5.5.5",
  "server_port": 443,
  "path": "/dns-query",
  "tls": {
    "enabled": true,
    "server_name": "dns.alidns.com"
  }
}

同时设置:

1
2
route.default_domain_resolver = "bootstrap-dns"
VLESS outbound.domain_resolver = "bootstrap-dns"

这些是需要合并到现有配置的字段说明。业务 DNS 的 final 仍保持 remote-dns,经代理查询;引导解析器只负责打通建立连接所需的解析依赖。该写法在本次 sing-box 1.13.3 环境中通过配置检查并实际运行。

更改前备份配置,执行 sing-box check 成功后重启。随后客户端 DNS 查询恢复,显式 SOCKS 访问 Google 返回 HTTP 200,但透明访问依旧连接超时,说明还存在第二个故障。

第二个故障:连接标记让回包再次进入代理路由

ROS 中有两条关键规则,简化后如下:

1
2
3
4
5
/ip firewall mangle
add chain=prerouting src-address-list=proxy dst-address-list=!CNIP \
    action=mark-connection new-connection-mark=conn-cross passthrough=yes
add chain=prerouting connection-mark=conn-cross \
    action=mark-routing new-routing-mark=cross passthrough=no

第一条给需要代理的连接打 conn-cross 标记;第二条将该连接中的包送到 cross 表。本次 cross 表只有一条默认路由,下一跳是 192.168.100.253

问题在第二条:连接标记能够匹配连接双向的包,而路由标记规则没有排除返回内网的方向。

客户端发出的请求被正确送到了 sing-box。TProxy 在客户端这一侧生成的响应仍以目标网站 IP 为源地址、客户端 IP 为目的地址。该响应经 ROS 返回时,也匹配 conn-cross,于是再次被标记进入 cross

1
2
3
4
5
6
网站源 IP → 192.168.10.32 的响应包
    从 100.253 发出
        → ROS 再次打 cross 路由标记
        → cross 默认路由指向 100.253
        → 100.253 按普通网关发回 ROS
        → 反复回转,无法到达客户端

在 100.253 抓包时,可以看到同一序列号、同一确认号的 SYN-ACK 在极短时间内反复出现;10.32 则一直重发 SYN。这与单纯的公网延迟或远端网站拒绝访问不同。

为什么已有绕过规则没有生效?

现网有一条按 src-address=192.168.100.253 绕过 mangle 的规则。它适用于代理主机使用自身 IP 发起的出站连接,但透明代理返回给客户端的包,源地址仍是网站 IP,因此不匹配。

另一条 dst-address-type=local 只匹配目的地址属于 ROS 自身的包。192.168.10.32 是客户端,不是 ROS 的本机地址,也不会命中。“本机地址”与“内网网段”在这里不能混为一谈。

最小修复:让内网目的地址走正常路由

本次在 MARK: Routing 规则上增加:

1
dst-address=!192.168.0.0/16

这样只有目的地址不在该内网范围内的包才继续打 cross 路由标记。回给 192.168.10.32 的响应不再匹配,便可使用主路由表的直连路由到达客户端。

执行前应导出 mangle 配置,并确认注释只对应目标规则:

1
2
/ip firewall mangle export file=before-return-loop-fix
/ip firewall mangle print detail where comment="MARK: Routing"

确认目标唯一后修改:

1
/ip firewall mangle set [find comment="MARK: Routing"] dst-address=!192.168.0.0/16

192.168.0.0/16 覆盖本次需要返回的内网。其他环境应按实际 LAN、隧道和子网路由规划排除范围;它不包含 10.0.0.0/8172.16.0.0/12、Tailscale 的 100.64.0.0/10,也不处理 IPv6。排除策略路由本身不会自动创建这些网段的可达路由。

修复中的一个字段陷阱

第一次添加排除条件后仍然不通。读取 ROS 实际配置才发现,条件被填到了地址列表字段:

1
2
dst-address-list = !192.168.0.0/16
dst-address      = 空

Dst. Address List 接受的是地址列表名称,不会把这里的字符串直接解释为 CIDR 网段。它检查的是名为 192.168.0.0/16 的列表成员关系,无法实现原本想要的网段排除。

纠正方法是清除误填字段,再设置目标地址字段:

1
2
/ip firewall mangle unset [find comment="MARK: Routing"] dst-address-list
/ip firewall mangle set [find comment="MARK: Routing"] dst-address=!192.168.0.0/16

上述 unset 仅适用于本次误填状态。如果已有其他有效的地址列表条件,应先评估其用途,不能直接照搬清除。

修正后读取到的状态为:

1
2
dst-address      = !192.168.0.0/16
dst-address-list = 未设置

复测:必须从真实客户端走完整链路

测试中显式禁用 curl 的环境代理,确保访问经过客户端的正常路由:

1
2
3
4
dig @192.168.100.253 www.google.com
curl --noproxy '*' --connect-timeout 5 --max-time 15 \
  -o /dev/null -sS -w 'HTTP=%{http_code} total=%{time_total}\n' \
  https://www.google.com

显式 SOCKS 作为对照:

1
2
3
curl --proxy socks5h://192.168.100.253:1080 \
  --connect-timeout 5 --max-time 15 \
  -o /dev/null -sS -w 'HTTP=%{http_code}\n' https://www.google.com

本次观察结果如下,耗时是单次请求记录,不代表长期性能:

测试项目 仅修复 DNS 后 修正 ROS 字段后
客户端 DNS 查询 正常 正常
显式 SOCKS 访问 Google HTTP 200 前一阶段已验证代理出口可用
普通访问 Google 连接超时 HTTP 200,约 1.08 秒
普通访问 YouTube 连接超时 HTTP 200,约 1.50 秒
普通访问 GitHub 连接超时 HTTP 200,约 1.45 秒
普通访问百度 HTTP 200 HTTP 200,约 1.33 秒
普通访问阿里云 连接超时 HTTP 302,约 1.50 秒

阿里云的 302 表示本次 HTTPS 请求成功获得重定向响应,并非连接故障。各网站的访问结果也不单独证明它们命中了哪条 CNIP 规则;要确认某个目的 IP 的具体出口,还需对照地址列表、规则计数和抓包。

最终用户确认连通,独立复测也得到上述结果。无需清空整张连接跟踪表或重启整台路由器。

后续排查应保留的边界

这次不是 VLESS REALITY 协议本身失效,也不能归因于缺少 sing-box 国内直连规则。可复用的排查顺序是:先区分 DNS 超时和 TCP 连接超时,再用显式代理与透明访问做对照,最后检查回程是否被策略路由再次捕获。

ROS 负责地址分流,sing-box 负责代理出口,两者之间必须同时具备正确的请求路径和响应路径。服务进程正常、节点 TCP 端口开放、SOCKS 请求成功,都只验证了链路的一部分。最终验收应回到真实客户端,以不显式指定代理的请求为准。

参考资料:sing-box DNS over HTTPSsing-box Dial FieldsRouterOS MangleRouterOS Packet Flow

使用 Hugo 构建
主题 StackJimmy 设计