XTLS 项目 · 渐进式学习手册 GitHub

REALITY

一份真正从零开始的协议学习手册——
如果你只听说过 TLS 但不知道它内部怎么工作,从这里开始。

从零开始 直接看加密算法

第〇章

没加密之前:明文通信的困境

在讨论 REALITY 之前,必须回到原点——两台计算机之间,数据是怎么"流"过去的。

想象你在咖啡馆连上 Wi-Fi,打开浏览器访问 google.com。你的电脑先通过 DNS 查到 Google 服务器的 IP 地址,然后发起一个 TCP 连接。TCP 在两端之间建立了一条"可靠的双向字节流"——你可以往里面写字节,对方按顺序收到同样的字节。

但问题来了:TCP 本身不加密。任何能看到这条链路的人——咖啡馆路由器、你的 ISP、骨干网上的中间设备——都能读到你的请求和 Google 返回的内容。这就是明文通信:数据像明信片一样在网络上传输,任何人路过都能看一眼。

你的电脑 Google 服务器 中间设备(路由器 / ISP / 审查者) 可以看到: GET /search?q=... HTTP/1.1 明文传输的数据流 ▸

图 0‑1:TCP 连接上传输的数据未经加密,路径上的任何设备都可以读取和修改。

解决问题的思路很直接:在 TCP 这条明信片信道之上,再盖一层加密。这就是 TLS(Transport Layer Security)的定位——它在 TCP 和应用数据之间插入一个"加密层",让上方所有的 HTTP 请求都变成密文。

但这提出了一连串新问题:加密需要密钥,密钥怎么在不安全的信道上安全地交给对方?这是整个现代密码学的核心问题,也是我们接下来要逐层拆解的内容。

关键认知 REALITY 之所以存在,是因为即使有了 TLS,仍然有办法从加密流量的外部特征(而非内容)识别出"这不是正常的 TLS 连接"。理解 REALITY,就是理解 TLS 的指纹从何而来,以及如何消除它们。

第一章

TCP 三向握手:两台机器如何"认识"对方

这是所有网络通信的起点。三向握手建立了字节流的"管道",但它不关心里面流的是什么。

两台计算机之间并没有"连接"这个物理概念——它们之间只有 IP 数据包在路由。TCP 的"连接"是一个状态机幻觉:双方各自维护一个状态(LISTEN → SYN_RCVD → ESTABLISHED),通过三次消息交换同步这个状态。

  1. SYN:客户端说"我想和你建立连接"

    客户端发送一个 SYN 包,包含一个随机的 32 位初始序列号 ISN(c)。这个包告诉服务端:"我准备开始传数据了,我的第一个字节编号将是 ISN(c)+1。"

  2. SYN-ACK:服务端说"好的,我也准备好了"

    服务端回复 SYN-ACK,包含自己的初始序列号 ISN(s),并确认 ISN(c)+1。这个包同时完成了两个动作:确认收到了客户端的 SYN,并发送自己的 SYN。

  3. ACK:客户端说"收到,开始传数据吧"

    客户端发送 ACK 确认 ISN(s)+1。从这一刻起,双方进入了 ESTABLISHED 状态,可以双向发送数据。

客户端 状态: ESTABLISHED 服务端 状态: ESTABLISHED ① SYN (seq=ISN(c)) ② SYN-ACK (seq=ISN(s), ack=ISN(c)+1) ③ ACK (ack=ISN(s)+1) ▼ 连接建立。此后所有 TCP 段都是纯载荷数据 双向数据流(HTTP / TLS / 任意上层协议)

图 1‑1:TCP 三向握手建立连接。三次消息之后,双方进入 ESTABLISHED 状态,可以传输任意字节。

注意:TCP 不知道也不关心你传的是 HTTP 文本、TLS 加密数据还是视频流。TLS 是在 TCP 这条管道之上叠加的额外协议——它在管道里建了一个"加密子管道"。理解这种分层关系,是理解 REALITY 的第一前提。

第二章

对称加密:同一个密钥,锁上和打开

如果用一把钥匙锁门,就必须把同一把钥匙交给对方才能开门——这就是对称加密的核心困境。

