本文是一次真实排障的脱敏复盘。拓扑、地址、设备名均做过替换,保留的是排查思路与结论。
一、起因:三个"灵异现象"
- 某个 AI 站点在浏览器里报
403,偶尔还提示"暂不支持你所在的地区" - 另一个站点直接
ERR_CONNECTION_RESET - 但在路由器本机上用 curl 测,又是正常的 200
最要命的是第三条——"在本机是好的",说明问题不在"通不通",而在"谁在用哪条路出去"。
二、第一层:先怀疑出口 IP 的声誉
跨境访问里,出口 IP 被目标站风控是最常见的原因。验证方法很简单也很有效:在同一台出口机器上,强制用 v4 和 v6 分别打同一个域名。
curl -4 -s -o /dev/null -w "%{http_code}\n" https://目标站
curl -6 -s -o /dev/null -w "%{http_code}\n" https://目标站
结果:v4 → 200,v6 → 403。
同一个账号、同一个时间、同一台机器,只换了地址族。结论很清楚:不是账号被封,是这个 IPv6 前缀 / ASN 被判定成了机房出口。
(后来确认,那几台 VPS 的 v6 全在同一家以"廉价 VPS"著称的 IDC 段里。这类地址在很多风控库里会被直接标成 hosting / VPN。)
三、第二层:一个反直觉的坑——"改 DNS"救不了
既然 v6 有问题,直觉就是"让这些域名别用 v6"。于是去代理内核的 DNS 配置里做域名级的 AAAA 抑制。
结果完全无效。
原因有个反直觉的点:在透明代理链里,目的域名的解析往往不是本地代理内核做的,而是隧道尽头的服务器做的。
验证方法(很值得记):
在出口 VPS 的
/etc/hosts里,把目标域名临时钉到一个 IPv4 地址,然后从本地再打一次。
如果本地出口跟着变了(比如从 v6 段变成 v4 段),就说明解析发生在 VPS 端,而不是本地。
确认之后,本地做的一切域名级地址族控制都成了摆设:
- DNS 里加
disable-ipv6✗ - 按域名指定解析器 ✗
- 代理内核里按节点设
ip-version✗
最后这条特别容易踩:内核文档里 ip-version 的说明是"影响节点的 server 为域名时使用的地址"——也就是只管"怎么连节点",不管"从节点出去时用哪个地址族"。很容易看漏。
四、第三层:真正的根因——IPv6 泄漏
既然本地改不动出口族,那就换个方向:从客户端侧看流量到底走没走代理。
关键在于——不要只在路由器上测。在路由器上测,测到的是路由器自己的出口;而真正出问题的是客户端。
从一台普通客户端上:
curl -6 https://ip检测站/cdn-cgi/trace
输出的 IPv6 地址,是自家宽带的地址,不是代理出口。
再去路由器上看连接跟踪表:一条来自客户端的 IPv6 连接都没有。
真相是:
这套"旁路由"方案只对 IPv4 生效。IPv6 的 RA 是上游主路由下发的,客户端压根不知道旁路由也是个 v6 网关——所以所有 v6 流量都绕开代理,直接出去了。
被墙的域名走 v6 → 直连 → 被 GFW 按 SNI 重置 → ERR_CONNECTION_RESET。
五、为什么是"突然"坏的
这里有个值得警惕的因果链。
之前,DNS 上游里混着明文解析器,而且被投毒了。客户端拿到的 AAAA 是被污染的假地址——连不上,浏览器按 Happy Eyeballs 回退到 IPv4(走代理),于是"能用,但总感觉怪怪的"。
后来把明文污染源清掉,DNS 干净了,客户端拿到了真实的 AAAA——那就真的去连 v6 了,然后被重置。
教训:修好一个数据源,可能把一个一直存在的结构性缺陷直接暴露成故障。修完 DNS 之后,应该顺着"数据变真了会导致什么行为变化"再想一层。
六、修复:让旁路由真正成为 IPv6 网关
本质目标是:让客户端的 v6 也交给旁路由。具体三件事。
· 1. 上游主路由:停发 RA
不能两边都发。两个默认路由同优先级时,客户端会按流随机选——表现就是"一会儿能通一会儿不能",非常难查。
这里有个坑:不是所有 RA 服务端都支持路由器优先级(prf)。我用的那个(odhcpd)查下来根本不支持,所以别指望"发个高优先级的抢过来",只能老老实实让上游停发。
· 2. 旁路由:打开 RA
ignore=0 # 打开该接口的 DHCP/RA 服务
dhcpv4=disabled # ★ 关键:显式关掉 DHCPv4,否则会和上游抢 DHCP
ra=server # 只做无状态 SLAAC
dhcpv6=disabled
ra_management=0
ra_default=2
第 2 条是血赚的一条——旁路由和上游在同一二层,如果不显式关掉自己的 DHCPv4,两个 DHCP 服务器会在同一网段里打架。
· 3. 什么都不用改代理
旁路由上的 IPv6 TPROXY 规则其实早就配好了,只是因为客户端从来不发 v6 过来,一直空转。RA 一开,规则立刻开始工作。
验证:
tracert -6 <某境外 IPv6 地址> # 第一跳应该是旁路由
curl -6 https://ip检测站/... # 出口应该是代理 IP
七、意外收获:一个"搭便车"的野前缀
排障途中还揪出一个隐藏更深的雷。
旁路由的 LAN 前缀是手工写死的,而且不在上游主路由正式委派的 /60 里面。它"一直能用",所以谁都没在意。
但当上游主路由按第 1 步停用 IPv6 之后:
改之前:curl -6 国内站 → 000 ← 早就断了,没人发现
改之后:curl -6 国内站 → 200
它一直是靠上游那条链路"搭便车"的,上游一关,立刻静默失效。
换成一个落在正式委派 /60 里的 /64 后恢复正常。
两个教训:
- "能用但来源不明"的配置,属于定时炸弹。上游一有变动就必须复测,别假设它还在工作。
- 这类问题最容易被代理路径掩盖。因为代理会重新发起连接,源地址根本不起作用——所以从代理侧测,永远是好的。必须测直连(DIRECT)路径。
八、可复用的排查顺序
- 同机双栈对照——同一台机器,
-4/-6打同一个域名,先确定是不是出口族的问题 - 判断解析位置——在出口机器的 hosts 里钉地址,看本地出口是否跟着变
- 从客户端侧验证——别只在路由器上测;要看客户端自己的源地址和第一跳
- 检查下发——RA / DHCP 有没有正常发给客户端,客户端知不知道这个网关
- 检查前缀合法性——手工配置的前缀到底在不在正式委派范围内
- 测直连路径——别只测代理路径,DIRECT 才能暴露前缀 / 路由问题
九、结语
这次排障里,最有价值的不是任何一个具体命令,而是两个判断习惯:
- "在 A 处测是好的",不等于"在 B 处是好的"——一定要在真正出问题的位置测。
- "一直能用"不等于"配置是对的"——要问一句"它凭什么能用"。
(完)
十、补记(原文发布后新增)
文章发出去之后,我又顺着这条线踩了三个坑,都挺典型,补上。
· 补记 1:DNS 劫持 + 只监听 IPv4 的 dnsmasq = 用 IPv6 DNS 的设备集体断网
透明代理通常会在网关上装一条 DNS 劫持:把所有去 53 端口的包重定向到本机。这条规则对 IPv4 和 IPv6 同时生效。
问题出在负责应答的那一侧:如果 dnsmasq 只监听了 IPv4,那么——
任何把网关的 IPv6 地址当作 DNS 服务器的设备(比如从 RA 的 RDNSS 里学到的),查询会被劫持到本机,然后因为没人监听 IPv6 的 53 端口而直接超时。
表现就是:设备网络"时好时坏",App 转圈,但你跑到路由器上用 IPv4 测,一切正常。
这个坑最阴的地方在于它平时不发作——只有客户端真的有 IPv6 地址、并且真的用了 IPv6 DNS 时才触发。所以它可以潜伏很久,直到你把某个地址族的流量引导进代理,才突然爆发。
检查方法:
netstat -lnp | grep ":53 "
# 期望 IPv4 和 IPv6 都要有:0.0.0.0:53 和 :::53
· 补记 2:透明代理接管一个地址族之后,规则必须"域名 + IP"双覆盖
这是个非常容易漏的结构性问题。
规则集里常见的写法是:
GEOSITE,cn,DIRECT
MATCH,PROXY
GEOSITE,cn 是按域名匹配的。那么不带域名、或端口不在嗅探范围内的连接——国内 App 的私有协议经常就是这样,直接连裸 IP、走非标端口——匹配不到任何规则,直接掉进兜底的那条 MATCH,PROXY,被发到海外代理去了。
结果就是:主要功能都正常,但偶发卡顿,而且很难复现。
正确写法:
GEOSITE,cn,DIRECT
GEOIP,CN,DIRECT <-- 按 IP 兜底
MATCH,PROXY
这个坑在 IPv4 时代可能一直不发作(如果上游只把境外流量转给你);但一旦你把某个地址族的流量全部接管过来,它立刻就会暴露。
· 补记 3:改 DNS 是高风险操作——我把自己家的网搞挂了一次
这条最值得写,因为代价最大。
为了修补记 1 里的问题,我在 OpenWrt 上执行了这么一行:
uci add_list dhcp.@dnsmasq[0].listen_address='::'
本意是"让它也听 IPv6"。结果——
在 OpenWrt 上,一旦显式设置
listen_address,dnsmasq 的绑定模式会从「按接口绑定」切换成「只绑定列出的地址」。而这台机器bindv6only=1,::并不覆盖 IPv4 → 网关的 IPv4 DNS 地址直接消失 → 整个局域网的名字解析全部失效。
后果是:家里所有设备断网,连我自己的运维通道也一起断了——我把自己的退路一起切掉了。
更糟的是,我同时还手动重启了 dnsmasq,把代理软件注入的上游配置也一并弄丢,等于双重故障。
最后能救回来,是因为之前加过一条兜底:
# 永远给 dnsmasq 留一个直连上游
server=223.5.5.5
server=119.29.29.29
这条的意义是:不管代理软件的 DNS 出什么问题,家里的名字解析都不会跟着一起死。
三条教训:
- 不要用
listen_address,优先用接口绑定——它会在你意想不到的地方静默漏掉地址。 - 不要在代理软件管理的路由器上手动重启 dnsmasq——会丢掉它注入的配置。要改就改配置文件,然后让代理软件自己重启。
- 改 DNS 之前先问自己一句:改坏了,我还连得上这台机器吗?DNS 是所有链路的入口,也是最容易切断自己退路的地方。