一次 IPv6 排障复盘:从「被 Cloudflare 风控」挖到「局域网 IPv6 根本没走代理」

本文是一次真实排障的脱敏复盘。拓扑、地址、设备名均做过替换,保留的是排查思路与结论。

一、起因:三个"灵异现象"

  • 某个 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 后恢复正常。

两个教训:

  1. "能用但来源不明"的配置,属于定时炸弹。上游一有变动就必须复测,别假设它还在工作。
  2. 这类问题最容易被代理路径掩盖。因为代理会重新发起连接,源地址根本不起作用——所以从代理侧测,永远是好的。必须测直连(DIRECT)路径。

八、可复用的排查顺序

  1. 同机双栈对照——同一台机器,-4 / -6 打同一个域名,先确定是不是出口族的问题
  2. 判断解析位置——在出口机器的 hosts 里钉地址,看本地出口是否跟着变
  3. 从客户端侧验证——别只在路由器上测;要看客户端自己的源地址和第一跳
  4. 检查下发——RA / DHCP 有没有正常发给客户端,客户端知不知道这个网关
  5. 检查前缀合法性——手工配置的前缀到底在不在正式委派范围内
  6. 测直连路径——别只测代理路径,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 出什么问题,家里的名字解析都不会跟着一起死。

三条教训:

  1. 不要用 listen_address,优先用接口绑定——它会在你意想不到的地方静默漏掉地址。
  2. 不要在代理软件管理的路由器上手动重启 dnsmasq——会丢掉它注入的配置。要改就改配置文件,然后让代理软件自己重启。
  3. 改 DNS 之前先问自己一句:改坏了,我还连得上这台机器吗?DNS 是所有链路的入口,也是最容易切断自己退路的地方。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