REALITY
一份真正从零开始的协议学习手册——
如果你只听说过 TLS 但不知道它内部怎么工作,从这里开始。
第〇章
没加密之前:明文通信的困境
在讨论 REALITY 之前,必须回到原点——两台计算机之间,数据是怎么"流"过去的。
想象你在咖啡馆连上 Wi-Fi,打开浏览器访问 google.com。你的电脑先通过 DNS 查到 Google 服务器的 IP 地址,然后发起一个 TCP 连接。TCP 在两端之间建立了一条"可靠的双向字节流"——你可以往里面写字节,对方按顺序收到同样的字节。
但问题来了:TCP 本身不加密。任何能看到这条链路的人——咖啡馆路由器、你的 ISP、骨干网上的中间设备——都能读到你的请求和 Google 返回的内容。这就是明文通信:数据像明信片一样在网络上传输,任何人路过都能看一眼。
图 0‑1:TCP 连接上传输的数据未经加密,路径上的任何设备都可以读取和修改。
解决问题的思路很直接:在 TCP 这条明信片信道之上,再盖一层加密。这就是 TLS(Transport Layer Security)的定位——它在 TCP 和应用数据之间插入一个"加密层",让上方所有的 HTTP 请求都变成密文。
但这提出了一连串新问题:加密需要密钥,密钥怎么在不安全的信道上安全地交给对方?这是整个现代密码学的核心问题,也是我们接下来要逐层拆解的内容。
第一章
TCP 三向握手:两台机器如何"认识"对方
这是所有网络通信的起点。三向握手建立了字节流的"管道",但它不关心里面流的是什么。
两台计算机之间并没有"连接"这个物理概念——它们之间只有 IP 数据包在路由。TCP 的"连接"是一个状态机幻觉:双方各自维护一个状态(LISTEN → SYN_RCVD → ESTABLISHED),通过三次消息交换同步这个状态。
-
SYN:客户端说"我想和你建立连接"
客户端发送一个 SYN 包,包含一个随机的 32 位初始序列号 ISN(c)。这个包告诉服务端:"我准备开始传数据了,我的第一个字节编号将是 ISN(c)+1。"
-
SYN-ACK:服务端说"好的,我也准备好了"
服务端回复 SYN-ACK,包含自己的初始序列号 ISN(s),并确认 ISN(c)+1。这个包同时完成了两个动作:确认收到了客户端的 SYN,并发送自己的 SYN。
-
ACK:客户端说"收到,开始传数据吧"
客户端发送 ACK 确认 ISN(s)+1。从这一刻起,双方进入了 ESTABLISHED 状态,可以双向发送数据。
图 1‑1:TCP 三向握手建立连接。三次消息之后,双方进入 ESTABLISHED 状态,可以传输任意字节。
注意:TCP 不知道也不关心你传的是 HTTP 文本、TLS 加密数据还是视频流。TLS 是在 TCP 这条管道之上叠加的额外协议——它在管道里建了一个"加密子管道"。理解这种分层关系,是理解 REALITY 的第一前提。
第二章
对称加密:同一个密钥,锁上和打开
如果用一把钥匙锁门,就必须把同一把钥匙交给对方才能开门——这就是对称加密的核心困境。
对称加密的原理简单到可以一句话讲清楚:加密和解密使用同一个密钥。AES(Advanced Encryption Standard)是目前最广泛使用的对称加密算法。它接受一个密钥(128、192 或 256 位)和一个明文块(128 位),输出一个密文块;同一个密钥反过来运算,从密文恢复明文。
但这里有一个致命的逻辑漏洞:密钥怎么传递?如果 Alice 和 Bob 之间没有安全的信道,Alice 怎么把密钥告诉 Bob?她不能直接在网络上发送——如果信道不安全,密钥本身也会被窃听者截获。
TLS 的解决方案是:用非对称加密来协商对称密钥。这就是下一章要讲的内容——但先让我们把对称加密本身拆清楚,因为它是 REALITY 协议栈中最频繁使用的密码学原语。
图 2‑1:AES-256-GCM 的加密和解密流程。GCM 是 AEAD(Authenticated Encryption with Associated Data)模式,一次运算同时提供加密和完整性校验。
TLS 1.3 支持三种对称密码套件:AES-128-GCM-SHA256、AES-256-GCM-SHA384、ChaCha20-Poly1305-SHA256。REALITY 完整支持这三种,与正常 TLS 1.3 完全一致——在密码套件层面没有任何指纹差异。
第三章
密钥交换:在不安全的信道上协商密钥
这是整个 TLS 协议最精妙的部分——双方从未直接发送密钥,却最终持有同一个密钥。
对称加密的前提是双方有同一个密钥。但如何在不安全的信道上协商出这个密钥?1976 年,Diffie 和 Hellman 提出了一个革命性的想法:用单向函数。
DH(Diffie-Hellman)协议的核心思想是:某些数学运算是单向的——正向计算很容易,反过来(已知结果求输入)在计算上不可行。Alice 和 Bob 各自生成一个秘密值,公开一个"公钥",然后各自用自己的秘密值结合对方的公钥,得到相同的共享密钥。窃听者看到了双方公开的所有数据,却无法高效计算出共享密钥。
图 3‑1:ECDH 密钥交换。G 是椭圆曲线的基点;a 和 b 是随机私钥;A 和 B 是公钥。双方各自用私钥乘以对方公钥,得到相同的共享密钥。
REALITY 使用的具体 ECDH 实例是 X25519——Curve25519 椭圆曲线上的 DH 密钥交换。选择它的理由:
- 32 字节的密钥极小(RSA-2048 需要 256 字节)
- 运算极快(约 10⁶ 次/秒)
- 128 位安全强度,设计上避免了许多经典 ECDH 的陷阱
- 每个 X25519 操作产生 前向保密性:私钥用完即弃,历史会话无法被解密
但 X25519 只产生共享密钥——它不认证对方的身份。攻击者可以站在中间,分别与 Alice 和 Bob 建立自己的 ECDH。如何防止?需要数字签名——我们稍后会讲。
authKey,用于验证客户端身份;第二次(服务端临时私钥 × 客户端临时公钥)产生 sharedKey,用于 TLS 1.3 密钥派生链。前者控制"谁能连接",后者保证"会话安全"。
第四章
TLS 1.3 握手:把三把锁串起来
前面三章的组件——TCP 连接、对称加密、密钥交换——在 TLS 1.3 握手中首次被组合成一个完整的"安全会话"。
TLS 1.3 的一次完整握手(不含 PSK 恢复)涉及四次消息往返(2-RTT)。以下是简化但保持准确性的流程:
-
ClientHello → 客户端亮出牌面
客户端发送:支持的 TLS 版本、密码套件列表、
key_share(至少一个 X25519 公钥)、SNI(要访问的域名)、ALPN(上层协议如 h2/http/1.1)。这些字段中除 key_share 外多数字段是明文——这是后面指纹问题的根源。 -
ServerHello → 服务端做出选择
服务端在 ClientHello 的菜单中点菜:选一个密码套件、选一个 key_share 组、填上自己的 X25519 公钥。至此,ECDH 共享密钥
sharedKey已经在双方独立算出。 -
加密扩展 + 证书 + 签名 + Finished → 服务端的飞行动作
ServerHello 之后所有消息都被 AEAD 加密。服务端发送:EncryptedExtensions(确认 ALPN)、Certificate(X.509 证书链)、CertificateVerify(用证书私钥对握手摘要签名)、Finished(密钥确认)。
-
Client Finished → 客户端确认并开始传输应用数据
客户端验证证书链和签名后,发送自己的 Finished。此后,双方切换到应用数据密钥,所有 HTTP 请求都以 AEAD 密文形式传输。
图 4‑1:TLS 1.3 全握手流程。{} 表示加密消息。注意 ServerHello 之后的所有握手消息都被加密——这正是 REALITY "借壳"的关键窗口。
现在梳理一下 TLS 1.3 在密码学上的三个"锁":
第一把锁:密钥交换(ECDH)——在不安全信道上协商出共享密钥,且每次握手临时生成(前向保密)。
第二把锁:数字签名(CertificateVerify)——服务端用证书私钥签名握手摘要,证明"我是这个域名的合法持有者"。客户端沿 CA 证书链验证。
第三把锁:AEAD 加密(GCM/Poly1305)——用派生出的对称密钥加密所有应用数据,同时提供完整性保护。
第五章
TLS 指纹:代理为什么会被检测到
加密了内容不等于隐藏了身份。TLS 握手本身暴露了大量可用于识别的"元特征"。
一个 TLS 代理(如传统的 V2Ray + TLS 模式)在网络上呈现的特征与一个正常网站截然不同。审查者不需要解密——他只需要观察握手的外部结构:
| 观察点 | 正常网站 (example.com) | 传统 TLS 代理 |
|---|---|---|
| 证书颁发者 | Let's Encrypt / DigiCert | 自签名 / 未知 CA |
| 证书 Subject | example.com (真实域名) | 随机 CN / IP 地址 |
| SNI | example.com | 代理自己的域名 (myproxy.xyz) |
| ServerHello 密码套件偏好 | 取决于 Nginx / Apache 版本 | 取决于代理软件 (Go / OpenSSL 默认) |
| TLS 扩展顺序 | 稳定、可预测 | 库依赖,常不同于主流 |
这些差异不是因为"不安全"——传统 TLS 代理在密码学上同样是安全的——而是在于它暴露了自己是一台代理服务器,而不是一个普通网站。审查者看到这些差异后,可以做出两个动作:
① 主动探测:向可疑 IP 的 443 端口发起一个 TLS 握手,如果返回的证书不符合预期(比如 CN 是一个随机 ID),标记为代理 IP。
② 被动监听:在骨干网上抓取 TLS ClientHello,如果发现某个 IP 上的 SNI 分布异常(大量不同域名但指向同一个 IP),标记为代理节点。
第六章
REALITY 的核心架构:"借壳"的哲学与实现
不部署自己的证书,不暴露自己的指纹。REALITY 服务端在 TLS 握手中"扮演"目标网站。
REALITY 服务端在启动时做了三件事,这三件事决定了它后续所有行为:
-
不部署证书——生成一个全局 Ed25519 密钥对
在
init()中,REALITY 调用ed25519.GenerateKey(rand.Reader)生成一个全局密钥对。用这个密钥对创建一个空白的自签名 X.509 证书模板(SerialNumber=0, Subject 为空)。这个证书不是用来验证域名的——它只是一张"白纸",后面会填入认证信息。 -
拨号到目标网站——建立回落通道
服务端向配置的
target(如example.com:443)发起一个 TCP 连接并保持。如果客户端认证失败,流量将直接通过这个连接转发到目标网站——从外部看,就像客户端的 TLS 连接被正常路由到了 example.com。 -
读取 ClientHello——开始"扮演"
服务端读取客户端发来的 ClientHello。它检查 SNI 是否在
serverNames列表中。然后提取第一个 X25519 key_share 中的公钥,与自己的privateKey执行 X25519 得到authKey。
图 6‑1:REALITY 握手的认证阶段。客户端的 X25519 公钥通过 ClientHello.key_share 到达;服务端用私钥算出 authKey,然后通过 AES-GCM 解密 sessionId 中的认证信息。认证失败则透明回落至目标网站。
认证通过后,服务端进入标准的 TLS 1.3 握手——但有一个关键区别:它发送的证书不是从 CA 购买的,而是动态生成的 Ed25519 临时证书。这张证书:
- 由 Ed25519 密钥签名——这在 TLS 1.3 中完全合法
- 证书的后 64 字节被替换为
HMAC-SHA512(authKey, ed25519PubKey) - 客户端用自己独立计算出的 authKey 验证 HMAC → 确认为 REALITY 证书
- 中间人看到的是一个结构正常、签名合法的证书,无法判断其背后还有一层 HMAC
第七章
逐字节跟踪一次 REALITY 握手
从 ClientHello 的第一个字节到 Finished 的最后一个字节,每一步的字节级变化。
以下是一次完整 REALITY 握手的精确步骤。每一步都对应源代码中的具体函数和变量。
-
ClientHello 到达 · 提取 X25519 公钥
服务端从 ClientHello 的
key_share扩展中提取第一个X25519(或X25519MLKEM768)条目中的 32 字节公钥。如果找到X25519MLKEM768,取后 32 字节(跳过 ML-KEM 的 1184 字节封装密钥)。
源码:handshake_server_tls13.go:93-105 -
第一次 X25519 → rawAuthKey
rawAuthKey = X25519(config.PrivateKey, clientX25519Pub)。config.PrivateKey 是服务端的长期 X25519 私钥(由./xray x25519生成)。这是 REALITY 唯一依赖的长期密钥。
源码:tls.go:225 -
HKDF-SHA256 派生 → authKey
authKey = HKDF-SHA256(ikm=rawAuthKey, salt=clientRandom[:20], info="REALITY")。Salt 取 ClientHello.random 的前 20 字节——这确保了即使客户端重连,每次的 authKey 也不同。
源码:tls.go:226 -
AES-256-GCM 解密 sessionId → 客户端身份信息
plainText = AES-256-GCM-Decrypt(key=authKey, nonce=clientRandom[20:32], ciphertext=sessionId, aad=整个ClientHello原始字节)。解密后得到 32 字节:ClientVer[3] | ClientTime(Unix秒)[4] | ClientShortId[8] | 预留[17]。
源码:tls.go:231-234 -
三重验证
① 版本号:
minClientVer ≤ ClientVer ≤ maxClientVer
② 时间差:|serverTime - ClientTime| ≤ maxTimeDiff
③ shortId:shortIds[ClientShortId] == true
任一失败 → 流量透明转发至目标网站。注意 shortId 不是密码——它只是一个索引,真正的安全源自 X25519 密钥交换。 -
第二次 X25519 → sharedKey(TLS 密钥协商)
服务端生成新的临时 X25519 密钥对。公钥填入 ServerHello.key_share。与客户端的临时公钥执行 ECDH 得到
sharedKey。这个密钥进入 TLS 1.3 标准密钥派生链。
源码:handshake_server_tls13.go:108-122 -
临时证书生成 → Ed25519 + HMAC
服务端复制启动时预生成的 Ed25519 证书模板。然后:
HMAC-SHA512(authKey, ed25519PubKey) → 64字节写入证书末尾的偏移位置。
再用 Ed25519 私钥对 TLS 握手摘要签名 → CertificateVerify。
源码:handshake_server_tls13.go:132-150 -
加密通道建立
ServerHello 之后的 Certificate、CertificateVerify、Finished 全部以 AEAD 加密发送。客户端用自己独立算出的 authKey 验证 HMAC → 确认证书可信。TLS 1.3 握手完成,应用数据开始传输。
第八章
密码学工具箱:七个密码学原语逐一拆解
REALITY 不是一个密码学发明——它是七个久经考验的密码学原语的巧妙组合。理解每一个,才真正理解协议。
① X25519 ECDH — 密钥协商的发动机
基于 Curve25519 椭圆曲线的 Diffie-Hellman。私钥 32 字节随机数,公钥 32 字节压缩点。一次 ECDH 得到 32 字节共享密钥。128 位安全强度。前向保密:每次握手使用新的临时密钥对,长期密钥泄露不影响历史会话。REALITY 使用两次 X25519:第一次产生 authKey,第二次产生 sharedKey。
② HKDF-SHA256 — 从"还行随机"到"完美均匀"
HMAC-based Key Derivation Function (RFC 5869)。X25519 的共享密钥虽然不可预测,但熵分布不一定完美均匀。HKDF 通过两次 HMAC(Extract 阶段和 Expand 阶段)将原始密钥材料"洗"成均匀分布的伪随机密钥。REALITY 用它从 rawAuthKey 派生 authKey,信息字符串 "REALITY" 用作域分离。
③ AES-256-GCM — sessionId 的铠甲
要理解 REALITY 为什么选 GCM 模式:它不仅加密(机密性),还生成一个 16 字节的认证标签(Authentication Tag),校验数据完整性。REALITY 将整个 ClientHello 原始字节作为 AAD(附加认证数据)——这意味着即使攻击者篡改了 ClientHello 的任何字段,解密都会失败。Nonce 取 clientRandom[20:32],确保同一密钥不会对同一条消息使用两次。
图 8‑1:AES-256-GCM 的输入输出。整个 ClientHello 作为 AAD——任何篡改都会被检测到。
④ Ed25519 — 轻量而强大的证书签名
Ed25519 是 Edwards 曲线上的 Schnorr 签名变体。相比 ECDSA:密钥更小(32 字节 vs 32 字节但 ECDSA 签名可变长)、签名速度更快、无分支代码(抗侧信道)、确定性的 nonce(不需要高质量随机数)。TLS 1.3 原生支持 Ed25519,所以 REALITY 的临时证书在协议层面完全合法。
⑤ HMAC-SHA512 — 证书的"隐形认证码"
这是 REALITY 最巧妙的设计:证书末尾 64 字节被替换为 HMAC-SHA512(authKey, ed25519PubKey)。HMAC 是一个"带密钥的哈希"——只有知道 authKey 的人才能验证这段数据的正确性。对客户端来说,验证 HMAC 就等价于验证"这张证书由持有相同 privateKey 的服务端签发"。对中间人来说,64 字节看起来只是证书中的随机填充。
⑥ TLS 1.3 密钥派生链 — HKDF 的体系化应用
TLS 1.3 使用 HKDF 构建了一个七层密钥层级:PSK → Early Secret → Handshake Secret → Master Secret。每层注入新的密钥材料(如 ECDH sharedKey),然后派生出一对握手流量密钥和一对应用流量密钥。REALITY 完整保留这一体系——在密钥派生层面,REALITY 与标准 TLS 1.3 完全等同。
⑦ ML-KEM-768 + ML-DSA-65 — 抗量子的未来防线
当量子计算机能够运行 Shor 算法时,X25519 和 Ed25519 都将不再安全。REALITY 通过两个 FIPS 标准化的后量子原语提供可选保护:ML-KEM-768(密钥封装)和 ML-DSA-65(数字签名),均由 cloudflare/circl 库实现。两者都基于 Module-Lattice 假设——目前认为量子计算机难以高效求解。
当客户端和服务端都启用 ML-KEM 时,sharedKey 变为 64 字节:前 32 字节来自 ML-KEM-768 的共享密钥,后 32 字节来自 X25519。混合模式确保即使 PQC 原语被攻破,经典 ECDH 仍提供保护。
第九章
安全性分析:REALITY 能做到什么、做不到什么
没有任何安全方案是完美的。理解 REALITY 的边界,才能正确使用它。
| 安全属性 | 状态 | 说明 |
|---|---|---|
| 前向保密 | ✅ 等同 TLS 1.3 | 每次握手使用临时 X25519 密钥对,长期私钥泄露不解密历史会话。 |
| 服务端指纹消除 | ✅ 消除 TLS 层指纹 | 证书、密码套件、扩展均与目标网站一致。Ed25519 签名在 TLS 1.3 中合法。 |
| 证书链攻击 | ✅ 完全免疫 | 客户端不信任标准 X.509 验证结果,而是用 HMAC 认证 REALITY 证书。 |
| 主动探测 | ✅ 与目标网站一致 | 中间人看到的是一次到目标网站的正常 TLS 1.3 连接。 |
| TCP/IP 层指纹 | ⚠️ 不在范围内 | TTL、TCP 窗口大小、IP 地理位置等底层特征不在 REALITY 保护范围内。 |
| 时序侧信道 | ⚠️ 持续改进中 | ECDH 运算时间、证书签名时间可能产生微小的时序差异。 |
| 流量分析 | ⚠️ 不在范围内 | 数据包大小模式、发送间隔等仍可被统计关联。 |
- shortIds 必须每个客户端唯一——这是客户端之间的唯一标识符。重复使用会削弱不可关联性。
- spiderX 也应每个客户端不同——爬虫路径参与证书识别状态机。
- 目标网站选择:支持 TLS 1.3 + H2 的国外网站,非跳转域名,理想上 ServerHello 后握手消息被一起加密(如 dl.google.com)。
- 不要开启回落限速——限速是一种可检测的特征。