XTLS 流控机制
位置:核心协议层 | 难度:较难 | 前置:TLS 1.3 深入、TLS-in-TLS、VLESS 协议源码证据:
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:
- 内部流量被识别为 TLS
- 操作系统支持
splice()(Linux) - 入站和出站连接都是 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 的三个主要局限性