REALITY 协议
简介
REALITY 是 Project X 设计的 TLS 握手层面流量伪装协议。它通过 fork Go 标准库 crypto/tls,在 TLS 1.3 握手中嵌入认证信息,使代理流量与普通 HTTPS 流量完全无法区分。本文将深入分析 REALITY 的完整设计、握手机制、认证流程和源码实现。
本文回答
- REALITY 协议的设计目标和工作原理
- REALITY 的 TLS 握手是如何实现认证和伪装的
- 服务端和客户端的完整交互流程
- 临时证书的生成和验证机制
- REALITY 的源码结构和关键实现
本文在项目中的位置
REALITY 是项目中最核心的安全组件,处于传输安全层。本文是理解整个项目反审查能力的核心文档。
适合读者
所有想深入理解 REALITY 协议设计和实现的开发者、安全研究人员。
前置知识
TLS 1.3 握手流程,ECDH 密钥交换,Ed25519 签名,HMAC 消息认证码,AEAD 加密。建议先阅读 背景知识:TLS 指纹 和 背景知识:GFW 审查机制。
文章理解难度
较难。涉及密码学原语、TLS 协议内部机制、Session ID 字段的巧妙利用。
重点
- REALITY 如何在 Session ID 中嵌入认证信息而不破坏 TLS 握手
- ECDH + HKDF 派生的 AuthKey 是整个认证体系的根
- 临时证书的证书签名替换机制
- 三层证书验证逻辑
难点
- Session ID 字段的修改涉及 TLS 记录的重新组装
- ECDH 密钥交换需要在 ClientHello 构造阶段确定
- 证书签名的 HMAC 替换需要理解 TLS 证书结构
上级文档和相关文档
- ../index.md:项目概述
- ../architecture.md:架构中 REALITY 的位置
- ../background/gfw-censorship.md:审查背景
- ../background/tls-fingerprinting.md:TLS 指纹
- ../background/mitm-attacks.md:MITM 攻击
主要证据
REALITY/目录全部源码(Go TLS 1.3 fork)xray-core/transport/internet/reality/reality.go(客户端和服务端集成)xray-core/transport/internet/reality/config.proto(配置定义)- REALITY README.md 中的配置示例和设计说明
前置知识补充
TLS 1.3 握手关键步骤
在理解 REALITY 之前,需要回顾 TLS 1.3 握手的关键步骤:
- ClientHello:客户端发送支持的密码套件、密钥共享(Key Share,包含客户端的 ECDH 公钥)、SNI 等信息
- ServerHello:服务端选择密码套件、回复密钥共享(包含服务端的 ECDH 公钥)
- EncryptedExtensions:服务端发送加密的扩展信息
- Certificate:服务端发送证书链
- CertificateVerify:服务端用证书私钥签名握手记录,证明拥有证书
- Finished:握手完成,开始应用数据
Session ID 字段
Session ID 是 ClientHello 中的一个字段,用于 TLS 会话恢复。在 TLS 1.3 中,这个字段实际上不再用于会话恢复(PSK 机制替代了它),但仍保留在协议中用于兼容性。
REALITY 利用了这个冗余字段——将认证信息嵌入 Session ID,既不影响 TLS 握手,也让外部观察者看到的 ClientHello 看起来正常。
REALITY 的设计目标
REALITY 需要达成以下三个看似互相矛盾的目标:
- 对审查方透明:TLS 握手在外部看起来必须是完全正常的 HTTPS 访问某个知名网站
- 对客户端可认证:REALITY 客户端必须能够确认"正在与 REALITY 服务端通信",而非被 MITM 攻击
- 对非客户端安全回落:审查方的主动探测或普通 HTTPS 访问应该被导向目标网站,无法区分代理和网站
关键设计选择
- 不使用真实 CA 证书:避免证书管理复杂度和 CA 层面的风险
- 使用目标网站的真实证书外观:服务端直接将目标网站的证书上下文暴露给客户端——客户端看到的 ServerHello 之后的握手消息与访问目标网站一致
- 在 Session ID 中完成认证:认证信息在 TLS 握手的第一步就嵌入,避免额外的认证步骤和特征
- 临时 Ed25519 证书:使用 Ed25519 签名算法签发临时证书,证书的"签名"实际上是认证密钥的 HMAC
完整握手流程
认证信息详解
Session ID 的结构
REALITY 将 32 字节的 Session ID 用作认证载体:
Session ID (32 bytes):
[0] - Xray 主版本号 (core.Version_x)
[1] - Xray 次版本号 (core.Version_y)
[2] - Xray 修订版本号 (core.Version_z)
[3] - 保留 (0)
[4:8] - 客户端时间戳 (Unix timestamp, BigEndian)
[8:16]- 加密的 ShortId (AEAD 加密)
[16:32] - 未直接使用或为后续扩展保留源码位置:xray-core/transport/internet/reality/reality.go 行 139-148
hello.SessionId = make([]byte, 32)
copy(hello.Raw[39:], hello.SessionId)
hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
copy(hello.SessionId[8:], config.ShortId)AuthKey 派生
AuthKey 是整个 REALITY 认证体系的根密钥。它的派生过程:
1. ECDH 密钥交换:
SharedSecret = ECDH(客户端ECDH私钥, 服务端X25519公钥)
2. HKDF 派生:
AuthKey = HKDF-SHA256(
IKM = SharedSecret,
Salt = ClientHello.Random[0:20],
Info = "REALITY"
)源码位置:xray-core/transport/internet/reality/reality.go 行 152-169
publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe
}
uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20],
[]byte("REALITY")).Read(uConn.AuthKey); err != nil {
return nil, err
}注意:当 uTLS 指纹支持后量子密钥交换(X25519MLKEM768)时,使用混合密钥交换的共享秘密。这时 Ecdhe 为 nil,使用 MlkemEcdhe。
ShortId 加密
ShortId 在嵌入 Session ID 前使用 AuthKey 进行 AEAD 加密:
aead := crypto.NewAesGcm(uConn.AuthKey)
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)- 加密算法:AES-GCM(AuthKey 作为密钥)
- Nonce:ClientHello.Random 的第 20-32 字节(12 字节,标准 GCM Nonce 长度)
- 明文:Session ID 的前 16 字节
- 关联数据:完整的 ClientHello Raw 字节
加密后的数据写回 Session ID 前 16 个字节,然后通过 copy(hello.Raw[39:], hello.SessionId) 写回原始 ClientHello 字节。
raw 字节修改
这里有一个关键细节:REALITY 修改了 uTLS 构造的 ClientHello 的 Raw 字节。Raw 是完整的 ClientHello 消息字节表示,而 hello.SessionId 指向 Raw 内部的一个切片,修改 hello.SessionId 就是修改 Raw。所以通过 copy(hello.Raw[39:], hello.SessionId) 将加密后的 Session ID 写回 Raw,后续发送的就是修改后包含认证信息的 ClientHello。
服务端处理
接收 ClientHello
源码位置:REALITY/handshake_server.go
服务端在 TLS 握手处理中接收 ClientHello,检查 Session ID 字段:
// 服务端配置中的 PrivateKey 用于解密短 Id
// 服务端通过 ECDH + HKDF 派生 AuthKey
// 然后用 AuthKey 解密 Session ID 中的认证信息
// 验证 ShortId 是否在允许列表中临时证书生成
当认证通过后,服务端生成临时 Ed25519 密钥对并签发证书:
源码位置:REALITY/generate_cert.go
证书的生成涉及:
- 生成随机的 Ed25519 密钥对
- 构造 X.509 证书模板(包含真实的 Organization、Country 等信息以使证书看起来更真实)
- 使用 HMAC-SHA512(AuthKey, Ed25519公钥) 作为证书的签名值(替代正常的 CA 签名)
这保证了证书的"签名"只有持有 AuthKey 的双方能够生成和验证。
版本验证
服务端在验证阶段会检查客户端版本是否在 [MinClientVer, MaxClientVer] 范围内:
// 从 Session ID 的 [0:3] 读取客户端版本
// 与配置中的 MinClientVer / MaxClientVer 比较版本检查允许服务端主动拒绝旧版本客户端,有助于推动客户端升级。
客户端验证
源码位置:xray-core/transport/internet/reality/reality.go 行 76-115
func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte,
verifiedChains [][]*x509.Certificate) error {
// 获取对端证书(使用 unsafe 指针读取内部字段)
p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates")
certs := *(*([]*x509.Certificate))(unsafe.Pointer(
uintptr(unsafe.Pointer(c.Conn)) + p.Offset))
// 第一层:临时证书验证
if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
h := hmac.New(sha512, c.AuthKey)
h.Write(pub)
if bytes.Equal(h.Sum(nil), certs[0].Signature) {
// 第二层:ML-DSA-65 后量子验证(可选)
if len(c.Config.Mldsa65Verify) > 0 {
// ... ML-DSA-65 签名验证 ...
}
c.Verified = true
return nil
}
}
// 第三层:标准 x509 证书链验证
opts := x509.VerifyOptions{
DNSName: c.ServerName,
Intermediates: x509.NewCertPool(),
}
if _, err := certs[0].Verify(opts); err != nil {
return err // 无效证书 → 断开连接
}
return nil // 真证书(非临时证书)→ 进入爬虫模式
}三层验证逻辑总结
| 层次 | 验证内容 | 通过条件 | 失败后果 |
|---|---|---|---|
| 第一层 | HMAC(AuthKey, cert.PublicKey) == cert.Signature | Ed25519 公钥 + HMAC 签名匹配 | 进入下一层 |
| 第二层(可选) | ML-DSA-65 签名验证 | 证书扩展中的后量子签名有效 | 进入下一层 |
| 第三层 | 标准 x509 证书链验证 | 证书链可追溯到一个受信任的根 CA | TLS alert 断开 |
源码结构
REALITY(TLS fork)
REALITY/ 是对 Go 标准库 crypto/tls 的修改版本,核心文件:
| 文件 | 作用 | 行数 |
|---|---|---|
common.go | TLS 常量定义、Config 结构体、证书选择 | ~1809 |
handshake_client.go | TLS 客户端握手(含 REALITY 修改) | ~1200 |
handshake_client_tls13.go | TLS 1.3 客户端握手 | ~900 |
handshake_server.go | TLS 服务端握手(含 REALITY 修改) | ~1000 |
handshake_server_tls13.go | TLS 1.3 服务端握手 | ~1200 |
handshake_messages.go | 握手消息的序列化/反序列化 | ~1700 |
conn.go | TLS 连接实现 | ~1800 |
auth.go | 签名验证 | ~298 |
generate_cert.go | 临时证书生成 | ~150 |
ech.go | Encrypted Client Hello 支持 | ~600 |
quic.go | QUIC 传输参数扩展处理 | ~500 |
key_agreement.go | 密钥交换(ECDHE, ML-KEM 等) | ~350 |
key_schedule.go | TLS 1.3 密钥调度 | ~90 |
cipher_suites.go | 密码套件定义 | ~800 |
ticket.go | 会话票据 | ~400 |
tls.go | TLS 连接包装 API | ~800 |
alert.go | TLS Alert 处理 | ~150 |
cache.go | 会话缓存 | ~50 |
xray-core REALITY 集成
xray-core/transport/internet/reality/:
| 文件 | 作用 | 行数 |
|---|---|---|
reality.go | 客户端 UClient 和服务端 Server 函数 | ~300 |
config.go | 配置解析 | ~200 |
config.proto | Protobuf 配置定义 | ~41 |
config.pb.go | 生成的 Protobuf 代码 | 自动生成 |
配置语义
服务端配置关键字段
| 字段 | 类型 | 默认值 | 含义 | 错误配置后果 |
|---|---|---|---|---|
privateKey | string | 必填 | X25519 私钥(Base64) | 客户端无法完成认证 |
shortIds | []string | 必填 | 允许的 ShortId 列表 | 客户端连接被拒绝后回落 |
serverNames | []string | 必填 | 客户端可用的 SNI 列表 | SNI 不匹配可能导致连接异常 |
target | string | 必填 | 回落目标 host:port | 回落连接失败 |
minClientVer | string | 无限制 | 最低客户端版本 | 旧客户端被拒绝 |
maxClientVer | string | 无限制 | 最高客户端版本 | 新客户端被拒绝 |
maxTimeDiff | uint64 | 0(不检查) | 允许的最大时间差(毫秒) | 时间不同步时连接失败 |
mldsa65Seed | string | 无 | ML-DSA-65 后量子签名种子 | 无后量子保护 |
show | bool | false | 输出调试信息 | 额外日志输出 |
客户端配置关键字段
| 字段 | 类型 | 默认值 | 含义 | 错误配置后果 |
|---|---|---|---|---|
publicKey | string | 必填 | 服务端 X25519 公钥 | 无法完成密钥交换 |
shortId | string | 必填 | 客户端 ShortId | 认证失败 |
serverName | string | dest 地址 | 目标网站 SNI | SNI 不匹配可能导致异常 |
fingerprint | string | chrome | uTLS 浏览器指纹 | 使用默认 TLS 指纹可能被检测 |
spiderX | string | 空 | 爬虫初始路径 | 爬虫模式下使用默认路径 |
spiderY | []int64 | 默认值 | 爬虫参数(并发/延迟等) | 爬虫行为异常 |
mldsa65Verify | string | 无 | ML-DSA-65 公钥 | 无后量子验证 |
运行时行为总结
正常 REALITY 连接
- 客户端发送修改了 Session ID 的 ClientHello
- 服务端验证通过,返回临时 Ed25519 证书
- 客户端验证 HMAC 签名匹配 →
Verified = true - TLS 握手完成后,上层代理协议(VLESS)开始工作
主动探测场景
- GFW 发送标准 TLS ClientHello(无 REALITY Session ID)
- 服务端检测到非 REALITY 连接 → 将连接转发到
target网站 - GFW 收到目标网站的真实 TLS 证书和响应
- GFW 认定这是一个正常的 HTTPS 网站,不会标记为代理
MITM 场景
- 攻击者截获连接,用自己的证书代替服务端证书
- 攻击者的证书不是 Ed25519 类型,或 HMAC 签名不匹配
- 客户端进入 x509 标准验证 → 如果攻击者使用了被信任 CA 签发的证书,验证通过
- 客户端判断:
verified = false→ 进入爬虫模式,不传输代理数据
外部参考资料
资料:REALITY 项目 README 类型:项目文档 链接:https://github.com/XTLS/REALITY 可信度:A
资料:Go crypto/tls 源码 类型:标准库源码 链接:https://github.com/golang/go/tree/master/src/crypto/tls 可信度:A(REALITY 基于此 fork)
资料:uTLS - TLS fingerprinting library 类型:开源项目 链接:https://github.com/refraction-networking/utls 可信度:A
资料:RFC 8446 - TLS 1.3 类型:IETF 标准 链接:https://www.rfc-editor.org/rfc/rfc8446 可信度:A
验证方式
- 客户端和服务端设置
"show": true查看 REALITY 握手调试信息 - 使用
openssl s_client -connect <server>:443 -servername <target>验证回落到目标网站 - 使用 Wireshark 抓包对比 REALITY 连接和普通 HTTPS 连接的 ClientHello
读者自检
读完本文后应能回答:
- REALITY 为什么使用 Session ID 嵌入认证信息
- AuthKey 是如何通过 ECDH + HKDF 派生的
- 临时证书的签名为什么是 HMAC 而不是 CA 签名
- 客户端三层证书验证的逻辑和各自的触发条件
- 爬虫模式在 MITM 场景下的作用
下一步阅读
- VLESS 协议:理解上层代理协议
- XTLS Vision 流控:理解流控机制
- 源码分析:REALITY 客户端
- 源码分析:REALITY 服务端(待编写)