绿联 NAS 容器 DNS 全挂?可能是 v2raya 留下的幽灵规则
那天本来岁月静好,绿联 NAS 上的 Octopus 代理安安静静地跑着,定时帮我调用 DeepSeek 和智谱的 API。然后它突然就炸了。
日志里甩出来这么一行报错:
dial tcp: lookup api.deepseek.com on 8.8.8.8:53: connection refused
我一愣。connection refused?8.8.8.8 把我拒了?Google 的 DNS 不是活得好好的吗?
行吧,排查开始。
第一感觉:是不是 DNS 53 端口被封了?
这个报错看着很唬人。on 8.8.8.8:53: connection refused——第一反应是:是不是运营商或者路由器把 53 端口给封了?毕竟这两年某些地区的 ISP 确实干过这种事。
我赶紧进容器里测一把:
# 容器内测试 TCP 443(HTTPS)
curl -v --connect-timeout 5 https://api.deepseek.com
# 等待... 居然连上了!TCP 握手成功,SSL 握手成功
# 再测 80
curl -v --connect-timeout 5 http://api.deepseek.com
# 也能通,服务器正常返回 301
# 那测测 DNS?
dig @8.8.8.8 api.deepseek.com
# ;; connection refused; could not reach 53
# nc 也试一下
nc -zv 8.8.8.8 53
# Connection refused
nc -zv 8.8.8.8 443
# succeeded!
好家伙,TCP 443 和 80 都活蹦乱跳的,唯独 53 端口死活不通。从宿主机测也一样,宿主机和容器是同一个症状。
这现象太典型了——「只有 53 不通,其他端口正常」,十个运维九个会告诉你:这是 ISP 或路由器在劫持/封锁 DNS。我当时的判断也是这个。
甚至还一度脑补了一出大戏:是不是最近 DNS over HTTPS 普及了,ISP 干脆把明文 53 给阉了?
为了「证实」这个猜测,我还在 VPS 上临时搭了个 HTTP 代理,让容器通过代理走出去绕开 DNS。代理一挂上,API 果然能连了——
「看吧,就是 53 被封了,绕过去就完事了。」我当时是这么安慰自己的。
但是,这个方案太丑了。所有容器都要配代理、域名解析还得靠代理解决,每多一个服务就多一份配置。而且我心里隐隐觉得不对劲:如果是 ISP 封的,为什么有时候又能偶尔通一下?ISP 封端口一般是很干脆的,不会这么暧昧。
我在这条岔路上耗了快一个小时。直到——
一句话,改写了整个排查方向
我把现象跟用户描述了一下,说「53 不通、别的端口都通,八成是 ISP 搞的鬼」。
用户听完,皱了皱眉,随口说了一句:
「这台 NAS 上之前跑过 v2raya,用的 host 网络模式,它会监听 53 端口做透明代理的 DNS 劫持。后来停掉了,但不知道规则清干净没有。」
这句话简直是当头一棒。
v2raya 做透明代理的时候,会把所有 DNS 查询(53 端口)REDIRECT 到自己监听的一个本地端口(通常是 52353),由它来接管 DNS 解析。如果容器停了,但 iptables/nftables 规则没清掉——那所有 DNS 包都会被转到一个没人监听的端口,结果就是 connection refused。
这跟现象完全对上了:443、80 不受影响(它们不在劫持规则里),只有 53 被劫持,而且劫持目标是本地端口。
ISP:不是我的锅,谢。
真凶现形:nftables 里藏着的幽灵
问题来了:绿联 NAS 的 SSH 是不给 root 的,我没法直接登进去看 nft list ruleset。
但是!Docker 给我们留了后门。可以用一个特权容器 + nsenter 直接进入宿主机的内核命名空间,操作宿主的 netfilter:
docker run --rm -it --privileged --pid=host \
--cap-add=NET_ADMIN \
alpine sh -c "nsenter -t 1 -n -- nft list ruleset"
这条命令的意思是:--pid=host 让容器看到宿主机的进程列表,--privileged 给足权限,然后 nsenter -t 1 -n 进入 PID 1 的网络命名空间(也就是宿主机内核的网络栈),最后在里面跑 nft list ruleset。
回车一敲,nat 表里的内容刷地一下出来了。我一眼就看到了这 4 条规则(关键字段摘录):
table ip nat {
chain PREROUTING {
meta l4proto tcp tcp dport 53 counter \
redirect to :52353 # packets 1152
meta l4proto udp udp dport 53 counter \
redirect to :52353 # packets 234821 <-- 23万包被劫持!
}
chain OUTPUT {
meta l4proto tcp tcp dport 53 counter \
redirect to :52353 # packets 87
meta l4proto udp udp dport 53 counter \
redirect to :52353 # packets 234019 <-- 又是23万!
}
}
破案了。
看那个 counter——PREROUTING 链的 UDP 53 已经累计劫持了 23 万多个包,OUTPUT 链也是 23 万。每一个 DNS 查询都被 REDIRECT 到了 :52353,然后——
52353 端口空空荡荡,v2raya 早就停了,没有任何进程在监听。
内核 dutifully 把每个 DNS 包转过去,然后 dutifully 收到一个 ECONNREFUSED,再把这个拒绝原样丢回给查询者。于是我们就看到了那个经典的 connection refused。
23 万个 DNS 包,石沉大海。每一个都代表着某个容器在绝望地尝试解析域名。
所以 TCP 443/80 为什么通?因为劫持规则只匹配了 dport 53,HTTPS 流量压根没被规则碰过,直接正常路由出去了。这完美解释了「只有 53 不通」的现象。
动手清理:四条规则,一了百了
找到了真凶就好办了。同样用 nsenter 进宿主网络栈,把这 4 条规则删掉就行:
docker run --rm -it --privileged --pid=host \
--cap-add=NET_ADMIN \
alpine sh -c '
nsenter -t 1 -n -- iptables -t nat -D PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 52353
nsenter -t 1 -n -- iptables -t nat -D PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 52353
nsenter -t 1 -n -- iptables -t nat -D OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports 52353
nsenter -t 1 -n -- iptables -t nat -D OUTPUT -p udp --dport 53 -j REDIRECT --to-ports 52353
'
这里有个细节:绿联 NAS 的内核用的是 nftables,但 iptables-nft 这个兼容层会把 iptables 命令翻译成 nftables 规则操作。所以用 iptables -D 是没问题的,它会在底层去删对应的 nft 规则。
四条命令,删干净。然后我立刻在容器里验证:
# 容器内
dig @8.8.8.8 api.deepseek.com
# ; <<>> DiG 9.x <<>> @8.8.8.8 api.deepseek.com
# ;; QUESTION SECTION:
# ;api.deepseek.com. IN A
# ;; ANSWER SECTION:
# api.deepseek.com. 300 IN A xxx.xxx.xxx.xxx
# ;; Query time: 38 msec ;; SERVER: 8.8.8.8#53
# 哦对了!正常返回!
DNS 瞬间恢复。宿主机、容器内、Octopus 代理、DeepSeek API、智谱 API——全部恢复正常,不需要任何代理绕路了。
那个我辛辛苦苦搭的 VPS HTTP 代理?拆了。根本用不着。
复盘:这次踩的坑,下次怎么避免
1. host 网络模式的容器,改了 iptables 不会自动回滚
这是最核心的教训。host 网络模式的容器,它的网络栈跟宿主机是同一个。v2raya 启动时往 nat 表里塞了 DNS 劫持规则,这是它做透明代理的正常行为——问题是,当你 docker stop 或者 docker rm 这个容器时,它塞进去的规则不会被自动清理。
规则就这么孤零零地留在了宿主机的 netfilter 里,像一个没人认领的地雷,等着下一个 DNS 查询踩上去。
v2raya 自己其实是有一套清理逻辑的(正常退出时会 flush 规则),但如果是被强制 kill、或者 Docker 直接干掉了容器、或者 OOM、或者系统重启后规则还在——这些情况都可能造成残留。这次大概率就是某次异常退出留下的。
2. v2raya 透明代理的 DNS 劫持机制
简单说一下原理,免得下次又一脸懵:
- 为什么要劫持 DNS? 因为透明代理需要知道你要访问的域名,才能按规则分流(国内直连/国外走代理)。如果让 DNS 直接走系统默认解析,代理就拿不到域名信息了。
- 怎么做? 在 nat 表的 PREROUTING 和 OUTPUT 链各加两条规则,把所有目标端口 53 的包 REDIRECT 到本地 52353 端口(v2raya 的 DNS 监听端口)。
- PREROUTING 管谁? 管从其他容器/设备转发过来的 DNS 查询(
--to-ports对经过路由的包生效)。 - OUTPUT 管谁? 管宿主机本机产生的 DNS 查询(本机发出的包)。
所以 v2raya 停了之后,规则还在,53 端口的包全被转到 52353——一个死端口,全军覆没。
3. 绿联 NAS 的 root 权限问题
绿联(UGREEN)NAS 的官方 SSH 是不给 root 的,普通用户很多东西碰不了(比如 nft、iptables)。这次能救场,全靠 Docker 的这个技巧:
docker run --rm -it --privileged --pid=host \
alpine sh -c "nsenter -t 1 -n -- <你要执行的命令>"
--pid=host:共享宿主机的 PID 命名空间--privileged:给容器几乎所有的内核权限nsenter -t 1 -n:进入宿主 PID 1(init/systemd)的网络命名空间
这套组合拳等于在容器里拿到了宿主机内核的网络操作权限。以后在绿联 NAS 上排查网络问题、看路由表、改防火墙规则,都可以这么干。记住这个模式,它会救你很多次。
4. 排查网络问题的优先级
这次最大的弯路,就是太早下了「端口被封」的结论。回头看,正确的排查顺序应该是:
- 先看本机防火墙规则——iptables/nftables 有没有奇怪的 REDIRECT/DNAT。这步成本极低(一条命令),但能排除掉一大类「本地自己搞的鬼」。
- 再怀疑上游——路由器、ISP、DNS 服务商。这种问题排查成本高、还改不了别人,应该放后面。
- 「只有特定端口不通」要格外警惕——如果是 ISP 封端口,通常会有一致的、稳定的表现;如果是本地规则残留,往往会有「时好时坏」或「不同协议表现不同」的诡异现象。
我这次就是第一步跳过了,直接跳到第二步,结果白白搭了个 VPS 代理折腾了一通。
最后
排查完,我回头看了看那 23 万个被劫持又石沉大海的 DNS 包,莫名有点心疼。
它们每个人都尽职尽责地发出了查询,被内核温柔地转了个方向,满怀期待地奔向 52353——
然后撞上了一扇不存在的门。
下次再遇到「只有 53 不通」的情况,我一定先 nft list ruleset 看一眼,再决定要不要把锅甩给 ISP。
——共勉。