REALITY 服务端:握手与记录检测
位置:源码分析层 | 难度:较难 | 前置:REALITY 协议详解、REALITY 客户端走读源码证据:
REALITY/handshake_server.go、REALITY/handshake_server_tls13.go、REALITY/record_detect.go
1. 服务端与客户端的非对称性
REALITY 服务端代码量远大于客户端。客户端的 UClient 是一个约 160 行的集成函数,而服务端需要:
- 完整改造 Go 标准库的 TLS 1.2/1.3 服务端握手(两个文件,合计约 1,800 行)
- 在 ClientHello 处理阶段判断连接是 REALITY 客户端还是普通 HTTPS 用户
- 生成临时 Ed25519 证书(
generate_cert.go) - 管理回落目标连接(将非 REALITY 流量转发到
target网站) - 主动采集目标网站的握手后记录长度特征(
record_detect.go)
2. 服务端握手入口
源码位置:REALITY/handshake_server.go 行 44-66
func (c *Conn) serverHandshake(ctx context.Context) error {
clientHello, ech, err := c.readClientHello(ctx)
if err != nil {
return err
}
if c.vers == VersionTLS13 {
hs := serverHandshakeStateTLS13{
c: c,
ctx: ctx,
clientHello: clientHello,
echContext: ech,
}
return hs.handshake() // → 进入 TLS 1.3 握手处理
}
hs := serverHandshakeState{
c: c,
ctx: ctx,
clientHello: clientHello,
}
return hs.handshake() // → 进入 TLS 1.2 握手处理
}REALITY 同时支持 TLS 1.2 和 1.3,但 TLS 1.3 是推荐模式(0-RTT 支持、更少的明文握手信息)。本文聚焦 TLS 1.3 路径。
3. TLS 1.3 服务端握手核心
源码位置:REALITY/handshake_server_tls13.go
3.1 ClientHello 处理
服务端收到 ClientHello 后的处理流程:
3.2 临时证书签发
认证通过后,服务端生成临时 Ed25519 证书:
源码位置:REALITY/generate_cert.go
// 生成 Ed25519 密钥对
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
// 构造 X.509 证书模板(使用真实的目标网站信息)
template := &x509.Certificate{
SerialNumber: randomSerial(),
Subject: pkix.Name{Organization: []string{"Internet Widgits Pty Ltd"}},
DNSNames: []string{serverName}, // 目标网站 SNI
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
// 关键:替换证书签名为 HMAC(AuthKey, Ed25519公钥)
// 而非正常的 Ed25519.Sign(privateKey, certDER)
h := hmac.New(sha512.New, authKey)
h.Write(publicKey)
cert.Signature = h.Sum(nil)为什么证书的签名字段是 HMAC 而不是正常签名:
正常的 x509 证书由 CA 的私钥签名。REALITY 没有 CA——证书签名字段(原本应是 Ed25519.Sign(privateKey, tbsCertificate) 的 64 字节签名值)被替换为 HMAC-SHA512(AuthKey, 公钥)。这个替换是安全的,因为:
- 客户端持有相同的
AuthKey,可以独立计算 HMAC 并与收到的cert.Signature比对 - 任何不持有
AuthKey的第三方无法伪造这个 HMAC - 证书的签名字段在网络上是加密传输的(ServerHello 之后全部加密),审查方看不到
3.3 回落流程
当服务端判断连接不是来自 REALITY 客户端时:
- 服务端向
config.Dest(配置的目标网站)发起 TLS 连接 - 将客户端原始 ClientHello 转发到目标
- 接收目标网站的 ServerHello + Certificate(真实证书)
- 将目标的响应转发回客户端
- 客户端收到的是目标网站的真实证书(非 Ed25519 非 HMAC 签名)
- 客户端的
VerifyPeerCertificate进入第三层(x509 标准验证)→Verified = false
4. 记录检测(Record Detection)
源码位置:REALITY/record_detect.go
这是 REALITY 的一个重要但被忽视的功能:主动采集目标网站的 TLS 握手后记录长度特征,用于服务端在主动探测场景中更精确地模仿目标网站的行为。
4.1 为什么需要记录检测
REALITY 服务端在回落模式下需要将客户端连接转发到目标网站。简单的转发是透明的——服务端不修改流量。但是:
- 审查方的主动探测可能发送异常短的 ClientHello 或特殊的 TLS 扩展组合
- 目标网站对异常 ClientHello 的响应可能与对正常 ClientHello 的响应不同
- 如果服务端盲目转发,审查方可以通过对比服务端的转发行为与直接访问目标网站的行为来识别代理
记录检测通过预先采集目标网站的 TLS 行为特征,使服务端的回落行为与目标网站完全一致。
4.2 检测触发
源码位置:record_detect.go 行 21-102
DetectPostHandshakeRecordsLens(config) 在服务端启动时被调用。对每个配置的 ServerName 和三种 ALPN 组合:
| alpn 值 | NextProtos | uTLS 指纹 | 检测目的 |
|---|---|---|---|
| 0 | nil(无 ALPN) | Chrome | 检测不使用 ALPN 时的记录长度 |
| 1 | ["http/1.1"] | Chrome | 检测 HTTP/1.1 使用的记录长度 |
| 2 | ["h2", "http/1.1"] | Go 指纹 | 检测 Go 客户端使用 H2 时的记录长度 |
alpn=2 使用 Go 指纹而非 Chrome 指纹——这是因为审查方如果使用 Go 标准库进行主动探测,其 TLS 指纹也会是 Go 的。REALITY 需要知道目标网站在收到 Go 指纹 ClientHello 时的行为。
4.3 PostHandshakeRecordDetectConn
源码位置:record_detect.go 行 104-135
type PostHandshakeRecordDetectConn struct {
net.Conn
Key string
CcsSent bool
}
func (c *PostHandshakeRecordDetectConn) Write(b []byte) (n int, err error) {
if len(b) >= 3 && bytes.Equal(b[:3], []byte{20, 3, 3}) {
c.CcsSent = true // 检测到 CCS 消息(TLS 1.2 ChangeCipherSpec)
}
return c.Conn.Write(b)
}
func (c *PostHandshakeRecordDetectConn) Read(b []byte) (n int, err error) {
if !c.CcsSent {
return c.Conn.Read(b)
}
// CCS 之后:设置 5 秒超时,读取所有数据
c.Conn.SetReadDeadline(time.Now().Add(5 * time.Second))
data, _ := io.ReadAll(c.Conn)
// 解析 TLS 记录:
// [0x17 0x03 0x03] = Application Data 记录类型
var postHandshakeRecordsLens []int
for {
if len(data) >= 5 && bytes.Equal(data[:3], []byte{23, 3, 3}) {
length := int(binary.BigEndian.Uint16(data[3:5])) + 5
postHandshakeRecordsLens = append(postHandshakeRecordsLens, length)
data = data[length:]
} else {
break
}
}
// 存入全局 Map: key = "dest sni alpn"
GlobalPostHandshakeRecordsLens.Store(c.Key, postHandshakeRecordsLens)
return 0, io.EOF
}采集的数据:目标网站在 TLS 握手完成后、关闭连接前发送的所有 Application Data 记录的长度序列。
例如 www.microsoft.com:443 可能返回:
[1400, 1400, 1400, 856, 511, 1400, 321]这些长度对应目标网站的 HTTP 响应分块策略(HTTP/2 frame 大小、TCP 拥塞窗口等),是目标网站的行为特征。
4.4 CCSDetectConn:ChangeCipherSpec 消息计数
源码位置:record_detect.go 行 139-185
type CCSDetectConn struct {
net.Conn
Key string
}
func (c *CCSDetectConn) Write(b []byte) (n int, err error) {
if len(b) >= 3 && bytes.Equal(b[:3], []byte{20, 3, 3}) {
// 已发送 CCS → 开始探测
go func() {
for {
_, err = c.Conn.Read(buf)
if buf[0] == 0x15 { // Alert → 服务器已关闭
return
}
}
}()
// 发送 2 个 CCS 消息 → 等 1 秒 → 检查是否 Alert
// 发送 15 个 CCS 消息 → 等 1 秒 → 检查是否 Alert
// 发送 16 个 CCS 消息 → 等 1 秒 → 检查是否 Alert
// 根据服务器容忍的 CCS 数量存储结果
}
}检测目的:测试目标网站对异常 TLS 行为的容忍度。某些 TLS 实现在收到过多 CCS 消息后会发送 Alert 并断开连接。这个信息用于服务端在面对审查方发送异常 CCS 消息时应该采用的行为。
5. 检测结果的使用
采集到的特征存储在两个全局 sync.Map 中:
GlobalPostHandshakeRecordsLens:key ="dest sni alpn",value =[]int(握手后记录长度序列)GlobalMaxCSSMsgCount:key ="dest sni alpn",value =int(最大容忍 CCS 消息数)
服务端在回落模式下使用这些数据来模拟目标网站在特定场景下的行为。例如:
- 如果审查方使用 Go 指纹的 ClientHello 进行主动探测,服务端以目标网站在收到 Go 指纹时的 TLS 记录长度序列进行响应
- 如果审查方发送多个 CCS 消息,服务端在目标网站容忍范围内不发送 Alert,超出后发送(匹配目标行为)
6. 与客户端文档的对应
客户端 (08-reality-client.md) | 服务端 (本文) |
|---|---|
| UClient 函数:构造认证 ClientHello | serverHandshake:解析并验证 ClientHello |
| ECDH + HKDF → AuthKey | 同样的 ECDH + HKDF → AuthKey |
| AES-GCM 加密 Session ID | AES-GCM 解密 Session ID |
| VerifyPeerCertificate:验证 HMAC 签名 | generate_cert:生成 HMAC 签名的证书 |
| Verified = true / false / 爬虫 | 认证成功 → 签发临时证书 / 失败 → 回落 |
7. 外部参考资料
资料:REALITY handshake_server.go 源码 类型:源码 链接:
REALITY/handshake_server.go+handshake_server_tls13.go可信度:A
资料:REALITY record_detect.go 源码 类型:源码 链接:
REALITY/record_detect.go可信度:A
资料:Go crypto/tls 标准库源码 类型:标准库源码 链接:https://github.com/golang/go/tree/master/src/crypto/tls 可信度:A(REALITY 基于此 fork)
下一步阅读
- REALITY 客户端走读:对称的客户端实现
- REALITY 协议详解:整体协议设计