对称加密的原理简单到可以一句话讲清楚:加密和解密使用同一个密钥。AES(Advanced Encryption Standard)是目前最广泛使用的对称加密算法。它接受一个密钥(128、192 或 256 位)和一个明文块(128 位),输出一个密文块;同一个密钥反过来运算,从密文恢复明文。

但这里有一个致命的逻辑漏洞:密钥怎么传递?如果 Alice 和 Bob 之间没有安全的信道,Alice 怎么把密钥告诉 Bob?她不能直接在网络上发送——如果信道不安全,密钥本身也会被窃听者截获。

TLS 的解决方案是:用非对称加密来协商对称密钥。这就是下一章要讲的内容——但先让我们把对称加密本身拆清楚,因为它是 REALITY 协议栈中最频繁使用的密码学原语。

明文 "Hello, World" AES-256-GCM Encrypt 密钥: K (256-bit) Nonce: N (96-bit) c7a3f9...3b2e (密文 + tag) 解密器用同样的 K + N 还原明文 GCM 模式额外提供: ✓ 机密性(Confidentiality):窃听者无法读出内容 ✓ 完整性(Integrity):任何篡改都会被 Authentication Tag 检测到 ✓ 认证加密(AEAD):上述两者在一次运算中同时完成

图 2‑1:AES-256-GCM 的加密和解密流程。GCM 是 AEAD(Authenticated Encryption with Associated Data)模式,一次运算同时提供加密和完整性校验。

TLS 1.3 支持三种对称密码套件:AES-128-GCM-SHA256AES-256-GCM-SHA384ChaCha20-Poly1305-SHA256。REALITY 完整支持这三种,与正常 TLS 1.3 完全一致——在密码套件层面没有任何指纹差异。

第三章

密钥交换:在不安全的信道上协商密钥

这是整个 TLS 协议最精妙的部分——双方从未直接发送密钥,却最终持有同一个密钥。

对称加密的前提是双方有同一个密钥。但如何在不安全的信道上协商出这个密钥?1976 年,Diffie 和 Hellman 提出了一个革命性的想法:用单向函数。

DH(Diffie-Hellman)协议的核心思想是:某些数学运算是单向的——正向计算很容易,反过来(已知结果求输入)在计算上不可行。Alice 和 Bob 各自生成一个秘密值,公开一个"公钥",然后各自用自己的秘密值结合对方的公钥,得到相同的共享密钥。窃听者看到了双方公开的所有数据,却无法高效计算出共享密钥。

Alice (客户端) Bob (服务端) 随机私钥 a (32B) 公钥 A = a × G ① 发送公钥 A(可被窃听) 随机私钥 b (32B) 公钥 B = b × G ② 发送公钥 B(可被窃听) sharedKey = a × B = a × (b × G) sharedKey = b × A = b × (a × G) 两者得到相同的 sharedKey = (a × b) × G 窃听者只有 A 和 B,无法高效计算 a 或 b(椭圆曲线离散对数问题)

图 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。如何防止?需要数字签名——我们稍后会讲。

REALITY 的双重密钥交换 REALITY 在握手中两次使用 X25519:第一次(服务端长期私钥 × 客户端临时公钥)产生认证密钥 authKey,用于验证客户端身份;第二次(服务端临时私钥 × 客户端临时公钥)产生 sharedKey,用于 TLS 1.3 密钥派生链。前者控制"谁能连接",后者保证"会话安全"。

第四章

TLS 1.3 握手:把三把锁串起来

前面三章的组件——TCP 连接、对称加密、密钥交换——在 TLS 1.3 握手中首次被组合成一个完整的"安全会话"。

