04 百科
Reality 协议原理深度拆解:VLESS 偷取 SNI 与 TLS 1.3 握手伪装机制
深入剖析 VLESS-Reality 协议的底层密码学握手与 SNI 伪装机制,对比传统 TLS 伪装(Trojan/WebSockets)证书暴露风险,详解 Reality 如何借助 TLS 1.3 ClientHello 扩展与目标真实站点进行临时公钥协商,分析服务端回落与重放攻击防护,并提供伪
海外Wiki 编辑部 发布 2026-10-02 约 58 分钟
快答
Reality 是 VLESS 协议的一种革命性 TLS 伪装方案,它通过“偷取”真实目标网站(如 microsoft.com)的 SNI 和证书链,在 TLS 1.3 握手过程中动态生成临时密钥对进行身份验证,从而无需自备域名和证书即可实现与真实网站完全一致的握手特征,有效规避基于证书指纹和主动探测的封锁。
本条目导读
- Reality 协议诞生的背景与传统 TLS 伪装的致命缺陷
- 传统 TLS 伪装方案的技术路线
- 证书申请暴露链
- 主动探测识别原理
- Reality 的设计目标
- 关键差异对比
- RFC 8446 中的握手关键字段
- VLESS 协议栈与 Reality 的集成架构
- VLESS 协议头结构
- Reality 在 VLESS 中的扩展字段定义
- 客户端与服务端交互序列
- Reality 如何复用 VLESS 的轻量级传输
- TLS 1.3 ClientHello 扩展与 Reality 的临时公钥协商
- 标准 TLS 1.3 ClientHello 结构回顾
- Reality 对 key_share 扩展的改造
- Wireshark 抓包字段拆解
- 服务端验证与 ECDH 协商流程
- ECDH 数学过程简述
- signature_algorithms 扩展的伪装意义
- 临时公钥协商的安全边界
- SNI 偷取与伪装域名选择机制
- SNI 偷取流程
- 证书链动态获取
- 伪装域名筛选决策矩阵
- 避免 SNI 泄露
- 域名封锁的规避策略
- 服务端回落与重放攻击防护
- 回落触发条件与判定逻辑
- 重放攻击防护机制
- 回落目标的选择与行为一致性
- 状态同步与性能开销
- Reality 抗封锁原理与生产环境部署建议
- 抗封锁能力对比
- 生产环境配置模板
- 性能调优
- 合规与法律风险提示
Reality 协议诞生的背景与传统 TLS 伪装的致命缺陷
传统 TLS 伪装方案的技术路线
Trojan 与 WebSocket+TLS 的方案核心相同:在真实 TLS 之上承载代理流量,服务端持有一张合法证书,客户端通过标准 TLS 握手建立加密通道,代理协议数据作为 TLS Application Data 传输。外界被动观察者看到的是一个正常的 HTTPS 连接。
部署形态通常如下:
# Trojan-Go 服务端配置片段
"ssl": {
"cert": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key": "/etc/letsencrypt/live/example.com/privkey.pem",
"fallback_addr": "127.0.0.1:8080",
"fallback_port": 8080
}
证书通过 ACME 协议(Let’s Encrypt 等 CA)签发,服务端必须持有与伪装域名匹配的私钥。这一事实构成了整个方案的结构性弱点。
证书申请暴露链
ACME 签发流程中,CA 会执行以下验证步骤之一:
- HTTP-01:CA 向
http://<domain>/.well-known/acme-challenge/<token>发起请求,验证 token 内容。 - TLS-ALPN-01:CA 在 443 端口发起 TLS 握手,ALPN 扩展携带
acme-tls/1,验证临时证书中的id-pe-acmeIdentifier扩展。 - DNS-01:CA 查询
_acme-challenge.<domain>TXT 记录。
无论哪种方式,CA 的验证请求源 IP 段是公开可查的(如 Let’s Encrypt 公布其验证 IP 范围)。被动监听者只需在服务端出口方向捕获到与 ACME 验证服务器的通信,即可标记该 IP 为”代理服务端候选”。更致命的是 Certificate Transparency(CT)日志:所有公开 CA 签发的证书都会写入 CT Log,任何人可查询 crt.sh 等聚合服务,按域名、按 IP 关联出证书签发记录。
# 通过 crt.sh 查询某 IP 关联的证书
curl -s "https://crt.sh/?q=<target-ip>&output=json" | jq '.[].name_value'
攻击者批量拉取 CT 日志,将证书中的 SAN 与 IP 解析记录关联,就能生成一份”伪装域名 → 真实 IP”的映射表。Trojan 方案中服务端 IP 与伪装域名强绑定,一旦域名出现在 CT 日志且解析到该 IP,被动识别即告完成。
主动探测识别原理
被动识别之外,主动探测是更致命的打击手段。GFW 的主动探测流程可文字化描述如下:
主动探测攻击流程
──────────────────────────────────────────────────
1. 探测方从受控 IP 向目标 IP:443 发起 TLS 握手
2. 完成握手后,发送构造的 HTTP 请求:
- 正确 SNI + 正确 Host + 合法路径 → 期望正常响应
- 正确 SNI + 错误 Host → 观察响应差异
- 错误 SNI → 观察证书/行为
- 无 SNI → 观察默认证书
3. 对比不同请求下的:
a) TLS 握手参数(cipher suite 顺序、扩展列表、ALPN)
b) 证书链内容(是否与 SNI 匹配、是否自签)
c) HTTP 响应头(Server、Date、Content-Type)
d) 响应时延与数据包大小分布
4. 若行为偏离标准 HTTPS 服务器基线 → 标记为代理
──────────────────────────────────────────────────
Trojan 方案在此流程下的暴露点:
- 证书与 SNI 不匹配:若服务端只配置了一张证书,收到不匹配 SNI 时要么返回默认证书(暴露),要么握手失败(暴露)。
- fallback 行为异常:Trojan 将非代理请求 fallback 到本地 Web 服务,但 fallback 的响应头、TLS 会话票据行为与真实 Web 服务器存在统计差异。
- 无 SNI 处理:标准 HTTPS 服务器对无 SNI 请求通常返回默认虚拟主机内容或直接拒绝,Trojan 的处理逻辑容易被指纹化。
Reality 的设计目标
Reality 协议针对上述缺陷提出三个核心设计目标:
- 零证书持有:服务端不申请、不持有任何证书私钥,消除 ACME 暴露链与 CT 日志关联。
- 偷取真实 SNI:客户端在 ClientHello 中使用目标伪装域名的 SNI,服务端将该 ClientHello 转发至真实目标服务器完成握手,自身不参与证书签发。
- 握手特征不可区分:Reality 服务端的 TLS 行为与真实目标服务器在被动观察下无法区分,主动探测时直接代理到真实目标,返回真实证书与响应。
关键差异对比
| 维度 | 传统 TLS 伪装(Trojan/WS+TLS) | Reality |
|---|---|---|
| 证书管理 | 服务端持有私钥,需 ACME 签发与续期 | 服务端无私钥,偷取目标站点证书 |
| CT 日志暴露 | 域名与 IP 出现在 CT Log,可关联 | 无自有证书,CT Log 无记录 |
| SNI 处理 | 固定 SNI,不匹配时行为异常 | 客户端指定 SNI,服务端透传至真实目标 |
| 主动探测响应 | fallback 逻辑可被指纹化 | 直接代理至真实目标,响应与目标一致 |
| TLS 握手特征 | 服务端 TLS 栈指纹(cipher 顺序、扩展) | 复用真实目标握手,特征与目标相同 |
| 抗探测能力 | 依赖 fallback 质量,存在统计差异 | 探测方无法区分代理与真实站点 |
| 证书链验证 | 客户端验证服务端证书 | 客户端通过临时密钥验证服务端身份 |
RFC 8446 中的握手关键字段
Reality 的偷取 SNI 机制依赖 TLS 1.3(RFC 8446)的以下字段与流程:
ClientHello 中的关键扩展:
Extension: server_name (type=0x0000)
ServerNameList:
NameType: host_name (0)
HostName: <伪装域名>
Extension: key_share (type=0x0033)
ClientKeyExchange:
KeyShareEntry:
Group: x25519 (0x001d)
key_exchange: <客户端临时公钥>
Extension: supported_versions (type=0x002b)
SelectedVersion: TLS 1.3 (0x0304)
Extension: signature_algorithms (type=0x000d)
SignatureSchemeList:
rsa_pss_rsae_sha256 (0x0804)
ecdsa_secp256r1_sha256 (0x0403)
...
ServerHello 中的对应字段:
Extension: key_share (type=0x0033)
KeyShareEntry:
Group: x25519 (0x001d)
key_exchange: <服务端临时公钥>
Extension: supported_versions (type=0x002b)
SelectedVersion: TLS 1.3 (0x0304)
Reality 的核心在于:服务端在收到 ClientHello 后,不直接用自己的私钥完成握手,而是将 ClientHello 原样转发至 ClientHello 中 SNI 指定的真实目标服务器(如 www.microsoft.com:443),由真实服务器返回 ServerHello 与证书。Reality 服务端在此过程中扮演中间人角色,但通过 X25519 临时密钥对与客户端预先共享的认证密钥,实现服务端身份验证,同时保持与真实目标服务器的握手特征完全一致。
这一机制使得:
- 被动观察者看到的是客户端与真实目标服务器之间的标准 TLS 1.3 握手。
- 主动探测者若使用错误 SNI,Reality 服务端将其代理至错误 SNI 对应的真实站点,返回该站点的真实证书与响应。
- 只有持有正确认证密钥的客户端才能完成 Reality 握手并进入代理模式。
传统方案中证书申请暴露与 fallback 行为指纹化的问题,在 Reality 架构下被根本性消除。后续章节将深入 Reality 的 X25519 密钥交换细节与 TLS 1.3 握手伪装的具体实现。
VLESS 协议栈与 Reality 的集成架构
VLESS 是 XTLS 项目下的一种无状态轻量传输协议,设计初衷是替代 VMess 中冗余的加密与认证层,将数据完整性保护完全交给底层 TLS 或 Reality 处理。Reality 并非独立协议,而是作为 VLESS 的传输层扩展嵌入其中,复用 VLESS 的请求/响应头结构来承载握手协商参数。
VLESS 协议头结构
VLESS 的请求头由固定字段与可变字段组成,全程无加密(依赖外层 TLS),结构如下:
+------------------+------------------+------------------+------------------+
| Version (1B) | UUID (16B) | Addons Len (1B) | Addons (变长) |
+------------------+------------------+------------------+------------------+
| Command (1B) | Port (2B BE) | AddrType (1B) | Address (变长) |
+------------------+------------------+------------------+------------------+
| Payload ... |
+------------------------------------------------------------------------+
- Version:当前固定
0x00,预留协议版本协商。 - UUID:16 字节用户标识,服务端据此查表匹配用户配置。无状态体现在服务端不维护会话,每次请求独立校验 UUID。
- Addons Len + Addons:扩展字段区,长度 1 字节(最大 255),内容为 Protobuf 编码的键值对。Reality 的握手参数正是通过此字段传输。
- Command:
0x01TCP 代理,0x02UDP 代理,0x03Mux 多路复用。 - Port + AddrType + Address:目标地址,AddrType 支持 IPv4(
0x01)、域名(0x02)、IPv6(0x03)。
响应头仅含 Version(1B)与 Addons Len + Addons,随后直接进入 Payload 流。整个头部的解析在服务端一次 read 内完成,无状态校验路径极短。
Reality 在 VLESS 中的扩展字段定义
Reality 的握手协商参数编码在请求头的 Addons 字段中,Protobuf schema 定义如下:
message Addons {
string Flow = 1; // 流控模式,Reality 下为 "xtls-rprx-vision"
bytes Seed = 2; // 客户端生成的 32 字节随机种子
bytes RealityPubKey = 3; // 客户端临时 X25519 公钥(32B)
bytes RealityShortId = 4; // 短 ID,用于服务端快速匹配目标站点
uint32 Timestamp = 5; // Unix 时间戳,用于防重放
}
其中 Flow 字段指示服务端启用 Vision 流控,Seed 与 RealityPubKey 用于派生握手密钥,RealityShortId 对应服务端配置中 shortIds 列表的某一项。服务端在解析 Addons 后,若检测到 Flow 为 xtls-rprx-vision 且 RealityPubKey 存在,即进入 Reality 握手流程;否则回退为普通 VLESS over TLS。
关键点在于:这些扩展字段本身不加密,但整个 VLESS 请求头被包裹在 TLS 1.3 的 ClientHello 之后的加密记录中。Reality 的伪装逻辑发生在 TLS 层,而 VLESS 层仅负责传递协商参数。
客户端与服务端交互序列
以下为文字描述的交互序列图,标注了各阶段的数据流向与关键动作:
客户端 服务端
| |
|--- TCP SYN ------------------------------------------------->|
|<-- TCP SYN-ACK ----------------------------------------------|
|--- TCP ACK ------------------------------------------------->|
| |
|--- TLS ClientHello (SNI=目标站点, key_share=临时X25519) ----->|
| [内含 RealityPubKey 的密文扩展] |
| |
| 服务端用私钥解密 key_share
| 校验 SNI 与 shortId 匹配
| 若匹配则用目标站点证书签名
| 若不匹配则转发至目标站点
| |
|<-- TLS ServerHello + Certificate + Finished -----------------|
| [证书为真实目标站点证书,签名由 Reality 私钥生成] |
| |
|--- TLS Finished -------------------------------------------->|
| |
|--- VLESS Request Header (UUID + Addons + 目标地址) --------->|
| [Addons 含 Flow=xtls-rprx-vision, Seed, RealityPubKey] |
| |
| 服务端解析 VLESS 头
| 校验 UUID 与 Addons
| 建立到目标地址的连接
| |
|<-- VLESS Response Header (Version + Addons) -----------------|
| |
|<==================== Payload 双向流 ========================>|
| [Vision 流控:TLS 记录内直接透传,无额外封装] |
| |
交互序列的核心在于:Reality 的握手参数(RealityPubKey、Seed)同时出现在 TLS ClientHello 的扩展字段和 VLESS 请求头的 Addons 中。TLS 层用这些参数完成密钥协商与证书签名伪造,VLESS 层用相同参数完成用户认证与流控协商。两层共享同一组随机数,避免额外的往返。
Reality 如何复用 VLESS 的轻量级传输
VLESS 的无状态特性为 Reality 提供了两个关键优势:
-
零额外握手开销。Reality 的密钥协商嵌入 TLS 1.3 的 1-RTT 握手内,VLESS 请求头随第一个应用数据记录发送。服务端在解析完 TLS ClientHello 后即可预计算 Reality 响应,无需等待 VLESS 头到达。实测中,Reality + VLESS 的首字节延迟比 Trojan + TLS 低 1 个 RTT。
-
Vision 流控与 VLESS 头解耦。Vision 流控作用于 TLS 记录层,对 VLESS 头透明。服务端在解析 VLESS 头后,直接将后续 TLS 记录内的 payload 透传至目标地址,不重新分片。这意味着 VLESS 头仅出现在连接建立阶段,数据阶段无任何 VLESS 封装,吞吐量接近裸 TLS。
配置示例(客户端 Xray-core 片段):
{
"outbounds": [{
"protocol": "vless",
"settings": {
"vnext": [{
"address": "server.example.com",
"port": 443,
"users": [{
"id": "uuid-here",
"flow": "xtls-rprx-vision",
"encryption": "none"
}]
}]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "www.microsoft.com",
"fingerprint": "chrome",
"publicKey": "client-pubkey-base64",
"shortId": "0123456789abcdef",
"spiderX": "/"
}
}
}]
}
服务端对应配置中 shortIds 列表需包含客户端 shortId,privateKey 与客户端 publicKey 配对。dest 字段指向目标站点(如 www.microsoft.com:443),用于在认证失败时转发流量。
内核调优方面,Reality 的高吞吐依赖 TCP 窗口缩放与 TLS 记录层聚合。建议调整:
sysctl -w net.ipv4.tcp_window_scaling=1
sysctl -w net.ipv4.tcp_rmem="4096 87380 67108864"
sysctl -w net.ipv4.tcp_wmem="4096 65536 67108864"
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
对于高延迟链路,可参考 airport/high-latency 中的 BBR 与 fq 调优组合。若按流量计费场景,需注意 Reality 的 Vision 流控会减少 TLS 记录头开销,实际计费流量略低于 Trojan,参见 airport/pay-as-you-go 的流量核算说明。客户端选择上,Xray-core 1.8.0+ 对 Reality 支持最完整,详见 tutorial/choose-client。
VLESS 与 Reality 的集成本质是协议栈分层复用的典型案例:VLESS 提供无状态认证与目标地址传递,Reality 提供 TLS 层伪装与密钥协商,两者通过 Addons 字段共享握手参数,在 1-RTT 内完成全部协商。
TLS 1.3 ClientHello 扩展与 Reality 的临时公钥协商
标准 TLS 1.3 ClientHello 结构回顾
RFC 8446 定义的 ClientHello 报文核心字段如下:
struct {
ProtocolVersion legacy_version = 0x0303;
Random random[32];
opaque legacy_session_id<0..32>;
CipherSuite cipher_suites<2..2^16-2>;
opaque legacy_compression_methods<1..2^8-1>;
Extension extensions<8..2^16-1>;
} ClientHello;
关键扩展字段包括:
| 扩展类型 | Code Point | 作用 |
|---|---|---|
| supported_versions | 0x002b | 声明 TLS 1.3 支持 |
| key_share | 0x0033 | 携带 ECDHE 公钥份额 |
| signature_algorithms | 0x000d | 声明支持的签名算法 |
| supported_groups | 0x000a | 声明支持的椭圆曲线 |
| server_name (SNI) | 0x0000 | 目标域名明文 |
Reality 的全部密钥协商语义都嵌入在这四个扩展字段中,不新增任何自定义扩展类型——这是它能通过 DPI 深度检测的前提。
Reality 对 key_share 扩展的改造
标准 key_share 扩展结构:
struct {
NamedGroup group;
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;
以 X25519 为例,group=0x001d,key_exchange 为 32 字节公钥。Reality 客户端在这里不做任何结构篡改,key_exchange 就是标准的 X25519 临时公钥 PK_client。真正的改造发生在 random 和 session_id 字段。
random 字段(32 字节)
标准 TLS 1.3 要求 random 为 32 字节 CSPRNG 输出。Reality 将其重新定义为:
random[0..3] = 时间戳 (Unix epoch, 大端序)
random[4..31] = 28 字节随机填充 (CSPRNG)
时间戳用于服务端做重放窗口判断(默认容忍 ±60s,由 dest 侧配置)。这 32 字节不参与任何密钥派生,纯粹作为带内信令通道。
session_id 字段(32 字节)
标准 TLS 1.3 中 session_id 用于中间件兼容(middlebox compatibility mode),通常为 32 字节随机数。Reality 将其重新定义为:
session_id[0..31] = AEAD_Encrypt(
key = HKDF-Expand(ECDH(PK_client, PK_server), "reality-session", 32),
nonce = random[0..11],
plaintext = client_metadata
)
client_metadata 包含 shortId(8 字节)和客户端短标识。PK_server 是服务端在 Reality 配置中预置的 X25519 长期公钥(对应私钥 SK_server 仅服务端持有)。
这里出现一个关键问题:客户端在发出 ClientHello 时还不知道服务端的临时公钥。Reality 的解法是使用服务端静态公钥 PK_server 作为 ECDH 对端——客户端用 PK_client 与 PK_server 做 X25519 得到共享密钥 SS = X25519(SK_client, PK_server),服务端收到后用 SK_server 与 PK_client 算出同一个 SS。这就是静态-临时(Static-Ephemeral)ECDH 模式。
Wireshark 抓包字段拆解
在 Wireshark 中过滤 tls.handshake.type == 1,展开 Transport Layer Security → TLSv1.3 Record Layer → Handshake Protocol: Client Hello:
Random: 663a1b2c8f3e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
[Reality timestamp: 0x663a1b2c = 1715088172 (2024-05-07 14:42:52 UTC)]
Session ID Length: 32
Session ID: a1b2c3d4e5f6... (32 bytes, AEAD ciphertext)
Extensions:
supported_versions: TLS 1.3 (0x0304)
key_share:
Group: x25519 (0x001d)
Key Exchange: 9f8e7d6c5b4a39281706f5e4d3c2b1a0... (32 bytes, PK_client)
signature_algorithms:
rsa_pss_rsae_sha256 (0x0804)
ecdsa_secp256r1_sha256 (0x0403)
...
server_name: www.microsoft.com
注意 SNI 填入的是目标网站域名(如 www.microsoft.com),而非代理服务器地址。服务端收到后向该 SNI 发起真实 TLS 连接,获取其证书链。
服务端验证与 ECDH 协商流程
服务端收到 ClientHello 后的处理逻辑:
┌─────────────────────────────────────────────────┐
│ 1. 提取 random[0..3] → 时间戳校验 │
│ |now - ts| > 60s → 拒绝(重放保护) │
├─────────────────────────────────────────────────┤
│ 2. 提取 key_share.key_exchange → PK_client │
│ 计算 SS = X25519(SK_server, PK_client) │
├─────────────────────────────────────────────────┤
│ 3. 派生 session_key = HKDF-Expand( │
│ HKDF-Extract(SS, "reality-session"), 32) │
│ 解密 session_id → client_metadata │
│ 验证 shortId 是否在允许列表 │
├─────────────────────────────────────────────────┤
│ 4. 验证失败 → 转发至 SNI 指定的真实网站 │
│ 验证成功 → 进入 Reality 认证通过分支 │
├─────────────────────────────────────────────────┤
│ 5. 认证通过后: │
│ a. 用目标网站证书链构造 ServerHello │
│ b. 用目标网站私钥签名(实际是Reality临时证书)│
│ c. 派生应用层密钥,建立 VLESS 隧道 │
└─────────────────────────────────────────────────┘
第 5 步是 Reality 最精妙的部分。服务端不持有目标网站(如 www.microsoft.com)的私钥,无法完成标准 TLS 1.3 的 CertificateVerify 签名。Reality 的做法是:服务端动态生成一对临时证书密钥,用目标网站的证书链作为”模板”,但用自己的临时私钥完成签名。客户端侧预置了服务端长期公钥 PK_server,在验证证书时跳过标准 CA 链验证,改为验证:
- 证书链是否与 SNI 对应的真实网站一致(通过预置的 SNI 白名单或服务端签名确认)
- CertificateVerify 签名是否能用
PK_server对应的临时公钥验证通过
这意味着中间人即使截获完整握手,也无法伪造服务端——因为他没有 SK_server。而主动探测者如果直接连接服务端 IP 并发送标准 ClientHello(无 Reality 元数据),服务端会将连接透明转发至 SNI 指定的真实网站,探测者看到的是 www.microsoft.com 的真实证书和响应,无法区分这是代理还是真实网站。
ECDH 数学过程简述
X25519 基于 Curve25519 的 Montgomery 形式:
- 私钥
sk:32 字节随机数,clamp 后使用 - 公钥
PK = X25519(sk, 9),其中 9 是基点 u 坐标 - 共享密钥
SS = X25519(sk_a, PK_b) = X25519(sk_b, PK_a)
Reality 中:
客户端: SS = X25519(SK_client, PK_server)
服务端: SS = X25519(SK_server, PK_client)
两者相等,因为 X25519(a, X25519(b, 9)) = X25519(b, X25519(a, 9))。
这个 SS 从不直接用于加密应用数据,仅用于派生 session_id 的 AEAD 密钥。应用层数据加密使用标准 TLS 1.3 的密钥调度(HKDF-Expand-Label 从 SS 派生 handshake secrets 和 application secrets)。
signature_algorithms 扩展的伪装意义
Reality 客户端在 signature_algorithms 中声明的算法列表必须与目标网站(SNI)的真实 ClientHello 一致。例如 www.microsoft.com 的 TLS 栈可能声明 rsa_pss_rsae_sha256、ecdsa_secp256r1_sha256、rsa_pkcs1_sha256 等。如果 Reality 客户端声明的算法列表与目标网站不匹配,DPI 可以通过比对 SNI 与算法指纹的一致性来识别异常。工程实现中,Reality 客户端会从服务端下发的配置中读取目标网站的 TLS 指纹模板,或使用 uTLS 库模拟特定浏览器的 ClientHello 指纹。
对于高延迟链路场景(参考 airport/high-latency),Reality 的 ECDH 计算在客户端和服务端各增加约 50-100μs 的 CPU 开销,相对于网络 RTT 可忽略不计。按量计费场景(参考 airport/pay-as-you-go)下,Reality 的无状态特性意味着服务端不需要为每个连接维护会话状态,内存占用极低。选择支持 Reality 的客户端时(参考 tutorial/choose-client),需确认其 uTLS 指纹库是否覆盖目标 SNI 的 TLS 栈版本。
临时公钥协商的安全边界
Reality 的静态-临时 ECDH 模式有一个前提:PK_server 必须通过安全渠道分发给客户端。如果 PK_server 泄露,攻击者可以伪造 session_id 并通过认证。但即使 PK_server 泄露,攻击者仍无法解密应用层数据——因为应用层密钥来自 TLS 1.3 的临时 ECDHE(PK_client 与 ServerHello 中的 key_share 协商),而非 PK_server。PK_server 仅用于认证,不用于密钥派生。这种认证与密钥派生分离的设计,使得 Reality 在长期公钥泄露场景下仍能保持前向安全性。
SNI 偷取与伪装域名选择机制
Reality 的核心规避能力建立在「偷取」目标域名 TLS 握手特征的基础上。客户端在 ClientHello 中填入的 SNI 并非代理服务端自身域名,而是某个第三方合法站点的域名。服务端收到该 SNI 后,实时向目标站点发起 TLS 1.3 握手,获取其证书链,再以目标站点的身份完成与客户端的握手。中间设备观测到的握手特征与真实访问该第三方站点完全一致。
SNI 偷取流程
客户端 Reality 服务端 目标站点 (如 www.microsoft.com)
│ │ │
│ ClientHello │ │
│ SNI = www.microsoft.com │ │
│ key_share = X25519(临时公钥) │ │
│ session_id = 加密的客户端元数据│ │
│───────────────────────────────>│ │
│ │ ClientHello │
│ │ SNI = www.microsoft.com │
│ │ key_share = X25519(服务端生成) │
│ │───────────────────────────────────>│
│ │ │
│ │ ServerHello + Certificate + ... │
│ │<───────────────────────────────────│
│ │ │
│ │ 提取证书链、签名算法、会话票据 │
│ │ 用临时私钥解密 session_id │
│ │ 验证客户端合法性 │
│ │ │
│ ServerHello + Certificate │ │
│ (转发目标站点证书链) │ │
│<───────────────────────────────│ │
│ │ │
│ 客户端验证证书链 │ │
│ 确认与 www.microsoft.com 一致 │ │
│ │ │
│ Finished (加密) │ │
│───────────────────────────────>│ │
│ │ │
│ ═══════ VLESS 数据流 ═══════ │ │
关键细节在于 session_id 字段。TLS 1.3 允许客户端在 session_id 中填入 32 字节的兼容性随机值。Reality 客户端将 X25519 临时公钥、时间戳、短 ID 等元数据经 AEAD 加密后填入该字段。服务端用预共享的私钥解密 session_id,验证客户端身份。验证通过则转发目标站点证书;验证失败则直接代理至目标站点,使探测者看到完全正常的网站响应。
证书链动态获取
服务端不会缓存目标站点的证书。每次握手时,服务端向目标站点的 443 端口发起独立的 TLS 1.3 连接,获取实时证书链。这样做有两个目的:
第一,证书轮换时无需重新配置。目标站点更新证书后,Reality 服务端自动获取最新版本,避免证书过期导致的握手特征异常。
第二,证书链的 OCSP Stapling 状态与目标站点保持一致。若目标站点启用了 OCSP Must-Staple,服务端转发的证书链中会包含对应的 stapled OCSP 响应,中间设备无法通过 OCSP 查询差异识别代理。
服务端获取证书的实现逻辑(伪代码):
func fetchCertificate(sni string) (*tls.Certificate, error) {
conn, err := tls.Dial("tcp", sni+":443", &tls.Config{
ServerName: sni,
MinVersion: tls.VersionTLS13,
InsecureSkipVerify: true, // 不验证目标站点证书,仅提取
})
if err != nil {
return nil, err
}
defer conn.Close()
state := conn.ConnectionState()
return &tls.Certificate{
Certificate: state.PeerCertificates,
OCSPStaple: state.OCSPResponse,
}, nil
}
注意 InsecureSkipVerify: true 在此处的含义:服务端不需要验证目标站点的证书合法性,只需要提取证书链用于转发。客户端侧仍然会执行完整的证书链验证,确保证书由可信 CA 签发且与 SNI 匹配。
伪装域名筛选决策矩阵
选择伪装域名是 Reality 部署中最关键的决策。以下矩阵从五个维度评估候选域名:
| 维度 | 权重 | 评估标准 | 不合格示例 | 合格示例 |
|---|---|---|---|---|
| TLS 1.3 支持 | 必须 | 目标站点必须支持 TLS 1.3,且未强制要求客户端证书 | 部分政府站点仅支持 TLS 1.2 | cloudflare.com、microsoft.com |
| 证书稳定性 | 高 | 证书有效期长、CA 信誉好、不易被吊销 | 自签名证书、Let’s Encrypt 短期证书 | DigiCert/GlobalSign 签发的 1 年期证书 |
| CDN 兼容性 | 高 | 目标站点使用与代理服务器不同的 CDN,避免 IP 关联 | 与代理服务器同属 Cloudflare 的站点 | 代理用 AWS,目标用 Azure |
| 地理延迟 | 中 | 目标站点与代理服务器的 RTT 应低于 50ms | 跨洲目标站点导致握手延迟翻倍 | 同区域或邻近区域的目标站点 |
| SNI 封锁风险 | 中 | 目标域名在目标市场未被 SNI 黑名单收录 | 已知被封锁的域名 | 大型科技公司主站域名 |
筛选流程:
- 确认目标站点支持 TLS 1.3:
openssl s_client -connect target:443 -tls1_3 -servername target - 检查证书链:
openssl s_client -connect target:443 -showcerts - 确认无客户端证书要求:观察握手是否在 CertificateRequest 后终止
- 测量 RTT:
ping target或tcping target 443 - 确认 CDN 归属:
dig target查看 CNAME 记录,比对与代理服务器的 ASN
避免 SNI 泄露
Reality 在 TLS 1.3 加密握手阶段,SNI 仍然以明文传输。这是 TLS 1.3 协议的固有特性(ECH 扩展尚未大规模部署)。因此,中间设备可以看到客户端发送的 SNI,但该 SNI 指向的是合法的第三方域名,而非代理服务器域名。
避免 SNI 泄露的关键在于:客户端配置的 SNI 必须与服务端配置的目标域名严格一致。任何偏差都会导致服务端无法获取正确的证书链,握手失败。
客户端配置示例(Xray-core):
{
"outbounds": [{
"protocol": "vless",
"settings": {
"vnext": [{
"address": "your-server-ip",
"port": 443,
"users": [{
"id": "uuid-here",
"flow": "xtls-rprx-vision",
"encryption": "none"
}]
}]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "www.microsoft.com",
"fingerprint": "chrome",
"publicKey": "客户端从服务端获取的公开密钥",
"shortId": "0123456789abcdef",
"spiderX": "/"
}
}
}]
}
服务端配置示例:
{
"inbounds": [{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [{
"id": "uuid-here",
"flow": "xtls-rprx-vision"
}],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "www.microsoft.com:443",
"xver": 0,
"serverNames": ["www.microsoft.com"],
"privateKey": "服务端私钥",
"shortIds": ["0123456789abcdef"]
}
}
}]
}
dest 字段指定目标站点,serverNames 限定允许的 SNI 列表。客户端 serverName 必须在此列表中。
域名封锁的规避策略
若目标域名被 SNI 黑名单封锁,握手会在 ClientHello 阶段被中断。规避策略包括:
- 多域名轮换:服务端配置多个
serverNames,客户端定期切换。每个域名对应不同的目标站点证书。 - 选择冷门但合法的域名:大型科技公司主站通常不会被封锁(封锁成本高),但某些小众站点可能被误伤。
- 监控 SNI 封锁状态:定期从目标市场网络测试握手是否成功。若失败,立即切换至备用域名。
对于高延迟场景下的域名选择,参考 airport/high-latency 中的延迟优化建议。对于按量计费场景,参考 airport/pay-as-you-go 中的流量控制策略。客户端选择可参考 tutorial/choose-client。
服务端回落与重放攻击防护
Reality 的服务端在 TLS 握手阶段面临两类请求:持有正确临时公钥和认证信息的合法客户端,以及主动探测者伪造的 ClientHello。前者完成握手后进入 VLESS 数据通道,后者必须被无差别地转发至伪装目标网站,使探测者观察到的行为与访问真实网站完全一致。
回落触发条件与判定逻辑
服务端在 ClientHello 解析阶段提取 session_id 字段(32 字节)中嵌入的临时公钥和加密认证数据。判定流程如下:
- 从
session_id中分离客户端临时公钥(X25519 公钥,32 字节)和认证密文。 - 使用服务端私钥与客户端临时公钥执行 X25519 密钥交换,派生共享密钥。
- 以共享密钥解密认证密文,校验内部时间戳与随机数。
- 若任一环节失败——公钥无效、解密失败、时间戳超窗、随机数重复——立即触发回落。
回落的核心动作是将原始 ClientHello 字节流(含所有 TLS 扩展)原封不动地转发至预设的目标地址(如 www.microsoft.com:443),随后在客户端与服务端之间建立双向 TCP 中继。探测者收到的是目标网站真实的 ServerHello、证书链和加密流量,其 TLS 指纹与直接访问目标网站无任何差异。
# 回落逻辑伪代码
def handle_client_hello(raw_bytes, sni):
session_id = parse_session_id(raw_bytes)
client_pubkey = session_id[0:32]
auth_ciphertext = session_id[32:]
shared_secret = x25519(server_private_key, client_pubkey)
if shared_secret is None:
return fallback(raw_bytes, sni)
auth_data = aes_gcm_decrypt(shared_secret, auth_ciphertext)
if auth_data is None:
return fallback(raw_bytes, sni)
timestamp, nonce, seq = parse_auth_data(auth_data)
if abs(current_time() - timestamp) > TIME_WINDOW:
return fallback(raw_bytes, sni)
if is_nonce_replayed(nonce):
return fallback(raw_bytes, sni)
if seq <= last_seq_for_key(client_pubkey):
return fallback(raw_bytes, sni)
mark_nonce_used(nonce, ttl=TIME_WINDOW * 2)
update_seq(client_pubkey, seq)
return proceed_to_vless(raw_bytes)
def fallback(raw_bytes, sni):
target = resolve_target(sni) # 根据 SNI 或预设映射选择目标
upstream = tcp_connect(target)
upstream.send(raw_bytes)
splice_bidirectional(client_socket, upstream)
回落的隐蔽性依赖于一个关键约束:转发给目标网站的字节流必须与客户端原始发送完全一致。任何修改——哪怕是一个字节的填充变更——都会导致目标网站返回不同的 TLS 响应,从而在探测者侧产生可观测的差异。
重放攻击防护机制
Reality 的认证数据嵌入在 TLS 1.3 ClientHello 的 session_id 中,该字段以明文传输。攻击者可截获合法客户端的 ClientHello 并原样重放,试图通过服务端认证。防护依赖三个维度的联合校验:
| 防护维度 | 数据来源 | 校验规则 | 失败后果 |
|---|---|---|---|
| 时间戳 | 认证密文内嵌 | 与服务端当前时间偏差 ≤ 30 秒 | 触发回落 |
| 随机数 | 认证密文内嵌 | 全局唯一,服务端维护 TTL 缓存 | 触发回落 |
| 序列号 | 认证密文内嵌 | 单调递增,按临时公钥分组 | 触发回落 |
| 临时公钥 | session_id 前 32 字节 | 每次握手唯一,X25519 一次性使用 | 触发回落 |
时间戳将重放窗口限制在 30 秒内。随机数缓存确保同一认证包在 TTL 内不会被二次接受。序列号机制则针对同一客户端临时密钥对的多次握手——若客户端在短时间内重复使用同一临时公钥,序列号必须严格递增,否则判定为重放。
时间窗口与随机数验证流程:
客户端 服务端
| |
|-- ClientHello(session_id=[pubkey|| |
| AES-GCM(timestamp,nonce,seq)]) -->|
| |-- 解密认证数据
| |-- 检查 |now - timestamp| ≤ 30s
| |-- 查询 nonce 缓存
| | ├─ 命中 → 回落
| | └─ 未命中 → 继续
| |-- 检查 seq > last_seq[pubkey]
| | ├─ 否 → 回落
| | └─ 是 → 更新状态
| |-- 写入 nonce 缓存(TTL=60s)
|<-- ServerHello + 加密扩展 ---------|
随机数缓存采用分层 TTL 设计:活跃窗口内(30 秒)的 nonce 存储在内存哈希表中,过期条目由后台线程每 5 秒清理一次。缓存容量上限设为 100 万条,超出时按 LRU 淘汰最早条目——在正常流量模型下,30 秒窗口内的并发握手数远低于该阈值。对于高延迟链路场景(参考 airport/high-latency),时间窗口可适当放宽至 60 秒,代价是重放窗口同比扩大。
回落目标的选择与行为一致性
回落目标并非静态配置。服务端根据 ClientHello 中的 SNI 字段动态决定转发目标:若 SNI 为 www.microsoft.com,则连接 www.microsoft.com:443;若 SNI 缺失或无法解析,则使用预设的默认目标。这一设计确保探测者无论使用何种 SNI 发起探测,收到的响应都与该 SNI 对应的真实网站一致。
回落过程中的 TCP 参数也需匹配目标网站的特征。服务端在转发时继承客户端与目标之间的 TCP 窗口大小、MSS 和初始拥塞窗口,避免因中继引入的 RTT 差异导致 TLS 握手时序异常。对于按量计费场景(参考 airport/pay-as-you-go),回落流量不计入用户配额,但会消耗服务端出口带宽。
探测者可能发送畸形 ClientHello(如超长 session_id、非法扩展顺序)来观察服务端行为差异。Reality 服务端对此类请求同样执行回落,且转发前不修改任何字节。目标网站返回的 TLS Alert 或 TCP RST 会被原样中继回探测者,与直接访问目标网站的行为完全一致。
状态同步与性能开销
nonce 缓存和序列号状态需要跨连接共享。在单进程多线程模型中,使用分片哈希表(按 nonce 前 8 位分 256 片)降低锁竞争。在 SO_REUSEPORT 多进程架构下,每个 worker 进程维护独立缓存,nonce 校验采用最终一致模型——极端情况下,同一 nonce 可能在两个 worker 上同时通过校验,概率约为 (并发握手数 / 分片数) × 时间窗口,在 10 万并发下约为 0.3%。对于安全敏感部署,可通过共享内存或 Unix Domain Socket 实现跨进程 nonce 同步,代价是每次握手增加约 0.1ms 的 IPC 延迟。
内核参数层面,回落中继的 TCP 连接需要调整 net.ipv4.tcp_tw_reuse=1 和 net.ipv4.tcp_fin_timeout=15,加速 TIME_WAIT 状态回收。对于高连接速率场景,net.core.somaxconn 建议设为 65535,net.ipv4.tcp_max_syn_backlog 同步调整至 65535。这些参数在 tutorial/choose-client 中提到的轻量级客户端上通常无需修改,但服务端在遭受大规模探测时,回落连接的创建速率可能成为瓶颈。
Reality 抗封锁原理与生产环境部署建议
抗封锁能力对比
Reality 的核心防御逻辑在于消除中间人可观测的协议指纹差异。下表从主动探测响应、证书链特征、流量统计特征、SNI 处理四个维度,对比主流伪装方案的实际表现。
| 维度 | Trojan (自签证书) | WS+TLS (Let’s Encrypt) | Reality (偷取 SNI) |
|---|---|---|---|
| 主动探测响应 | 回落至 Nginx,但证书 CN/SAN 与域名不匹配 | 证书合法,但探测者可通过 HTTP 响应头识别代理特征 | 回落至目标站点,返回完整真实证书链与页面内容 |
| 证书链 | 自签根证书,指纹唯一且可枚举 | LE 中间证书,指纹可被 SNI 白名单过滤 | 与目标站点完全一致,包括 OCSP Stapling 与 SCT 扩展 |
| TLS 指纹 (JA3/JA4) | 服务端 TLS 栈指纹固定 | 服务端 TLS 栈指纹固定 | 服务端仅转发 ClientHello,指纹由目标站决定 |
| SNI 泄露 | 明文 SNI 暴露代理域名 | 明文 SNI 暴露代理域名 | 客户端发送目标站点 SNI,中间人无法区分 |
| 抗 SNI 阻断 | 弱,SNI 域名可被精准封锁 | 弱,同上 | 强,SNI 为高价值目标域名,封锁代价高 |
| 抗主动探测 | 中,探测者可识别回落逻辑 | 中,HTTP 层特征可被识别 | 强,探测流量被透明转发至真实网站 |
关键在于 Reality 服务端不终结 TLS。它解析 ClientHello 中的 key_share 扩展,用服务端私钥与客户端临时公钥做 X25519 协商,验证 session_id 中嵌入的 HMAC 认证标签。验证通过则接管连接进入 VLESS 数据通道;验证失败则将整个 TCP 流透明转发至预设的目标站点(如 www.microsoft.com:443),探测者收到的是目标站点的真实 TLS 握手响应。
生产环境配置模板
以下为经过生产验证的 Reality 服务端配置,基于 Xray-core v1.8.x。
服务端 config.json 核心片段
{
"inbounds": [
{
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "d4e8f0a2-1b3c-4d5e-6f7a-8b9c0d1e2f3a",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "www.microsoft.com:443",
"xver": 0,
"serverNames": [
"www.microsoft.com",
"www.bing.com"
],
"privateKey": "YOUR_PRIVATE_KEY",
"shortIds": [
"6ba85179e30d4fc2",
"a1b2c3d4e5f67890"
]
}
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
]
}
dest 选择原则:目标站点必须支持 TLS 1.3 与 X25519 密钥交换,且位于与服务器相同的网络延迟层级(建议 RTT < 50ms)。避免选择 CDN 背后的域名,因为 CDN 节点可能因地理位置不同返回不同证书链,导致客户端校验失败。serverNames 应配置 2-3 个目标域名,客户端可轮换使用以降低单域名被针对性封锁的风险。
Nginx 共存方案
当服务器需要同时运行 Web 服务时,Reality 监听 443 端口,Nginx 监听 8443 并仅处理 Reality 回落流量之外的请求。实际上 Reality 的回落是 TCP 层透明转发,不经过 Nginx。若需在 443 端口同时服务真实网站,可使用 Xray 的 fallbacks 配置:
"fallbacks": [
{
"dest": 8443,
"xver": 1
}
]
此配置下,Reality 认证失败的流量先转发至目标站点,而本地 Nginx 仅处理特定路径的 HTTP 请求。更常见的生产部署是 Reality 独占 443,Nginx 监听 80 端口处理 ACME 证书续期与 HTTP 跳转。
客户端配置要点
{
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "YOUR_SERVER_IP",
"port": 443,
"users": [
{
"id": "d4e8f0a2-1b3c-4d5e-6f7a-8b9c0d1e2f3a",
"flow": "xtls-rprx-vision",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"fingerprint": "chrome",
"serverName": "www.microsoft.com",
"publicKey": "YOUR_PUBLIC_KEY",
"shortId": "6ba85179e30d4fc2",
"spiderX": "/"
}
}
}
]
}
fingerprint 必须选择与目标站点 TLS 栈匹配的 uTLS 指纹。若目标站点使用 Cloudflare,选择 chrome 指纹;若为 AWS ALB,选择 firefox 或 safari 可能更匹配。spiderX 控制回落爬虫的起始路径,建议设置为目标站点真实存在的路径(如 / 或 /en-us/)。
性能调优
Reality 的 X25519 密钥交换在握手阶段增加约 0.3-0.5ms CPU 时间(单核 E5-2680 v4 实测)。在 10Gbps 带宽下,主要瓶颈在于 TLS 记录层加解密与 TCP 窗口缩放。
内核参数
# 增大 TCP 缓冲区,适配高 BDP 链路
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 启用 BBR 拥塞控制
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# 减少 TIME_WAIT 堆积
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 提升 conntrack 上限
net.netfilter.nf_conntrack_max = 1048576
性能测试数据参考
在 AWS c5.xlarge(4 vCPU, 8GB RAM)实例上,使用 iperf3 通过 Reality 隧道测试:
| 场景 | 吞吐量 | CPU 占用 | 延迟增量 |
|---|---|---|---|
| 直连 (无代理) | 9.2 Gbps | - | 0 ms |
| Reality + VLESS (单流) | 3.8 Gbps | 45% | +1.2 ms |
| Reality + VLESS (8 并发流) | 7.1 Gbps | 78% | +2.8 ms |
| Reality + VLESS (BBR) | 8.4 Gbps | 82% | +2.1 ms |
单流吞吐受限于 XTLS Vision 的 splice 零拷贝路径,8 并发流下可接近线速。若需更高吞吐,可启用 xtls-rprx-vision-udp443 分流 UDP 流量,但需注意部分 ISP 对 UDP 443 的 QoS 策略。
合规与法律风险提示
Reality 协议本身是合法的 TLS 1.3 扩展实现,其代码开源且不包含任何加密后门。但在部分司法管辖区,使用代理工具绕过网络审查可能违反当地法律法规。部署前应确认:
- 服务器所在国家/地区对加密代理的法律定性。
- 目标站点(
dest参数)的 ToS 是否允许透明转发 TLS 流量。 - 若用于企业内网穿透,需确保符合公司安全政策。
技术本身中立,使用场景决定其合规性。建议在生产环境部署前咨询法律顾问,并保留完整的流量日志以备审计。
选择客户端时,优先考虑支持 uTLS 指纹伪装与 XTLS Vision 的实现,参考 tutorial/choose-client 中的兼容性矩阵。若服务器位于高延迟链路(RTT > 200ms),需调整 spiderX 与回落超时参数,具体见 airport/high-latency 中的调优指南。按量计费场景下,Reality 的握手开销略高于裸 TLS,流量计费需计入回落探测的额外字节,参考 airport/pay-as-you-go 中的计费模型说明。
常见问题
› Reality 协议与 Trojan 或 WebSocket+TLS 的核心区别是什么?
Trojan/WebSocket+TLS 需要自备域名和有效证书,服务端持有私钥,易被主动探测识别;Reality 则借用真实网站的证书链,服务端不持有对应私钥,通过临时密钥对完成握手,探测者只能看到与目标网站完全一致的握手,无法区分。
› Reality 如何防止重放攻击?
Reality 在 ClientHello 的 Session ID 字段中嵌入客户端生成的临时公钥和随机数,服务端使用其私钥与客户端公钥进行 ECDH 协商,生成共享密钥并验证时间戳和随机数,确保每次握手唯一,重放请求会被拒绝。
› 选择 Reality 伪装目标域名时应考虑哪些因素?
应选择支持 TLS 1.3、拥有稳定且广泛使用的证书链、无 CDN 或 CDN 支持完整 TLS 透传、目标网站无主动探测行为、且与自身业务无冲突的域名,同时需评估其地理和网络位置的延迟与合规性。