REALITY 安全层
简介
REALITY 是一种自定义安全协议,扩展了 TLS 1.3,使服务端能够伪装成任何真实的 TLS 服务器而无需其证书或私钥。REALITY 服务端不呈现自己的证书,而是将 TLS 握手代理到合法的"dest"服务器。授权客户端(知道服务端公钥和 Short ID)可以通过嵌入在 TLS 1.3 ClientHello Session ID 中的自定义方案验证服务端。未授权客户端或主动探测者收到来自 dest 服务器的真实证书,使代理与真实服务无法区分。无需证书颁发机构。
关键文件
transport/internet/reality/reality.go-- 客户端(UClient)和服务端(Server)连接逻辑transport/internet/reality/config.go--GetREALITYConfig()、配置提取
架构
sequenceDiagram
participant C as REALITY 客户端
participant S as REALITY 服务端
participant D as Dest 服务器(如 google.com)
Note over C: 已知:PublicKey、ShortId、ServerName
Note over S: 已知:PrivateKey、ShortIds[]、ServerNames[]
C->>S: TLS ClientHello (SessionId = 加密认证)
S->>S: 解密 SessionId,验证 ShortId + 时间戳
alt 授权客户端
S->>C: TLS ServerHello (包含用 AuthKey 签名的 ed25519 证书)
C->>C: 用 AuthKey 验证证书签名
Note over C,S: 已认证的 REALITY 连接
else 未授权 / 探测
S->>D: 转发 ClientHello
D->>S: 真实 ServerHello + 证书
S->>C: 转发真实响应
Note over C,S: 客户端看到真实的 google.com 证书
Note over S: 连接变为到 Dest 的透明代理
end客户端实现
UClient
reality.UClient(reality/reality.go:117-277)执行 REALITY 握手:
步骤 1:构建 uTLS 连接
func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) {
uConn := &UConn{Config: config}
utlsConfig := &utls.Config{
VerifyPeerCertificate: uConn.VerifyPeerCertificate,
ServerName: config.ServerName,
InsecureSkipVerify: true,
SessionTicketsDisabled: true,
}
fingerprint := tls.GetFingerprint(config.Fingerprint)
uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint)InsecureSkipVerify 为 true,因为验证由 REALITY 自定义的 VerifyPeerCertificate 处理,而非 Go 内置的证书链验证。
步骤 2:构建认证 Session ID
Session ID(32 字节)被构建并加密(reality.go:138-176):
hello := uConn.HandshakeState.Hello
hello.SessionId = make([]byte, 32)
// 字节 0-2:Xray 版本
hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0 // 保留
// 字节 4-7:当前 Unix 时间戳(大端序)
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
// 字节 8-15:Short ID(8 字节)
copy(hello.SessionId[8:], config.ShortId)步骤 3:派生 Auth Key
Auth Key 从 ECDH 共享密钥派生(reality.go:152-169):
publicKey, _ := ecdh.X25519().NewPublicKey(config.PublicKey)
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
// 对于后量子密钥交换回退到 MlkemEcdhe
uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
// HKDF 派生
hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey)HKDF 使用 ClientHello.Random 的前 20 字节作为盐值,字面量字符串 "REALITY" 作为信息。
步骤 4:加密 Session ID
aead := crypto.NewAesGcm(uConn.AuthKey)
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)- Nonce:
ClientHello.Random的后 12 字节(字节 20-31) - 明文:Session ID 的前 16 字节
- 附加数据:完整的原始 ClientHello
- 输出:16 字节密文 + 16 字节 GCM 标签 = 覆写完整的 32 字节 Session ID
加密后的 Session ID 随后被复制回原始 ClientHello 字节中(reality.go:175)。
步骤 5:验证服务端证书
VerifyPeerCertificate(reality/reality.go:76-115)检查服务端是否为真实的 REALITY 服务端:
func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
certs := // 解析 rawCerts
if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
h := hmac.New(sha512.New, c.AuthKey)
h.Write(pub)
if bytes.Equal(h.Sum(nil), certs[0].Signature) {
// 可选的 ML-DSA-65 后量子验证
if len(c.Config.Mldsa65Verify) > 0 {
// 验证 ClientHello + ServerHello 上的 ML-DSA-65 签名
}
c.Verified = true
return nil
}
}
// 回退到标准 x509 验证(来自 dest 服务器的真实证书)
opts := x509.VerifyOptions{DNSName: c.ServerName, ...}
certs[0].Verify(opts)
return nil // 验证通过(是真实证书),但 Verified 保持 false
}REALITY 服务端生成的 ed25519 证书具有以下特征:
- 公钥:ed25519 公钥
- 签名:
HMAC-SHA512(AuthKey, PublicKey)
如果此 HMAC 匹配,客户端确认正在与真实的 REALITY 服务端通信。如果不匹配,证书按正常方式验证(来自 dest 服务器),但 c.Verified 保持 false。
步骤 6:处理验证失败
如果握手后 uConn.Verified 为 false(reality.go:183-274),客户端模拟真实浏览器行为:
if !uConn.Verified {
// "蜘蛛"模式:爬取 dest 服务器以生成逼真流量
client := &http.Client{Transport: &http2.Transport{...}}
// GET 页面,跟随链接,添加 cookie
// 使连接看起来像真正的浏览器在访问网站
time.Sleep(randomDuration)
return nil, errors.New("REALITY: processed invalid connection")
}蜘蛛以逼真的时序爬取 dest 服务器的页面,然后关闭连接。这使得主动探测无法通过时序分析与真实浏览器访问区分。
服务端连接
reality.Server(reality/reality.go:52-55)使用 reality 库包装:
func Server(c net.Conn, config *reality.Config) (net.Conn, error) {
realityConn, err := reality.Server(context.Background(), c, config)
return &Conn{Conn: realityConn}, err
}实际的服务端逻辑在 github.com/xtls/reality 库中,它:
- 读取 ClientHello
- 使用私钥解密 Session ID
- 验证 Short ID、时间戳和客户端版本
- 如果授权:使用共享 AuthKey 生成 ed25519 证书签名
- 如果未授权:透明代理到 dest 服务器
配置
客户端配置
来自 reality/config.go:74-83:
func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config {
config, ok := settings.SecuritySettings.(*Config)
if !ok { return nil }
return config
}客户端需要:
- ServerName:TLS 握手的 SNI(被伪装的服务器)
- Fingerprint:uTLS 指纹(必填,不能为空)
- PublicKey:服务端的 X25519 公钥(32 字节)
- ShortId:客户端的 Short ID(最多 8 字节)
- SpiderX:蜘蛛爬取的起始路径
- SpiderY:包含 10 个 int64 值的数组,控制蜘蛛行为(cookie 填充、并发数、间隔等)
- Show:调试模式,将 REALITY 状态输出到标准输出
- Mldsa65Verify:用于后量子验证的 ML-DSA-65 公钥
服务端配置
GetREALITYConfig(reality/config.go:16-58)构建 reality.Config:
func (c *Config) GetREALITYConfig() *reality.Config {
config := &reality.Config{
Show: c.Show,
Type: c.Type,
Dest: c.Dest, // dest 服务器地址
Xver: byte(c.Xver), // PROXY 协议版本
PrivateKey: c.PrivateKey, // X25519 私钥
MinClientVer: c.MinClientVer, // 最低客户端版本
MaxClientVer: c.MaxClientVer, // 最高客户端版本
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
SessionTicketsDisabled: true,
}
// 填充 ServerNames 映射和 ShortIds 映射
config.ServerNames[serverName] = true
config.ShortIds[shortId] = true
// 可选的 ML-DSA-65 签名密钥
// 可选的回退连接速率限制
return config
}服务端需要:
- PrivateKey:X25519 私钥(32 字节)
- ServerNames:允许的 SNI 值
- ShortIds:允许的 Short ID([8]byte 映射)
- Dest:未授权客户端代理到的真实服务器地址
- MaxTimeDiff:允许的最大时钟偏差(毫秒)
- MinClientVer / MaxClientVer:允许的 Xray 客户端版本范围
Session ID 线格式
字节 0-2: Xray 版本 (x.y.z)
字节 3: 保留 (0)
字节 4-7: Unix 时间戳(大端序 uint32)
字节 8-15: Short ID(8 字节,可能部分为零)
[使用 AES-GCM 和 AuthKey 加密]
[Nonce = ClientHello.Random[20:32]]
[AAD = 原始 ClientHello 字节]服务端解密后:
- 检查 Short ID 是否在允许列表中
- 验证时间戳在
MaxTimeDiff范围内 - 检查客户端版本在允许范围内
后量子支持
密钥交换
REALITY 在 TLS 指纹可用时支持 X25519-MLKEM768 密钥交换(reality.go:156-161):
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe
}证书验证
可选的 ML-DSA-65(Dilithium)验证(reality.go:88-95):
if len(c.Config.Mldsa65Verify) > 0 {
h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify)
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
c.Verified = true
}
}这提供了后量子认证:服务端用其 ML-DSA-65 私钥签名 HMAC(AuthKey, PublicKey || ClientHello.Raw || ServerHello.Raw),客户端用预共享的公钥进行验证。
密钥日志支持
客户端和服务端均支持 TLS 密钥日志以供调试(reality/config.go:61-72):
func KeyLogWriterFromConfig(c *Config) io.Writer {
if len(c.MasterKeyLog) <= 0 || c.MasterKeyLog == "none" { return nil }
writer, _ := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
return writer
}传输协议集成
REALITY 在两个点与传输协议集成:
客户端
每个传输协议检查 REALITY 配置并应用:
// tcp/dialer.go:89-93
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
conn, err = reality.UClient(conn, config, ctx, dest)
}服务端
传输协议包装其监听器:
// tcp/hub.go:76-79
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
l.realityConfig = config.GetREALITYConfig()
go goreality.DetectPostHandshakeRecordsLens(l.realityConfig)
}DetectPostHandshakeRecordsLens 在启动时调用,用于对 dest 服务器的握手后行为进行指纹识别,以实现更准确的伪装。
实现说明
- 无需 CA:REALITY 不需要任何证书。服务端要么呈现 dest 服务器的真实证书(给未授权客户端),要么呈现动态生成的 ed25519 证书(给授权客户端)。
- 主动探测抵抗:未授权连接被透明代理到 dest 服务器。提供 dest 服务器的真实证书,使探测无法与直接连接区分。
- 蜘蛛反指纹:当 REALITY 客户端收到真实证书(表示探测或配置错误)时,它像真正的浏览器一样爬取 dest 服务器,然后断开连接,防止基于时序的检测。
- 指纹必填:与 TLS 不同,REALITY 要求 uTLS 指纹(
reality.go:133-136)。不能使用标准 Go TLS,因为 REALITY 需要访问 ClientHello 内部结构。 - Session ID 固定位置:Session ID 占据原始 ClientHello 第 39 字节之后的位置(
reality.go:142),这是 TLS 1.3 ClientHello 结构中的固定位置。 - 时钟同步:客户端和服务端时间戳必须在
MaxTimeDiff范围内(默认值因配置而异)。较大的时钟偏差将导致认证失败。 - Short ID 作为访问控制:不同的 Short ID 可以分配给不同的用户/客户端,实现无需更换密钥的逐用户访问控制。
- 速率限制:
LimitFallbackUpload/LimitFallbackDownload可以限制未授权(回退)连接的带宽(config.go:41-49)。