TLS 1.3 的一次完整握手(不含 PSK 恢复)涉及四次消息往返(2-RTT)。以下是简化但保持准确性的流程:

  1. ClientHello → 客户端亮出牌面

    客户端发送:支持的 TLS 版本、密码套件列表、key_share(至少一个 X25519 公钥)、SNI(要访问的域名)、ALPN(上层协议如 h2/http/1.1)。这些字段中除 key_share 外多数字段是明文——这是后面指纹问题的根源。

  2. ServerHello → 服务端做出选择

    服务端在 ClientHello 的菜单中点菜:选一个密码套件、选一个 key_share 组、填上自己的 X25519 公钥。至此,ECDH 共享密钥 sharedKey 已经在双方独立算出。

  3. 加密扩展 + 证书 + 签名 + Finished → 服务端的飞行动作

    ServerHello 之后所有消息都被 AEAD 加密。服务端发送:EncryptedExtensions(确认 ALPN)、Certificate(X.509 证书链)、CertificateVerify(用证书私钥对握手摘要签名)、Finished(密钥确认)。

  4. Client Finished → 客户端确认并开始传输应用数据

    客户端验证证书链和签名后,发送自己的 Finished。此后,双方切换到应用数据密钥,所有 HTTP 请求都以 AEAD 密文形式传输。

客户端 服务端 ClientHello [明文] +key_share(A), SNI, ciphers, ALPN ServerHello [明文] +key_share(B), cipher=TLS_AES_128_GCM sharedKey = ECDH(a,B) = ECDH(b,A) ← 双方独立算出 {Encrypted Extensions} [加密] ALPN=h2 {Certificate} [加密] X.509 证书链 {CertVerify} [加密] 用私钥签名握手摘要 {Finished} [加密] HMAC(trafficSecret, transcriptHash) {Finished} [加密] 客户端密钥确认 ▼ 握手完成。应用数据以 AEAD 密文传输 {HTTP Request} [加密] GET /... {HTTP Response} [加密] <html>...

图 4‑1:TLS 1.3 全握手流程。{} 表示加密消息。注意 ServerHello 之后的所有握手消息都被加密——这正是 REALITY "借壳"的关键窗口。

现在梳理一下 TLS 1.3 在密码学上的三个"锁":

第一把锁:密钥交换(ECDH)——在不安全信道上协商出共享密钥,且每次握手临时生成(前向保密)。

第二把锁:数字签名(CertificateVerify)——服务端用证书私钥签名握手摘要,证明"我是这个域名的合法持有者"。客户端沿 CA 证书链验证。

第三把锁:AEAD 加密(GCM/Poly1305)——用派生出的对称密钥加密所有应用数据,同时提供完整性保护。

TLS 指纹从哪来? 仔细看图 4‑1 的 ClientHello 和 ServerHello——它们是明文的。ClientHello 里包含了客户端偏好的密码套件顺序、扩展列表、椭圆曲线列表、签名算法列表……这些信息组合在一起,形成了客户端的"指纹"。同样,ServerHello 的选择偏好、Certificate 的 CA 来源和结构,形成了服务端指纹。REALITY 的核心目标,就是消除服务端指纹。

第五章

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 的解决方案——一个思想实验 如果你能让你的代理服务器在 TLS 层面完全看起来像 google.com——包括它的证书结构、密码套件偏好、ALPN——那么审查者来探测时,看到的就是一个"正常的 Google 边缘节点"。他无法区分这是真的 Google 服务器,还是你的代理。这就是 REALITY 的核心洞察:不要隐藏,要模仿。借别人的壳。

第六章

REALITY 的核心架构:"借壳"的哲学与实现

不部署自己的证书,不暴露自己的指纹。REALITY 服务端在 TLS 握手中"扮演"目标网站。

REALITY 服务端在启动时做了三件事,这三件事决定了它后续所有行为:

  1. 不部署证书——生成一个全局 Ed25519 密钥对

    init() 中,REALITY 调用 ed25519.GenerateKey(rand.Reader) 生成一个全局密钥对。用这个密钥对创建一个空白的自签名 X.509 证书模板(SerialNumber=0, Subject 为空)。这个证书不是用来验证域名的——它只是一张"白纸",后面会填入认证信息。

  2. 拨号到目标网站——建立回落通道

    服务端向配置的 target(如 example.com:443)发起一个 TCP 连接并保持。如果客户端认证失败,流量将直接通过这个连接转发到目标网站——从外部看,就像客户端的 TLS 连接被正常路由到了 example.com。

  3. 读取 ClientHello——开始"扮演"

    服务端读取客户端发来的 ClientHello。它检查 SNI 是否在 serverNames 列表中。然后提取第一个 X25519 key_share 中的公钥,与自己的 privateKey 执行 X25519 得到 authKey

