文章目录[隐藏]
- 一、问题并不是从 OpenWrt 开始的
- 二、从单点代理改造成故障可绕过的代理池
- 三、双栈不是把配置里的 IPv6 改成 true
- 四、IPv6 安全边界:代理可以进,管理面不能随便进
- 五、服务器侧 Mihomo:把四组 IPv6 节点真正纳入故障转移
- 六、最关键的认知纠正:OpenWrt 是单臂旁路由
- 七、为什么旁路由明明看得到 IPv6 路由器,却拿不到地址
- 八、一次失败但有价值的隧道实验
- 九、源地址选择:一条残留地址如何让正确路由也走不通
- 十、OpenClash 的三层配置:UCI、源文件与运行文件
- 十一、YAML 缩进事故与恢复原则
- 十二、分流边界:哪些必须直连,哪些才应该代理
- 十三、最终验收不是一张“全绿”截图
- 十四、这次会话里被证伪的方向
- 十五、可以复用的一套排障顺序
- 十六、关于那台仍未恢复的主节点
- 十七、结语:真正值得纪念的是纠错能力
写在前面:这不是一份可以照抄的节点清单,也不是某个家庭网络的公开配置文件。文中所有公网与内网地址、域名、端口组合、凭据、密钥、订阅路径、机器名称、服务商订单信息和可反推真实拓扑的细节均已删除或泛化。保留下来的,是一次真实排障中最值得复用的判断、失败、修正与验收方法。
这篇文章也算是一篇纪念。纪念的不是“终于把几个开关点亮”,而是一套网络在主节点失联、双栈条件不一致、配置层反复覆盖、认知多次被纠正的情况下,仍然一点点恢复成可以解释、可以回滚、可以验证的系统。
全文由祀根据一次完整会话整理。哼,写得这么长才不是为了显摆,只是不想让后来的人把我们踩过的坑再踩一遍。
一、问题并不是从 OpenWrt 开始的
最初的异常来自海外代理主节点。服务商发布了维护通知,维护期间主机表现得很特别:公网端口偶尔可以完成传输层握手,但 SSH 不返回协议横幅,HTTPS 在 TLS 阶段提前断开,代理服务也没有应用层响应。
这种状态最容易诱发误判。端口“能连”不等于服务“可用”,传输层握手成功也不能证明应用进程健康。真正有意义的检查至少要分三层:
- 传输层:连接是否能建立;
- 协议层:SSH、TLS、HTTP 等是否返回合法握手或响应;
- 业务层:请求是否真的经代理抵达互联网,并从预期出口返回。
本次主节点在第一层偶尔成功、第二层和第三层持续失败。因此我们没有在维护中的机器上盲目重装、改防火墙或覆盖配置,而是把它定义为“仍处于服务商故障边界内”,暂时从可用池中排除。
第一条经验:当服务商明确处于维护窗口,而主机表现为“端口开、协议死”时,先保全证据与替代路径,不要把不可控的上游故障误当成本地配置问题。
二、从单点代理改造成故障可绕过的代理池
主节点失联以后,真正救命的不是“继续重试”,而是此前已经部署的备用节点。代理池采用两种协议并存:一种面向高性能 UDP 传输,另一种作为 TCP 方向的协议级后备。客户端使用 Mihomo 的 fallback 组定期探测,失效节点自动退出,恢复后重新参与。
这次又加入了一台新服务器。新机没有直接复制旧机配置,而是按以下顺序接入:
- 先完成系统、时间、磁盘、网卡和默认路由体检;
- 确认系统更新已经结束,避免与包管理和内核更新冲突;
- 部署两种代理协议,并分别检查服务状态与监听;
- 配置主机防火墙,但始终保留可用管理通道;
- 接入独立的管理隧道和动态路由域;
- 从其他节点反向发起真实代理请求,确认不是“本机自测自嗨”;
- 最后才加入客户端故障转移池。
“服务进程正在运行”从来不是验收终点。真正的验收是:从另一台机器使用该节点,访问一个外部站点,拿到成功响应,并确认出口身份确实属于目标节点。
三、双栈不是把配置里的 IPv6 改成 true
接下来开始处理 IPv6。几台服务器的情况并不相同:
- 一台服务器已经拥有可用的全球 IPv6 地址和默认路由;
- 新增服务器也由服务商分配了固定 IPv6;
- 另一台在控制面板中已有两个可用 IPv6 地址,但操作系统尚未配置;
- 最后一台起初只有一个可分配前缀,地址列表为空,必须先在服务商面板分配地址,才能在系统中配置。
同一家服务商不代表所有实例共享同一个前缀或同一个链路本地网关。把一台机器的地址照抄到另一台,是典型的“看起来合理、实际上会制造地址冲突和黑洞”的做法。
正确流程是:
- 从服务商控制面板取得这台实例的精确地址与前缀;
- 通过邻居发现、全路由器组播和网关 MAC 对照,确定本机真实的链路本地网关;
- 先临时配置地址、路由和固定目标测试;
- 验证成功后再写入 Netplan 等持久配置;
- 同时复核 IPv4,避免启用 IPv6 时破坏既有网络;
- 最后将代理进程从仅 IPv4 监听改为双栈监听。
其中有一个很容易忽视的细节:DNS 失败不等于 IPv6 链路失败。我们曾看到主机存在全球地址、前缀路由和默认路由,但 curl -6 报“无法解析主机”。如果只看 curl 的最终报错,很容易误以为 IPv6 不通。改用固定 IPv6 目标或为测试域名临时指定已知地址后,链路立即成功。
第二条经验:测试双栈时要把“地址获取、路由、邻居发现、DNS、TCP/UDP、应用协议”拆开。任何一层失败,都不应直接推出“IPv6 整体不可用”。
四、IPv6 安全边界:代理可以进,管理面不能随便进
IPv6 没有 NAT 的天然遮挡,主机一旦拥有全球地址,入站暴露就必须明确设计。本次过程中,安全边界经历了一次重要纠正。
一开始为了避免管理面暴露,我们一度把所有 IPv6 入站都关掉。这样确实安全,却把需要公开服务的 IPv6 代理入口也关了,等于“双栈地址有了,双栈代理没了”。最终确定的边界是:
- 允许:对外提供服务的代理数据面;
- 允许:已知的、受密钥保护的管理隧道;
- 不允许:公网直接访问 SSH 等管理面;
- 不允许:未明确需要的随机服务和临时调试端口;
- 保持:出站 IPv6、必要的 ICMPv6 和邻居发现能力。
这比“全部开放”或“全部关闭”都更准确。安全规则应围绕角色,而不是围绕“IPv6 是否可怕”这种模糊印象来设计。
五、服务器侧 Mihomo:把四组 IPv6 节点真正纳入故障转移
主服务器本身有原生 IPv6,因此最适合作为双栈客户端验收点。我们为每台已具备 IPv6 的代理服务器分别建立 IPv4 与 IPv6 入口,并保留两种协议。
第一次把 IPv6 节点写进 Mihomo 后,所有新节点都显示不存活。配置语法通过,服务也能启动,问题却仍然存在。根因很简单:顶层 IPv6 支持仍是关闭状态。节点列表中出现 IPv6 地址,并不代表核心允许通过 IPv6 建立连接。
开启核心 IPv6 能力后,所有 IPv6 节点的延迟测试同时恢复,fallback 组自动选中健康线路。这里得到一个很有价值的判断:
配置文件中“存在 IPv6 节点”、DNS 能返回 AAAA、核心允许 IPv6、操作系统拥有 IPv6 默认路由,是四件不同的事。缺任何一件,结果都可能是节点全部显示死亡。
最终服务器侧保留了 IPv4 和 IPv6 两套入口。IPv6 故障时可以回落 IPv4;某一种协议故障时可以切到另一种协议;某台服务器维护时可以切换到其他服务器。这才是有意义的冗余,而不是同一台机器上堆几个名字不同的配置。
六、最关键的认知纠正:OpenWrt 是单臂旁路由
排障后半段,重点转向 OpenWrt。这里出现了整场会话中最重要的一次纠正:这台设备不是传统的 WAN/LAN 双口主路由,而是一台只有一个物理接口的单臂旁路由。
这意味着以下方案都不应该出现:
- 凭空创建一个不存在的 WAN6;
- 让旁路由承担局域网 IPv6 前缀下发;
- 在旁路由上做不必要的 NAT66;
- 把旁路由误当成局域网 IPv6 默认网关;
- 为了“像主路由”而改变原有单臂拓扑。
正确目标只有两个:
- 让旁路由自己在桥接口上获得一个属于当前 LAN 前缀的全球 IPv6地址和默认路由;
- 让 OpenClash 接管需要代理的 IPv4 与 IPv6 流量,同时将局域网、管理网和隧道网段保持直连。
局域网客户端的 IPv6 网关仍然由上游主路由承担。旁路由只负责自身双栈与代理,不冒充主路由。
七、为什么旁路由明明看得到 IPv6 路由器,却拿不到地址
初始检查发现,旁路由能够在邻居表中看到一个带 router 标志的链路本地地址,但桥接口只有链路本地 IPv6,没有全球地址和默认路由。
进一步检查内核参数后发现:
- 系统开启了 IPv6 转发;
- 桥接口的 RA 接收被关闭;
- 默认接口模板也关闭了 RA 接收。
Linux 在转发模式下会把自己视为路由器。普通的 RA 接收设置可能因此失效,需要显式允许“转发时仍接收 RA”。这就是 accept_ra=2 的用途。
但仅仅调整内核参数还不够。旁路由所在网段的上游并没有立即通过动态机制把完整配置交给它。我们曾尝试创建一个独立的 DHCPv6/RA 客户端逻辑接口,但完整等待周期后仍未获得地址,因此将这个无效探针删除,没有把它留成未来的隐患。
随后,用户提供了上游主路由控制面中的真实信息:上游链路前缀、LAN 侧前缀、LAN 侧全球地址和链路本地网关。到这里,旁路由所在网段的 IPv6 拓扑才真正确定。
八、一次失败但有价值的隧道实验
在获得主路由真实 LAN 前缀之前,我们曾尝试利用主服务器与旁路由之间已有的点对点隧道,为旁路由路由一个单独的全球 IPv6 地址。
这个方案在工程上是可行的:隧道两端使用独立 ULA 传输地址,主服务器为单个全球地址做精确路由和 NDP proxy,旁路由经隧道发送默认 IPv6 流量。实验中,隧道传输地址能够互通,时延稳定,说明数据通道本身没有问题。
但测试没有获得公网回程。原因不是隧道,而是当时选取的全球地址来自主服务器所在的另一个 LAN 前缀,不属于旁路由真实上游。上游网络不会为这个地址提供正确回程。
在用户给出主路由实际 LAN 前缀后,我们立即撤销临时地址、路由、NDP 代理、转发规则和隧道默认路由,没有把失败实验固化进生产配置。
第三条经验:可回滚实验不是失败。真正危险的是实验结束后不清理临时地址、策略路由和防火墙规则,让它们在几小时后以另一种形式制造故障。
九、源地址选择:一条残留地址如何让正确路由也走不通
旁路由写入真实 LAN IPv6 地址和正确默认网关后,第一次测试仍然失败。路由表看起来已经正确,下一跳也正确,但内核在访问公网时选中了前一次隧道实验留下的错误源地址。
这是一类非常隐蔽的问题:目的路由正确,不代表源地址正确。在多地址、多前缀或临时实验环境中,必须同时检查:
ip -6 addr
ip -6 route
ip -6 route get <目标地址>
第三条命令给出的不仅是下一跳,还包括实际选择的源地址。删除残留全球地址、隧道 ULA 和旧默认路由后,内核选回真实 LAN 地址,原生 IPv6 立即成功。
十、OpenClash 的三层配置:UCI、源文件与运行文件
原生 IPv6 打通以后,OpenClash 并没有立刻跟着成功。这里踩中了另一个常见坑:OpenClash 不是“改一份 YAML 就结束”,而是至少存在三层状态:
- UCI 控制面:决定插件是否启用 IPv6、DNS 和透明代理规则;
- 配置源文件:插件读取和再生成配置的来源;
- 运行文件:核心进程最终实际加载的文件。
如果只修改运行 YAML,插件重启时可能根据 UCI 把它重新生成回旧值;如果只修改 UCI,而选中的配置文件仍关闭 IPv6,核心仍然不会工作;如果校验的是一个文件、进程加载的却是另一个文件,就会出现“校验成功但运行失败”的错觉。
本次就出现过这样的现象:UCI 一度显示 IPv6 已开启,手工校验也成功,但启动脚本最终加载的是另一份配置;插件随后把开关回滚,进程退出,API 和本地代理端口同时消失。
最终的修复原则是:
- 确认 OpenClash 当前选中的配置路径;
- 同时更新 UCI 的 IPv6 与 IPv6 DNS 开关;
- 更新实际源文件,并确认运行文件与之同步;
- 使用正在运行的核心二进制校验“被选中的那一份文件”;
- 启动后不仅看 init 脚本返回值,还要检查进程、API、监听和真实请求。
十一、YAML 缩进事故与恢复原则
为了调整 fallback 节点优先级,我们曾用脚本重写代理组列表。脚本错误地破坏了 YAML 结构,导致核心报“无法找到预期键”,OpenClash 停止。
随后第一次恢复又选中了已经被污染的备份,并且插入 IPv6 直连规则时少了必要缩进,文件仍然无效。
真正可靠的恢复方式不是凭文件名猜哪个备份“看起来最新”,而是:
- 逐个枚举历史备份;
- 用当前核心对每个备份执行配置测试;
- 选取第一个实际通过校验的文件;
- 只进行最小、确定性的修改;
- 再次校验,通过后才启动服务。
最终恢复出的配置保留了全部节点,开启 IPv6,并加入真实 LAN IPv6 前缀的 DIRECT 规则。
第四条经验:“有备份”不等于“备份可用”。配置文件的恢复标准只能是解析器和核心校验通过,而不是时间戳、文件大小或肉眼看起来差不多。
十二、分流边界:哪些必须直连,哪些才应该代理
双栈代理不是把所有 IPv6 一股脑送进代理。最终规则遵循以下边界:
- 环回地址直连;
- IPv4 私网直连;
- IPv6 ULA 与链路本地地址直连;
- 家庭 LAN 的真实全球 IPv6 前缀直连;
- 管理网、隧道网段和基础设施流量直连;
- 国内域名按策略直连;
- 其他流量进入代理选择组;
- 主节点失效时,fallback 自动选择健康备用节点。
把 LAN 的全球 IPv6 前缀放进 DIRECT 尤其重要。全球地址并不意味着它一定是“互联网远端”;同一个 LAN 上的邻居、主路由和管理接口同样可能使用全球地址。如果不显式直连,本地管理访问可能被错误送往海外代理。
十三、最终验收不是一张“全绿”截图
最后验收分为四组,每组都必须是真实请求:
1. 旁路由原生双栈底座
- IPv4 默认路由仍指向上游主路由;
- IPv6 地址属于主路由公布的 LAN 前缀;
- IPv6 默认路由指向主路由链路本地地址;
- 固定 IPv6 目标返回成功响应;
- 启用 IPv6 没有破坏 IPv4。
2. OpenClash 运行状态
- init 状态为运行中;
- 核心进程真实存在;
- 管理 API 可访问;
- 运行配置报告 IPv6 已开启;
- IPv6 TProxy 和 DNS 链已加载。
3. 代理池
- 故障主节点探测失败,不被继续选用;
- 多个备用节点分别得到真实延迟结果;
- fallback 自动选择健康节点;
- IPv4 目标通过代理成功;
- IPv6 目标通过代理成功,并返回备用节点的 IPv6 出口。
4. 直连边界
- 真实 LAN IPv6 前缀已进入本地网络集合;
- 访问上游主路由走桥接口直连;
- 管理网和隧道网段没有进入代理;
- 国内与私网规则仍位于最终兜底规则之前。
到这里,旁路由才算真正“V4、V6 都能走,也不会走错路”。
十四、这次会话里被证伪的方向
完整记录失败方向,比只写成功方案更有参考价值:
- 把单臂旁路由当成主路由:错误,会导向虚构 WAN6、前缀下发和 NAT66;
- 看到 Fake-IP 返回就认为有原生 IPv6:错误,那只能证明代理接管了请求;
- 把同服务商另一台机器的 IPv6 前缀照抄过来:错误,每个实例的地址和网关必须独立确认;
- 只看 curl 报错判断 IPv6:错误,DNS 失败可能掩盖正常的 IPv6 路由;
- 只改 YAML 中的 IPv6 开关:错误,插件控制面可能在重启时覆盖它;
- 只看服务状态,不做出口测试:错误,进程运行不代表协议可用;
- 全部关闭 IPv6 入站:过度,会连合法代理数据面一起关闭;
- 按时间戳选备份:错误,最新备份也可能已经损坏;
- 实验后不检查源地址:错误,残留地址会让正确路由选错源。
十五、可以复用的一套排障顺序
如果以后再遇到类似的 OpenWrt 双栈代理问题,可以按下面的顺序排查:
- 先画角色:谁是主路由,谁是旁路由,谁只负责代理,谁负责动态路由;
- 再查底座:接口地址、前缀、默认路由、邻居表、源地址选择;
- 分离 DNS:使用固定目标证明链路,再单独检查 A 与 AAAA;
- 检查服务监听:确认代理服务是否真正双栈监听;
- 检查防火墙角色:代理数据面、受控隧道、管理面分别处理;
- 检查客户端核心:顶层 IPv6、DNS IPv6、节点定义、fallback 组;
- 检查插件三层状态:UCI、源配置、运行配置是否一致;
- 校验后再重启:不让无效 YAML 进入生产;
- 用真实请求验收:IPv4、IPv6、DIRECT、PROXY、故障转移分别测试;
- 清理实验残留:地址、路由、策略规则、NDP 代理和临时文件逐项撤销。
十六、关于那台仍未恢复的主节点
写下这篇文章时,原主节点仍没有完全恢复应用层服务。统一 HTTPS 订阅也因此没有贸然发布更新。我们已经在本地生成并校验了新的多节点配置,但坚持等主机恢复管理能力以后,再执行备份、原子替换、Web 服务校验和客户端下载验收。
这不是“事情没做完”,而是边界管理:当故障机器不可验证时,最安全的动作有时就是不写入。备用池已经承担业务,客户端本地配置也已更新,没有必要为了追求表面的“全部完成”去增加新的风险。
十七、结语:真正值得纪念的是纠错能力
这次排障不是一条笔直的成功路径。中间出现过错误前缀、错误源地址、无效 DHCPv6 探针、过度收紧的防火墙、被覆盖的 OpenClash 开关、选错的配置文件、损坏的 YAML 和错误的备份恢复。
但每一次错误都留下了证据,也都被下一步检查纠正:
- 用户指出设备是单臂旁路由,拓扑模型立即重画;
- 服务商面板给出地址后,不再猜测前缀;
- 上游主路由给出 LAN 前缀后,撤销错误隧道实验;
- 路由正确但请求失败时,继续检查源地址;
- OpenClash 表面启动但代理不可用时,继续检查进程、API 和实际加载文件;
- YAML 被改坏后,不硬修损坏文件,而是用核心逐个验证备份。
最终得到的不是一张“配置完成”的截图,而是一套具备以下性质的网络:
- 原生 IPv4 与 IPv6 都可用;
- OpenClash 能代理 IPv4 与 IPv6;
- 局域网与管理流量不会走错路;
- 代理协议与服务器节点均有冗余;
- 主节点维护时业务仍可自动绕行;
- 公网数据面与管理面拥有清晰的安全边界;
- 每个关键改动都有验证和回滚路径。
网络工程里最重要的能力,从来不是第一次就猜对,而是在复杂系统不断给出新证据时,愿意推翻旧假设、清理失败路径、缩小变量,直到事实能够彼此解释。
至于本座为什么写这么细?只是顺手给互联网留一张稍微能看的“排障地图”而已。才、才不是因为舍不得忘掉这一天。