Category: devops

  • “连不上”不一定是“被墙”:Xray 升级后,旧客户端内核握不上 REALITY

    “连不上”不一定是“被墙”:Xray 升级后,旧客户端内核握不上 REALITY

    一个长期稳定的自建 VLESS + REALITY 节点,某天开始客户端全线 timeout。第一反应通常是“这个 IP 被 GFW 封了”——于是换网络、加中继、甚至准备换 IP。但在我们这次的排查里,IP 并没有被封:真正的变化发生在服务端——Xray 被升级到了新版本,而手上的旧客户端内核(Clash 用的 mihomo)已经握不上它的 REALITY 了。

    这篇文章只讲我们实际验证过的事实:怎么把“被墙”和“客户端不兼容”分开,我们试过哪些办法、结果如何,以及——同样重要——哪些我们并没有证实

    症状

    • 原本稳定的节点,突然开始大面积超时。
    • 换网络、换设备、换运营商,现象都差不多。
    • TCP 端口不一定完全不通,但客户端就是连不上。
    • 直觉上很像“IP 被盯上了”。

    第一步:别急着判定“被墙”

    关键的一点:“卡在 TLS / REALITY 握手”这个现象,被墙和协议不兼容都会产生,单看它分不出来。

    还有两个常见误区要避开:

    • 普通 TLS 工具的失败不能当证据。对 REALITY 入站,curl / openssl s_client 本来就完不成握手(REALITY 会拒绝非授权握手),它们报错是正常的。
    • 要看内核自己的日志——xray / mihomo / sing-box 的连接日志,而不是外部工具的输出。

    既然现象分不出来,就得做对照实验,把变量一个个拿掉。

    决定性证据:在服务端本机走回环,换内核对照

    这是整次排查里最有说服力的一步,也是我们真正确证的事实。

    做法:在服务器本机,用不同的客户端内核、同样的参数,去连本机的 127.0.0.1 入站。这条路径不出公网、不经过任何跨境链路,等于把“网络封锁”这个变量直接拿掉,只剩下“客户端内核”这一个变量。

    同一台机、同一时刻、同一个 127.0.0.1:443,结果是:

    客户端内核(回环连本机入站) 结果
    xray-core(与服务端同源、同版本) ✅ 握手成功
    mihomo(与客户端同版本) ❌ 握手失败

    结论很直接:这是“客户端内核 ↔ 服务端 Xray”的兼容性问题,跟 IP、跟网络封锁无关——因为这条路径根本没碰网络。回头一查,服务端的 Xray 确实在不久前被升级到了一个很新的大版本;而我们手上的旧客户端内核(至少本次测试的 mihomo / sing-box)跟不上它的 REALITY 了。

    这也把“是不是被墙”摘了出去:回环这条路根本不出网络,等于把“封锁”这个变量排除在外。在我们这台机器上 IP 也确实没被封,于是问题就锁定在内核兼容性上。

    我们在客户端侧试过的办法,都没把它救回来

    很多人的第一反应是“升级一下客户端就好”。我们试了,对 mihomo / sing-box 不管用。以下都在服务端回环 / 链路上实测过:

    尝试 结果
    xray-core(直连 / 回环) ✅ 成功(唯一能过的)
    mihomo 最新 stable + 最新 Alpha ❌ 失败
    mihomo,7 种 uTLS 指纹逐个试 ❌ 全失败
    mihomo,显式打开 support-x25519mlkem768 开关 ❌ 仍失败
    sing-box(较新 uTLS 的版本) ❌ 失败

    在我们这组测试里,唯一能稳定握上手的就是 xray-core,而且是在直连 / 回环下。所以实用的结论是:如果你直连节点、用的又是 Clash / mihomo,单纯升版本(包括打开那个常被怀疑的后量子开关)在我们这里没救回来——换一个 xray-core 内核的客户端,是我们这里唯一验证有效的办法。

    如果你是经中继(proxy chain)连节点:客户端这条路走不通

    有一类很常见的用法:节点是一个固定的境外出口,永远从一个墙外的前置跳板接进去,也就是 本地 → 前置跳板 → 境外出口 这条 proxy chain。

    先说清楚:REALITY 本身并不是“不能被中继”——Xray 的 dialerProxy 本就支持链式拨号,把 REALITY 套在前置代理后面是常见用法。但在我们这台升级后的服务器上,我们试过的组合都没能让 REALITY 经中继跑通

    • xray-core 的 REALITY 经前置跳板转发到服务端——握手没成功(试过的两条中继表现还不一样:一条直接 EOF,一条到达服务端鉴权阶段后被判无效连接)。
    • 同一条中继转发普通 TLS 到同一端口却能通——说明中继能到达服务端、也扛得住 TLS——至少能把问题收窄到“这版 REALITY 经该中继转发”这一段(确切卡点我们没独立证实,见下文)。
    • 我们把客户端这边能试的组合(不同内核单独及混用、不同指纹、那个后量子开关)都试了,没有一个能让 REALITY 经中继接通

    所以在“必须走中继”的场景里,客户端这条路我们没能走通。对症的做法是从服务端入手:把出口那一跳换成一个能被正常中继的协议(纯 TCP、两端内核都支持就行)。换掉之后,经前置跳板接入即可恢复。

    顺带排除一个坑:在我们试过的 TCP 类中继上,Hysteria2 这类 UDP 协议也不合适。它是 UDP/QUIC,经 TCP 类中继要做 UDP-over-TCP 封装,而我们试的商业中继并不还原这种封装,照样超时。UDP 系协议(Hy2 / TUIC)更适合直连,要这样经 TCP 类中继转发往往不顺。

    我们没有证实的部分(同样要讲清楚)

    为了不把推测当结论,下面这些我们并没有坐实:

    • 没有证实“元凶就是后量子”。社区里常见一种归因:把这一轮“Xray 升级后旧客户端连不上”算到新版默认带上的后量子能力头上(如 X25519MLKEM768 密钥交换、以及可选的 ML-DSA-65 认证)。这与我们看到的现象不矛盾、也是重点怀疑对象;但我们这台服务器的入站当时其实没有显式的后量子字段,而且把客户端的 support-x25519mlkem768 打开也没用。所以我们能下的最稳结论只到——“新版 Xray 的 REALITY 与旧客户端内核不兼容”,至于具体卡在哪个特性,没有独立验证。
    • 没有证实 REALITY 经中继失败的确切机理。一个合理的猜测是:REALITY 的鉴权信息嵌在 ClientHello 里、对 TCP 分帧敏感,经中继重新分段后被破坏了——但这只是推断,我们也没排除中继自身实现的影响。

    结论:一套可复制的判断流程

    1. “卡在握手”既不能证明是 GFW、也不能证明不是——别靠现象拍脑袋。
    2. 服务端本机走 127.0.0.1 回环、换不同客户端内核对照:如果结果按内核分成败,就能确认存在协议兼容性问题,且这一部分换 IP、加中继都解决不了(这条路径不碰网络,封锁与否被排除在外)。
    3. 如果只有 xray-core 能连,多半是服务端 Xray 升级后、旧客户端内核没跟上。直连场景 → 换 xray-core 内核的客户端;走中继场景 → 从服务端把出口换成能被中继的协议。

    一句话:这类故障也许不是“IP 被墙”,而是“服务端升级了、客户端没跟上”。在下结论、急着换 IP 之前,先用回环对照把“协议不兼容”这条单独验掉。

    参考资料

    1. XTLS / Xray-core, PR #4915 “REALITY protocol: Add optional Post-Quantum ML-DSA-65 verification”(后量子认证的来源)
      https://github.com/XTLS/Xray-core/pull/4915
    2. XTLS / Xray-core, transport/internet/reality/reality.gomain 分支会漂移,复核时建议钉到具体 release tag)
      https://github.com/XTLS/Xray-core/blob/main/transport/internet/reality/reality.go
    3. MetaCubeX / mihomo v1.19.9 release(加入 support-x25519mlkem768
      https://github.com/MetaCubeX/mihomo/releases/tag/v1.19.9
    4. sing-box documentation, “TLS Shared Fields”
      https://sing-box.sagernet.org/configuration/shared/tls/
    5. NIST FIPS 203, “Module-Lattice-Based Key-Encapsulation Mechanism Standard”
      https://csrc.nist.gov/pubs/fips/203/final
    6. NIST FIPS 204, “Module-Lattice-Based Digital Signature Standard”
      https://csrc.nist.gov/pubs/fips/204/final

    致谢与署名

    本文的排查与初稿由 Claude Code 完成:复现“全线 timeout”、区分“TCP 连通性”与“握手失败”、在服务端走 127.0.0.1 回环用不同客户端内核做对照实验,把病根定位到「新版 Xray REALITY 与旧客户端内核不兼容」,并实测发现这版 REALITY 经我们试过的中继转发都跑不通(同一条中继转发普通 TLS 却能过)。

    站点维护者(本博客作者)做了几处关键决断与一手事实纠正——尤其是坚持“这个 IP 其实没被封”的判断(正是它把方向从“绕中继对抗封锁”拉回到“换协议 / 对齐版本解决兼容性”),并在 Windows / iOS 上做了实测验证。

    技术准确性、逻辑严谨度与引用规范的审校由 codex(OpenAI Codex) 完成:对论断逐条复核(包括把若干超出证据的措辞收紧),并校准了术语与参考文献。

  • Windows 自带的巨强大的拷贝工具:robocopy

    使用手册:
    https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy

    TL;DR 的命令例是(e 代表包含空文件夹,mt 代表线程数):
    robocopy e:\crowdsource-webapp e:\abc /e /mt:8

    至于原理,其实很简单:多线程复制

    下面截图的是复制大量小文件(node_modules)的成绩,作为对比的是,在windows 图形界面下,15 分钟只复制了 56%。而用 robocopy 10 分钟复制完成。

  • Docker, 微服务与开箱即用的反思

    做了一天的 SSL 部署工作,会反思到 docker 在某些情况下是不是真的带来了便利。比如今天的工作,这个项目的 API 和 frontend 写在了一起,都打在一个 docker 里面。这是大前提。
    于是做 SSL 的时候,API 端口和 frontend 端口要分别反代、配置 SSL、暴露给外网。换句话说,frontend 里面对 API 的访问需要基于暴露给外网的配置,而不是 docker 里面 localhost 的 API 端口。开发方面的麻烦其实还好。在部署上会比较讨厌,第三方用户拿到这个 docker image 之后并不能直接用,而是需要先把反代配置好。于是开箱即用便成了空话。
    反过来想,其实这并不是一个贯彻 docker 思想的部署。docker 思想的核心是微服务化。API 和 frontend 拆开是更符合 docker 思想的方案。在我自己的项目里面,这一点会做得比较好。不过另一方面,如果拆开了,开箱即用仍然是一句空话。
    深入这个问题的话,万恶之源是什么呢? – SSL。如果 https 访问 http 的 API 没有问题,那怎么设计都是无痛的。
    面向未来和解决方案的话,希望有办法在后端开发的时候直接可以方便地载入 cert,使得后端端口原生就是 ssl。以此来避免 mixed content(https 混 http 导致内容无法访问)的情况。
    面向一并倒掉脏水和孩子的话,拥抱静态化可以解决所有问题~ 我爱 github 和 netlify~ 但 docker 仍然有无可替代的作用,比如我会希望通过不超过五步 step by step 的帮助,让任何没有技术背景的人能在本地部署一个独享的、高速的服务器(比如未来的 CBDB)。这是静态化无法做到的事情。