HaiwaiWiki 海外Wiki·词条式知识库
目录 ☰

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 伪装方案的技术路线

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 会执行以下验证步骤之一:

  1. HTTP-01:CA 向 http://<domain>/.well-known/acme-challenge/<token> 发起请求,验证 token 内容。
  2. TLS-ALPN-01:CA 在 443 端口发起 TLS 握手,ALPN 扩展携带 acme-tls/1,验证临时证书中的 id-pe-acmeIdentifier 扩展。
  3. 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 协议针对上述缺陷提出三个核心设计目标:

  1. 零证书持有:服务端不申请、不持有任何证书私钥,消除 ACME 暴露链与 CT 日志关联。
  2. 偷取真实 SNI:客户端在 ClientHello 中使用目标伪装域名的 SNI,服务端将该 ClientHello 转发至真实目标服务器完成握手,自身不参与证书签发。
  3. 握手特征不可区分: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:0x01 TCP 代理,0x02 UDP 代理,0x03 Mux 多路复用。
  • 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 提供了两个关键优势:

  1. 零额外握手开销。Reality 的密钥协商嵌入 TLS 1.3 的 1-RTT 握手内,VLESS 请求头随第一个应用数据记录发送。服务端在解析完 TLS ClientHello 后即可预计算 Reality 响应,无需等待 VLESS 头到达。实测中,Reality + VLESS 的首字节延迟比 Trojan + TLS 低 1 个 RTT。

  2. 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_versions0x002b声明 TLS 1.3 支持
key_share0x0033携带 ECDHE 公钥份额
signature_algorithms0x000d声明支持的签名算法
supported_groups0x000a声明支持的椭圆曲线
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 链验证,改为验证:

  1. 证书链是否与 SNI 对应的真实网站一致(通过预置的 SNI 白名单或服务端签名确认)
  2. 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.2cloudflare.com、microsoft.com
证书稳定性高证书有效期长、CA 信誉好、不易被吊销自签名证书、Let’s Encrypt 短期证书DigiCert/GlobalSign 签发的 1 年期证书
CDN 兼容性高目标站点使用与代理服务器不同的 CDN,避免 IP 关联与代理服务器同属 Cloudflare 的站点代理用 AWS,目标用 Azure
地理延迟中目标站点与代理服务器的 RTT 应低于 50ms跨洲目标站点导致握手延迟翻倍同区域或邻近区域的目标站点
SNI 封锁风险中目标域名在目标市场未被 SNI 黑名单收录已知被封锁的域名大型科技公司主站域名

筛选流程:

  1. 确认目标站点支持 TLS 1.3:openssl s_client -connect target:443 -tls1_3 -servername target
  2. 检查证书链:openssl s_client -connect target:443 -showcerts
  3. 确认无客户端证书要求:观察握手是否在 CertificateRequest 后终止
  4. 测量 RTT:ping target 或 tcping target 443
  5. 确认 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 阶段被中断。规避策略包括:

  1. 多域名轮换:服务端配置多个 serverNames,客户端定期切换。每个域名对应不同的目标站点证书。
  2. 选择冷门但合法的域名:大型科技公司主站通常不会被封锁(封锁成本高),但某些小众站点可能被误伤。
  3. 监控 SNI 封锁状态:定期从目标市场网络测试握手是否成功。若失败,立即切换至备用域名。

对于高延迟场景下的域名选择,参考 airport/high-latency 中的延迟优化建议。对于按量计费场景,参考 airport/pay-as-you-go 中的流量控制策略。客户端选择可参考 tutorial/choose-client。

服务端回落与重放攻击防护

Reality 的服务端在 TLS 握手阶段面临两类请求:持有正确临时公钥和认证信息的合法客户端,以及主动探测者伪造的 ClientHello。前者完成握手后进入 VLESS 数据通道,后者必须被无差别地转发至伪装目标网站,使探测者观察到的行为与访问真实网站完全一致。

回落触发条件与判定逻辑

服务端在 ClientHello 解析阶段提取 session_id 字段(32 字节)中嵌入的临时公钥和加密认证数据。判定流程如下:

  1. 从 session_id 中分离客户端临时公钥(X25519 公钥,32 字节)和认证密文。
  2. 使用服务端私钥与客户端临时公钥执行 X25519 密钥交换,派生共享密钥。
  3. 以共享密钥解密认证密文,校验内部时间戳与随机数。
  4. 若任一环节失败——公钥无效、解密失败、时间戳超窗、随机数重复——立即触发回落。

回落的核心动作是将原始 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 Gbps45%+1.2 ms
Reality + VLESS (8 并发流)7.1 Gbps78%+2.8 ms
Reality + VLESS (BBR)8.4 Gbps82%+2.1 ms

单流吞吐受限于 XTLS Vision 的 splice 零拷贝路径,8 并发流下可接近线速。若需更高吞吐,可启用 xtls-rprx-vision-udp443 分流 UDP 流量,但需注意部分 ISP 对 UDP 443 的 QoS 策略。

合规与法律风险提示

Reality 协议本身是合法的 TLS 1.3 扩展实现,其代码开源且不包含任何加密后门。但在部分司法管辖区,使用代理工具绕过网络审查可能违反当地法律法规。部署前应确认:

  1. 服务器所在国家/地区对加密代理的法律定性。
  2. 目标站点(dest 参数)的 ToS 是否允许透明转发 TLS 流量。
  3. 若用于企业内网穿透,需确保符合公司安全政策。

技术本身中立,使用场景决定其合规性。建议在生产环境部署前咨询法律顾问,并保留完整的流量日志以备审计。

选择客户端时,优先考虑支持 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 透传、目标网站无主动探测行为、且与自身业务无冲突的域名,同时需评估其地理和网络位置的延迟与合规性。