REALITY 客户端 REALITY 服务端 :443 目标网站 :443 ClientHello: key_share(pubA), SNI=google.com rawAuthKey = X25519(priK, pubA) authKey = HKDF-SHA256(ikm, rand[:20], "REALITY") AES-256-GCM.Decrypt(authKey, sessionId) → [Ver|Time|ShortId|Reserved] ✓ 认证通过 继续 TLS 1.3 握手(使用临时 Ed25519 证书) {加密应用数据} 若认证失败 → 直接转发到目标网站 回退:透明TCP转发

图 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 握手的精确步骤。每一步都对应源代码中的具体函数和变量。

  1. ClientHello 到达 · 提取 X25519 公钥

    服务端从 ClientHello 的 key_share 扩展中提取第一个 X25519(或 X25519MLKEM768)条目中的 32 字节公钥。如果找到 X25519MLKEM768,取后 32 字节(跳过 ML-KEM 的 1184 字节封装密钥)。
    源码:handshake_server_tls13.go:93-105

  2. 第一次 X25519 → rawAuthKey

    rawAuthKey = X25519(config.PrivateKey, clientX25519Pub)。config.PrivateKey 是服务端的长期 X25519 私钥(由 ./xray x25519 生成)。这是 REALITY 唯一依赖的长期密钥。
    源码:tls.go:225

  3. HKDF-SHA256 派生 → authKey

    authKey = HKDF-SHA256(ikm=rawAuthKey, salt=clientRandom[:20], info="REALITY")。Salt 取 ClientHello.random 的前 20 字节——这确保了即使客户端重连,每次的 authKey 也不同。
    源码:tls.go:226

  4. 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

  5. 三重验证

    ① 版本号:minClientVer ≤ ClientVer ≤ maxClientVer
    ② 时间差:|serverTime - ClientTime| ≤ maxTimeDiff
    ③ shortId:shortIds[ClientShortId] == true
    任一失败 → 流量透明转发至目标网站。注意 shortId 不是密码——它只是一个索引,真正的安全源自 X25519 密钥交换。

  6. 第二次 X25519 → sharedKey(TLS 密钥协商)

    服务端生成新的临时 X25519 密钥对。公钥填入 ServerHello.key_share。与客户端的临时公钥执行 ECDH 得到 sharedKey。这个密钥进入 TLS 1.3 标准密钥派生链。
    源码:handshake_server_tls13.go:108-122

  7. 临时证书生成 → Ed25519 + HMAC

    服务端复制启动时预生成的 Ed25519 证书模板。然后:
    HMAC-SHA512(authKey, ed25519PubKey) → 64字节 写入证书末尾的偏移位置。
    再用 Ed25519 私钥对 TLS 握手摘要签名 → CertificateVerify。
    源码:handshake_server_tls13.go:132-150

  8. 加密通道建立

    ServerHello 之后的 Certificate、CertificateVerify、Finished 全部以 AEAD 加密发送。客户端用自己独立算出的 authKey 验证 HMAC → 确认证书可信。TLS 1.3 握手完成,应用数据开始传输。

为什么中间人无法区分这是 REALITY 还是直连? 因为 REALITY 服务端的行为完全等同于一个标准的 TLS 1.3 服务端:接收 ClientHello → 选密码套件 → 发送 Ed25519 证书 → 签名握手摘要 → Finished。Ed25519 是 TLS 1.3 合法签名算法。证书结构正常。唯一嵌入的 HMAC 对没有 authKey 的第三方是完全随机且不可验证的。

第八章

密码学工具箱:七个密码学原语逐一拆解

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],确保同一密钥不会对同一条消息使用两次。

AES-256-GCM key: authKey (256-bit) nonce: clientRandom[20:32] AAD: clientHello.original (all bytes) 输入: sessionId (32B 密文) 输出: Ver|Time|ShortId|Res (32B 明文) 输出: 16B 认证标签(验证完整性) 若 tag 不匹配 → 解密失败 → 认证失败 → 回落

图 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)。
  • 不要开启回落限速——限速是一种可检测的特征。