Skip to content

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 证书结构

上级文档和相关文档

主要证据

  • 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 握手的关键步骤:

  1. ClientHello:客户端发送支持的密码套件、密钥共享(Key Share,包含客户端的 ECDH 公钥)、SNI 等信息
  2. ServerHello:服务端选择密码套件、回复密钥共享(包含服务端的 ECDH 公钥)
  3. EncryptedExtensions:服务端发送加密的扩展信息
  4. Certificate:服务端发送证书链
  5. CertificateVerify:服务端用证书私钥签名握手记录,证明拥有证书
  6. Finished:握手完成,开始应用数据

Session ID 字段

Session ID 是 ClientHello 中的一个字段,用于 TLS 会话恢复。在 TLS 1.3 中,这个字段实际上不再用于会话恢复(PSK 机制替代了它),但仍保留在协议中用于兼容性。

REALITY 利用了这个冗余字段——将认证信息嵌入 Session ID,既不影响 TLS 握手,也让外部观察者看到的 ClientHello 看起来正常。


REALITY 的设计目标

REALITY 需要达成以下三个看似互相矛盾的目标:

  1. 对审查方透明:TLS 握手在外部看起来必须是完全正常的 HTTPS 访问某个知名网站
  2. 对客户端可认证:REALITY 客户端必须能够确认"正在与 REALITY 服务端通信",而非被 MITM 攻击
  3. 对非客户端安全回落:审查方的主动探测或普通 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

go
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

go
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 加密:

go
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 字段:

go
// 服务端配置中的 PrivateKey 用于解密短 Id
// 服务端通过 ECDH + HKDF 派生 AuthKey
// 然后用 AuthKey 解密 Session ID 中的认证信息
// 验证 ShortId 是否在允许列表中

临时证书生成

当认证通过后,服务端生成临时 Ed25519 密钥对并签发证书:

源码位置:REALITY/generate_cert.go

证书的生成涉及:

  1. 生成随机的 Ed25519 密钥对
  2. 构造 X.509 证书模板(包含真实的 Organization、Country 等信息以使证书看起来更真实)
  3. 使用 HMAC-SHA512(AuthKey, Ed25519公钥) 作为证书的签名值(替代正常的 CA 签名)

这保证了证书的"签名"只有持有 AuthKey 的双方能够生成和验证。

版本验证

服务端在验证阶段会检查客户端版本是否在 [MinClientVer, MaxClientVer] 范围内:

go
// 从 Session ID 的 [0:3] 读取客户端版本
// 与配置中的 MinClientVer / MaxClientVer 比较

版本检查允许服务端主动拒绝旧版本客户端,有助于推动客户端升级。


客户端验证

源码位置:xray-core/transport/internet/reality/reality.go 行 76-115

go
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.SignatureEd25519 公钥 + HMAC 签名匹配进入下一层
第二层(可选)ML-DSA-65 签名验证证书扩展中的后量子签名有效进入下一层
第三层标准 x509 证书链验证证书链可追溯到一个受信任的根 CATLS alert 断开

源码结构

REALITY(TLS fork)

REALITY/ 是对 Go 标准库 crypto/tls 的修改版本,核心文件:

文件作用行数
common.goTLS 常量定义、Config 结构体、证书选择~1809
handshake_client.goTLS 客户端握手(含 REALITY 修改)~1200
handshake_client_tls13.goTLS 1.3 客户端握手~900
handshake_server.goTLS 服务端握手(含 REALITY 修改)~1000
handshake_server_tls13.goTLS 1.3 服务端握手~1200
handshake_messages.go握手消息的序列化/反序列化~1700
conn.goTLS 连接实现~1800
auth.go签名验证~298
generate_cert.go临时证书生成~150
ech.goEncrypted Client Hello 支持~600
quic.goQUIC 传输参数扩展处理~500
key_agreement.go密钥交换(ECDHE, ML-KEM 等)~350
key_schedule.goTLS 1.3 密钥调度~90
cipher_suites.go密码套件定义~800
ticket.go会话票据~400
tls.goTLS 连接包装 API~800
alert.goTLS Alert 处理~150
cache.go会话缓存~50

xray-core REALITY 集成

xray-core/transport/internet/reality/

文件作用行数
reality.go客户端 UClient 和服务端 Server 函数~300
config.go配置解析~200
config.protoProtobuf 配置定义~41
config.pb.go生成的 Protobuf 代码自动生成

配置语义

服务端配置关键字段

字段类型默认值含义错误配置后果
privateKeystring必填X25519 私钥(Base64)客户端无法完成认证
shortIds[]string必填允许的 ShortId 列表客户端连接被拒绝后回落
serverNames[]string必填客户端可用的 SNI 列表SNI 不匹配可能导致连接异常
targetstring必填回落目标 host:port回落连接失败
minClientVerstring无限制最低客户端版本旧客户端被拒绝
maxClientVerstring无限制最高客户端版本新客户端被拒绝
maxTimeDiffuint640(不检查)允许的最大时间差(毫秒)时间不同步时连接失败
mldsa65SeedstringML-DSA-65 后量子签名种子无后量子保护
showboolfalse输出调试信息额外日志输出

客户端配置关键字段

字段类型默认值含义错误配置后果
publicKeystring必填服务端 X25519 公钥无法完成密钥交换
shortIdstring必填客户端 ShortId认证失败
serverNamestringdest 地址目标网站 SNISNI 不匹配可能导致异常
fingerprintstringchromeuTLS 浏览器指纹使用默认 TLS 指纹可能被检测
spiderXstring爬虫初始路径爬虫模式下使用默认路径
spiderY[]int64默认值爬虫参数(并发/延迟等)爬虫行为异常
mldsa65VerifystringML-DSA-65 公钥无后量子验证

运行时行为总结

正常 REALITY 连接

  1. 客户端发送修改了 Session ID 的 ClientHello
  2. 服务端验证通过,返回临时 Ed25519 证书
  3. 客户端验证 HMAC 签名匹配 → Verified = true
  4. TLS 握手完成后,上层代理协议(VLESS)开始工作

主动探测场景

  1. GFW 发送标准 TLS ClientHello(无 REALITY Session ID)
  2. 服务端检测到非 REALITY 连接 → 将连接转发到 target 网站
  3. GFW 收到目标网站的真实 TLS 证书和响应
  4. GFW 认定这是一个正常的 HTTPS 网站,不会标记为代理

MITM 场景

  1. 攻击者截获连接,用自己的证书代替服务端证书
  2. 攻击者的证书不是 Ed25519 类型,或 HMAC 签名不匹配
  3. 客户端进入 x509 标准验证 → 如果攻击者使用了被信任 CA 签发的证书,验证通过
  4. 客户端判断: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 场景下的作用

下一步阅读