Skip to content

XTLS 流控机制

位置:核心协议层 | 难度:较难 | 前置TLS 1.3 深入TLS-in-TLSVLESS 协议源码证据proxy/vless/encoding/encoding.go、Project X 官方文档 (xtls.github.io) 外部参考:XTLS Vision 设计讨论 (GitHub Discussions #1295)、RPRX 关于 Vision 的设计说明


1. 解决的问题

当代理工具使用 TLS 作为传输层、而代理的流量本身也是 TLS(如访问 HTTPS 网站)时,形成 TLS-in-TLS 嵌套,审查方可从数据包大小分布中检测异常。

XTLS 的核心思路:识别内部 TLS 流量后,停止外层封装,直接在内核层面转发原始 TLS 数据。结果:审查方在网络上看到的不是"TLS 隧道中的 TLS",而是正常的单层 TLS 流量。


2. TLS 记录识别逻辑

Vision 读取数据流的前几个字节,判断是否为 TLS 记录。

TLS 记录头格式:

[内容类型: 1 byte] [TLS 版本: 2 bytes] [数据长度: 2 bytes]

识别算法(伪代码):

读取 5 字节 → 检查:
  byte[0] 是否为合法内容类型? (0x14-0x17)
  byte[1:3] 是否为合法 TLS 版本? (0x0301-0x0304)
  byte[3:5] 解析为 uint16 → 长度是否在合理范围? (0-16384)

全部通过 → 判定为 TLS 流量 → 分流模式
任一不通过 → 判定为非 TLS → 正常代理模式

为什么检查 5 字节就够

  • TLS 内容类型的合法值只有 4 个(0x14, 0x15, 0x16, 0x17),非这 4 个就是非 TLS
  • TLS 版本的合法范围 0x0301-0x0304 非常窄,随机数据很难落在这个区间
  • 5 字节的判断避免了读取整个 TLS 记录——减少延迟

3. Splice:内核级零拷贝转发

3.1 Linux splice() 系统调用

Linux 的 splice() 系统调用允许在两个文件描述符之间直接在内核空间移动数据,数据不经过用户态内存。

传统代理的数据路径:
  客户端 TCP socket → 内核 → read() → 用户态 buffer → write() → 内核 → 目标 TCP socket
  (2 次内核-用户态拷贝)

XTLS Splice 的数据路径:
  客户端 TCP socket → 内核 → splice() → 内核 → 目标 TCP socket
  (0 次内核-用户态拷贝)

3.2 何时使用 Splice

XTLS 在满足以下条件时启用 Splice:

  1. 内部流量被识别为 TLS
  2. 操作系统支持 splice()(Linux)
  3. 入站和出站连接都是 TCP

Splice 不仅消除了 TLS-in-TLS 特征,还大幅降低了 CPU 使用率——数据拷贝是代理服务器最大的 CPU 开销来源。


4. Vision 填充策略

Vision 是针对 TLS 握手阶段短数据包的定向填充

4.1 为什么 TLS 握手包需要填充

XTLS Splice 消除了"加密内层 TLS"的双层 TLS 记录开销。但还有一个更微妙的问题:TLS 握手包的大小模式

正常 HTTPS 流量的 TLS 握手包大小分布:

  • ClientHello:200-500 字节(包含 SNI、扩展等)
  • ServerHello + Certificate:1500-4000 字节(证书链)
  • 后续 Application Data:900-1400 字节(加密的 HTTP 请求/响应)

XTLS Splice 后的 TLS 握手包就是原始 TLS 握手包——大小完全由目标服务器决定。但当审查方对比"客户端→代理"和"代理→目标"两段连接时,Splice 之前的握手阶段数据包仍有可能被观察到。

4.2 Vision 的填充算法

Vision 只对TLS 握手阶段的短包添加填充:

对于每个 TLS 记录:
  if 记录大小 < 900 字节:
    添加随机长度的填充 → 使总大小进入 [900, 1400] 范围
  else if 记录大小 > 1400 字节:
    不做填充(正常 TLS 记录大小)

与传统随机填充的关键区别:Vision 不填充所有包。只有短包(TLS 握手完成前的标志性短包)才被填充。数据阶段的正常大小包不增加填充开销。

4.3 填充的作用

填充将不同连接的 TLS 握手包大小统一到 [900, 1400] 区间,消除了审查方可以通过包大小来区分连接的途径。

填充的内容是随机数据。这些随机数据在 TLS 通道内传输,最终被客户端或服务端检测并丢弃(因为不是合法的 TLS 记录)。


5. Vision vs Direct(已废弃)

特性xtls-rprx-direct(已废弃)xtls-rprx-vision
Splice
TLS 握手包填充
检测 TLS 流量是(更精确)
抗 TLS-in-TLS 检测部分
抗包大小指纹
状态自 v1.8.0 废弃当前推荐

xtls-rprx-direct 只做 Splice,不做填充。2022 年 GFW 升级后,社区发现 Direct 模式下的 TLS 握手包大小仍然可被指纹识别。


6. XTLS 的局限性

6.1 仅对 TLS 内部流量有效

如果被代理的流量是普通 HTTP(非 TLS),XTLS 无法识别,数据通过正常代理加密传输。此时外层 TLS 隧道仍存在——但因为内层流量不是 TLS,不存在 TLS-in-TLS 特征。

6.2 Linux 依赖

Splice 是 Linux 特有的系统调用。在 macOS、Windows、BSD 上,XTLS 退化为普通代理模式(仍然工作,但没有零拷贝优化)。

6.3 填充开销

Vision 填充增加了带宽开销。填充的数据最终被接收方丢弃,不传输有用信息。对于频繁短连接的场景,填充开销可能显著。

6.4 与 Mux 的兼容性

多路复用(Mux)将多个代理连接共享在一个 TLS 隧道中。Mux 与 XTLS 不兼容——因为 Mux 打乱了内部 TLS 流量的边界,使 XTLS 无法识别单个 TLS 连接。

这导致了 VLESS + XTLS Vision + XHTTP + XMUX 等组合的出现——通过应用层的多路复用替代传输层的 Mux。


7. 在 VLESS 配置中的使用

7.1 常用 flow 值

flow: ""                      → 无流控(普通 TLS 代理)
flow: "xtls-rprx-vision"      → XTLS Vision(推荐)
flow: "xtls-rprx-vision-udp443" → Vision + 允许 UDP/443 端口(可用于 QUIC/HTTP3)

7.2 服务端和客户端必须一致

VLESS 入站配置中的 flow 必须与客户端出站配置中的 flow 匹配。不一致会导致连接失败。

7.3 XTLS 仅在与以下入站协议配合时可用

  • VLESS
  • Trojan
  • Dokodemo-door
  • SOCKS
  • HTTP

8. 外部参考资料

资料:XTLS Vision 设计说明 类型:项目官方讨论(GitHub Discussions) 链接:https://github.com/XTLS/Xray-core/discussions/1295 可信度:A 建议参考:Vision 创建的原始动机和设计权衡

资料:XTLS/Go 项目 类型:开源项目 链接:https://github.com/XTLS/Go 可信度:A 建议参考:XTLS 的原始 Go 实现

资料:nthLink White Paper 类型:技术白皮书 可信度:B 建议参考:TLS-in-TLS 检测对 Trojan-go 和 VMess+TLS 的影响


读者自检

  • XTLS Splice 与传统代理数据路径的核心区别(零拷贝 vs 用户态拷贝)
  • Vision 如何通过 5 字节 TLS 记录头判断内层流量是否为 TLS
  • Vision 的填充策略与传统随机填充的关键区别(只填充短 TLS 握手包)
  • XTLS 的三个主要局限性