VLESS 协议原理入门:从设计哲学到安全风险

2026 年 7 月 8 日

你有没有遇到过这种情况:一个网页打不开,但换个网络环境就好了,或者某个在线服务突然无法访问——问题不在你自己的电脑上,而是网络路径中某个环节拒绝了连接。

为了解决这类问题,人们设计了各种代理协议(proxy protocol)来做一件事:把数据从 A 点绕过限制送到 B 点。VLESS 是其中一种轻量级传输协议,它来自 Xray / Project V 生态,设计上追求极简和高效。

这篇教程从代理协议的核心问题讲起,逐步深入到 VLESS 的协议结构、安全模型、流控机制,最后整理一份安全风险清单。不需要你先了解 V2Ray 或 Xray 的具体用法,只需要基本的网络概念(TCP、TLS)就能跟上。

学完这篇教程,你可以:


第1章 代理协议解决了什么问题

本章目标:搭建一个清晰的代理协议心智模型——任何代理协议本质上都在做三件事:告诉服务器你是谁、告诉服务器你要去哪里、然后原封不动地搬运数据。

1.1 直连 vs 代理:一个比喻

想象你要寄一个包裹,但是快递公司告诉你"这个地址不送"。你的朋友住在那个地址附近,你可以:

  1. 把包裹先寄给朋友
  2. 朋友收到后,把里面的东西转送到目标地址

这就是代理的最简模型。你的电脑是寄件人,代理服务器是朋友,目标服务器是收件人。


直连:  你 → 目标服务器
代理:  你 → 代理服务器 ↪ 目标服务器

在网络层面,直连时操作系统直接把 TCP 连接发到目标 IP。使用代理时,你的设备先和代理服务器建立一条 TCP 连接,代理服务器收到数据后,再与目标服务器建立另一条连接。数据流从一条隧道穿过。

1.2 代理协议的三个核心责任

任何代理协议都要解决以下三个问题:

身份识别:代理服务器需要知道"是谁在连接"。VLESS 使用 UUID(16 字节的全局唯一标识符)来标识每个客户端。服务器收到连接请求时,先查 UUID 是否在自己的用户列表里。

目标转发:客户端需要告诉代理"我要访问哪里"。代理协议需要把目标地址(域名或 IP)和目标端口编码在协议头中。VLESS 将 Command(指令)、Port(端口)、Address Type(地址类型)和 Address(地址值)依次编码在请求头中。

流量承载:在身份验证和地址信息之后,剩下的就是双向的数据搬运。代理协议需要选择一种方式把原始数据封装起来——可以不加分割地直接流式传输(TCP),也可以按包加上长度前缀(UDP)。

这三个责任的前两个通常只在连接建立时出现一次,只有流量承载是持续进行的。

1.3 一次 TCP 代理的完整流程


客户端              代理服务器              目标服务器
  │                     │                    │
  │── 1. TCP 连接 ─────→│                    │
  │── 2. 协议头(UUID+目标地址)─→│            │
  │                     │── 3. TCP 连接 ────→│
  │                     │── 4. 原始请求 ────→│
  │                     │←─ 5. 原始响应 ────│
  │←─ 6. 原始响应 ──────│                    │
  │                     │                    │

关键观察:第 1-2 步完成后,第 4-6 步就是完全的双向数据拷贝,代理协议本身不再参与数据内容——它只负责搬运。

1.4 ⚠ 为什么协议设计很重要

代理协议的工作看似简单——搬运数据。但搬运过程中,攻击者(和网络审查系统)可以从三个层面观察:

  1. 内容层面:如果数据不加密,任何人都能读懂你在访问什么网站。你的密码、搜索内容都是明文。
  2. 握手特征层面:即使数据加密了,首次连接时协议头的格式、字节长度、字段排列模式也会暴露"这是一个代理协议"。
  3. 流量统计特征层面:连接数、包大小分布、时间间隔模式可以暴露"这不是普通网页浏览"。

不同代理协议在这三个层面的应对策略不同,这也是 VLESS 设计中的关键取舍。

📌 注意

>

代理协议 ≠ VPN。VPN 通常在操作系统层面建立一个虚拟网卡,拦截所有流量。代理协议只处理配置了代理的应用程序的流量。两者在实现上的差异很大,但核心目的是相似的——替数据绕路。

🛠 本章练习

实践:打开你的浏览器开发者工具的网络面板,观察一次 HTTPS 请求的时序。思考——如果通过代理发送这个请求,哪些部分的延迟是代理引入的?

检查点:用自己的话解释"代理协议的核心责任是身份识别、目标转发和流量承载"——你能分别举一个具体的协议设计对应哪个责任吗?


第2章 VLESS 的设计哲学:无状态、轻量、可扩展

本章目标:理解 VLESS 与 VMess 在设计理念上的根本区别,以及这种区别带来的性能和安全影响。

2.1 从 VMess 到 VLESS:一次减法设计

VMess 是 Project V 生态中最早的传输协议。它的设计思路是"协议自己搞定一切"——包括身份认证、加密、抗重放、时间同步校验等。这导致 VMess 有以下负担:

VLESS 的设计者 rprx 在协议文档中写道:"我始终觉得响应验证机制(Response Authentication)没有必要"。这句话捕捉到了 VLESS 的设计精神——把不必要的东西去掉。

2.2 "无状态"的真正含义

VLESS 被描述为"无状态(stateless)"。这不是一个营销词,而是有具体技术含义的:

状态来源VMess 有吗VLESS 有吗为什么
时间同步✅ 必须❌ 不需要VLESS 不依赖时间做校验
动态端口✅ 支持❌ 不需要动态端口通过 ProtoBuf Addons 实现
加密算法协商✅ 响应中包含算法 ID❌ 协议层不加密加密由传输层(TLS)负责
重放保护状态表✅ 需要维护❌ 不需要无内置加密,无重放攻击面
连接用户状态✅ 每次请求需要解析✅ 每次需要验证 UUID验证逻辑极精简,使用 sync.Map 实现

这个对比表说明:VLESS 把大量复杂的状态管理从协议层移走了。服务端不需要维护时钟、不需要管理加密上下文、不需要记录历史指令——只需要验证 UUID。

2.3 去掉内置加密:协议的一次减法

VMess 在协议层进行数据加密——即使你已经在外面包了一层 TLS,VMess 还会再加密一次。这种"双重加密"被 VLESS 团队认为是对 CPU 资源的浪费。

VLESS 的决定很干脆:协议层不做加密。加密完全交给传输层(TLS、REALITY 等)。这样:

以下是一个对比:


VMess 中的数据路径:

  应用数据 → VMess 加密(AES-GCM) → TLS 加密 → TCP 传输

VLESS 中的数据路径:

  应用数据 → TLS 加密 → TCP 传输

在 VMess 路径中,应用数据被加密了两次——一次在协议层,一次在传输层。在 VLESS 中,只有传输层加密一次。

2.4 VMess vs VLESS 详细对比

2.5 ProtoBuf 带来的可扩展性

对比维度VMessVLESS
设计哲学大而全,协议自己搞定一切极简,协议只做最少的事
协议层加密强制(AES-128-GCM / ChaCha20-Poly1305)无(由传输层负责)
系统时间依赖严格,误差 < 90 秒不依赖
UUID 验证通过 alterId 和用户 ID 组合直接验证 16 字节 UUID
协议包头部体积较大(含加密相关的认证数据)极小(仅 Version + UUID + Addons)
ProtoBuf 扩展支持 Addons ProtoBuf
流控(XTLS Vision)不原生支持原生支持
被动探测防御强(通过 Fallback 回落机制)
配置复杂度较高(需要 security、alterId 等)较低(UUID + encryption: none 即可)

VLESS 引入了一个关键的设计元素:Addons(ProtoBuf)。这是一个嵌入在协议头中的可选信息块。

当你想添加额外功能(如流控指令、HTTPS 代理转发)时,不需要修改协议的基本结构——只需要在 Addons 中定义新的 Protobuf 字段。这比 VMess 那种高度耦合的协议结构更灵活。

重要的一点:当 Addons 长度为 0 时,没有任何 ProtoBuf 序列化/反序列化开销。这意味着大多数情况下(不需要流控时),协议头的开销就是一个 0 字节。

⚠ 常见误区

>

"VLESS 更快"是一个常见的笼统说法。实际上 VLESS 本身不做数据加密,所以协议层的 CPU 开销确实比 VMess 低。但传输层 TLS 加密的开销仍在(除非使用更轻量的 REALITY 传输层)。"更快"主要来自去掉了双重加密,而不是 VLESS 协议本身有什么神奇的加速能力。

🛠 本章练习

实践:在一张纸上画出 VMess 和 VLESS 的数据处理管道对比图。标注哪些层负责什么工作(身份认证、加密、地址解析)。

检查点:如果有人说"VLESS 不需要加密是因为它足够安全",你会怎么回应?你的回答应该包含 VLESS 的安全模型说明。


第3章 协议包的字节结构

本章目标:能够读懂 VLESS 协议包的二进制结构,理解每个字段的含义和长度。

3.1 请求头:客户端发给服务器的第一个消息

VLESS 的请求头是一个紧凑的二进制结构。下面是它在网络上的布局:


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+---------------+-------------------------------+
|  Version (1)  |                                                       |
+---------------+                                                       |
|                       UUID (16 字节)                                    |
|                                                                       |
|                                                                       |
+---------------+-------------------------------+-----------------------+
|  AddonsLen (1)|  Addons (M 字节,可选)        |  Command (1)          |
+---------------+-------------------------------+-----------------------+
|  Port (2 BE)  |  AddrType (1) |  Address (S 字节)                  |
+-------------------------------+---------------+-----------------------+
|  Request Data (剩余数据)                                            |
+-----------------------------------------------------------------------+

逐字段解释:

Version(1 字节):协议版本号。在正式版中固定为 0x01(测试版为 0x00,设计上服务器同时支持所有版本)。

UUID(16 字节):用户身份凭证。注意,这是 UUID 的原始二进制字节,不是十六进制字符串表示。服务器收到后直接在内存中对比这 16 个字节。

Addons Length(1 字节):附加信息的字节长度(0-255)。当不需要流控等扩展功能时,这个值为 0x00

Addons(M 字节,可选):Protobuf 编码的 Addons 消息。当前使用的主要字段:


message Addons {
    string Flow = 1;   // 流控模式,如 "xtls-rprx-vision"
    bytes  Seed = 2;   // 填充种子(Padding Seed)
}

Flow 为空时,Addons Length 为 0,没有 ProtoBuf 开销

Command(1 字节):指示连接的用途:

Port(2 字节,大端序):目标服务器的端口号。对于 Mux 和反向代理指令,这个字段省略。

Address Type(1 字节) + Address(S 字节):目标服务器的地址:

类型地址长度格式
IPv4`0x01`4 字节标准 IPv4 地址
域名`0x02`1+N 字节第一个字节是域名长度,后面是域名本身
IPv6`0x03`16 字节标准 IPv6 地址

Request Data:Command 之后的所有剩余数据。对于 TCP 连接,这是一个无帧结构的数据流。对于 UDP,这是一个带 2 字节长度前缀的数据包。

3.2 响应头:服务器的回复


+---------------+---------------+-------------------------------+
|  Version (1)  |  AddonsLen (1)|  Addons (N 字节,可选)         |
+---------------+---------------+-------------------------------+
|  Response Data (剩余数据)                                          |
+--------------------------------------------------------------------+

响应头比请求头更简单——没有 UUID、没有地址信息。Version 回显客户端的版本号,Addons 可用于服务器返回自定义信息。

3.3 数据体编码:不同 Command 的不同策略

协议头之后的数据体编码方式取决于 Command:

TCP(Command = 0x01):无帧结构。VLESS 不对 TCP 数据做任何分割。从认证完成后到连接关闭,所有字节都是原始数据。这就是"流式传输"。

UDP(Command = 0x02):长度前缀封包。每个 UDP 包前加 2 字节(大端序)的长度表示载荷大小。


+---------------+---------------+-------------------------------+
|  Length (2 BE) |  UDP Payload (Length 字节)                    |
+---------------+---------------+-------------------------------+
|  Length (2 BE) |  UDP Payload (Length 字节)                    |
+---------------+---------------+-------------------------------+

Mux(Command = 0x03):使用标准的 Mux 帧格式,允许多个虚拟连接共享一条 TCP 连接。

XUDP:在 Mux 帧内封装 UDP 包,通过魔力端口号(666)和地址(v1.mux.cool)来标识。

3.4 完整的工作流示例

假设客户端要从 192.168.1.2:54321 连接 VLESS 服务器 example.com:443,目标地址是 google.com:80(TCP):


1. 客户端与 example.com:443 建立 TCP 连接(如果有 TLS,握手在这里完成)
2. 客户端发送 VLESS 请求头:
   [0x01]                         ← Version = 1
   [16 字节 UUID 二进制]            ← 用户身份
   [0x00]                         ← Addons Length = 0,无 Addons
   [0x01]                         ← Command = TCP
   [0x00 0x50]                    ← Port = 80(大端序)
   [0x02]                         ← Address Type = 域名
   [0x0A]                         ← 域名长度 = 10
   [0x67 0x6F 0x6F 0x67 0x6C     ← "google.com"
    0x65 0x2E 0x63 0x6F 0x6D]

3. 服务器响应:
   [0x01]                         ← Version = 1
   [0x00]                         ← Addons Length = 0

4. 双向数据拷贝开始——HTTP 请求和响应在多路连接中对等传输

📌 注意

>

VLESS 的 UUID 在网络上是以原始 16 字节传输的,不是常见的 UUID 字符串格式(如 550e8400-e29b-41d4-a716-446655440000 是 36 字节)。这意味着包头的 UUID 部分正好固定为 16 字节,解析起来非常高效(直接内存对比)。

⚠ 常见陷阱:Addons 的误解

一个容易混淆的点:Addons Length = 0 不意味着"没有 Addons",而是意味着"Addons 长度为零"。在二进制编码中,一个字节的 0x00 表示"没有后续的 Addons 数据"。这意味着当你使用 Vision 流控时,协议头只比平时多出几个字节的 ProtoBuf 编码(流控模式名称字符串),开销极小。

🛠 本章练习

实践:用你熟悉的语言写一个函数,将 ("example.com", 443, "TCP") 编码为 VLESS 请求头(只需要组装字节序列,不需要实际发送)。

检查点:如果目标地址是 2607:f8b0:4005:80b::200e:443(IPv6,Google),请求头中 Address Type 和 Address 字段应该怎么写?


第4章 传输层安全:为什么 VLESS 离不开 TLS

本章目标:理解 VLESS 的安全模型——为什么协议层不加密不等于不安全,以及传输层安全的选择与配置。

4.1 VLESS 的安全模型

这是理解 VLESS 最重要的一个概念:

VLESS 协议本身不提供加密、不提供完整性保护、不提供抗重放攻击。

这三个"不"听起来很吓人,但 VLESS 的设计假设是:这些工作应该由传输层来完成。就像 HTTP 不加密数据、把加密交给 HTTPS(TLS)一样,VLESS 把加密完全交给它下面的传输层。

VLESS 的安全依赖链:


应用安全(端到端加密,如 HTTPS)
    ↕
传输层安全(TLS / REALITY / VLESS Encryption)
    ↕
VLESS 协议(纯转发,无加密)
    ↕
TCP / UDP 传输

每一层负责不同的安全工作。VLESS 只做最少的协议控制面,加密全部交给下一层。

4.2 TLS 的开销与权衡

标准 TLS 1.3 连接的开销:

如果 VLESS 不配合 TLS,而是直接使用 security: none 传输层:


[VLESS 协议头 + 应用数据] → TCP 明文传输

这意味着任何在中间网络节点上抓包的人都能立刻看到:

官方文档明确警告:VLESS 必须配合外层传输安全层使用,除非对端是内网地址且连接本身可信。

4.3 REALITY:当 TLS 也需要伪装

REALITY 是 Xray 生态中替代标准 TLS 的传输层方案。它解决了一个标准 TLS 做不到的问题:让代理连接的 TLS 握手看起来和普通 HTTPS 网站一模一样。

标准 TLS 的问题:如果你的代理服务器自己签发了一个证书(甚至是 Let's Encrypt 签发的),这个证书和 example.com 的证书是不一样的。网络审查系统可以通过 TLS 握手中的证书信息猜到"这不是普通的 HTTPS 浏览,而是一个代理连接"。

REALITY 的做法:

  1. 代理服务器不提供自己的证书——它借用目标网站(如 cloudflare.com)的证书
  2. 代理客户端在 TLS 握手中使用 uTLS 库模拟浏览器的 TLS 指纹(如 Chrome 的指纹)
  3. 审查系统看到的是:客户端在和 cloudflare.com 做 TLS 握手——和正常的 HTTPS 浏览完全一样

对于审查系统来说,一个 REALITY 连接的流量和普通 HTTPS 流量在握手层面没有区别。

📌 注意

>

REALITY 不要求你拥有 cloudflare.com 的私钥——它只是把验证证书的责任转嫁给客户端,让客户端拿着 X25519 公钥去验证服务器确实是你部署的代理(而不是真的 cloudflare.com)。这个机制结合了 TLS 的证书链验证和自定义密钥验证。

4.4 ML-KEM-768 后量子加密

VLESS 在传输层安全之外,还支持一个可选的后量子加密层:ML-KEM-768(前身为 Kyber,NIST 标准化的后量子密钥封装机制)。

ML-KEM-768 提供的是"先窃取,后解密"防护——即使攻击者现在记录了你的所有加密流量,等到未来量子计算机足够强大时,也无法解密这些历史数据。

配置方式:在服务端设置 decryption 字段,在客户端设置 encryption 字段,格式为多段配置字符串,如 mlkem768x25519plus.native.600s.100-111-1111...

4.5 VLESS Encryption:协议层的可选加密

除了依赖传输层,VLESS 从 Xray-core v1.8.0+ 开始也支持协议层自身的加密机制(称为 VLESS Encryption)。这是一个可选项,不是默认开启的。

它的设计哲学和 VMess 不同:

这允许用户在以下场景中灵活选择:

⚠ 常见误区

场景推荐方案理由
公共互联网REALITY / TLS + VLESS传输层加密 + 指纹伪装
内网 / 可信链路security: none无加密开销
后量子安全需求TLS + ML-KEM-768抵御未来量子计算机攻击
需要双重加密TLS + VLESS Encryption额外保护层(牺牲性能)

误区:"VLESS 比 Shadowsocks 安全,因为 VLESS 更新、设计更现代。"

事实:Shadowsocks 协议层自带加密(AEAD 加密)。VLESS 协议层不加密。如果你把 VLESS 配置为 security: none 且不启用 VLESS Encryption,它的数据是明文的——比 Shadowsocks 的默认配置更不安全。

误区:"配置了 TLS 就万无一失。"

事实:TLS 保护的是数据在传输过程中的机密性和完整性,但不保护元数据。攻击者仍然可以知道你在连接哪个 IP、连接时长、数据流量大小等。这些元数据本身就可能暴露你的行为模式。

🛠 本章练习

实践:用 Wireshark 抓取一个标准 TLS 1.3 连接,观察 Client Hello 中的 Server Name Indication(SNI)字段。思考——如果代理不隐藏 SNI,审查系统能不能知道你在访问哪个网站?

检查点:用自己的话解释"VLESS 把加密责任移交给传输层"这一设计决策的利与弊。


第5章 XTLS Vision 流控机制:从双重加密到零拷贝

本章目标:理解 XTLS Vision 如何通过识别 TLS 状态来避免双重加密,以及它的实现原理和局限性。

5.1 TLS-in-TLS 的性能陷阱

当 VLESS 配合 TLS 使用时,一个常见的配置是:


[应用数据] → [TLS 加密] → [VLESS 封装] → [TLS 加密] → [TCP]

注意这里的两层 TLS:应用自己可能已经在使用 HTTPS(外层加密),代理传输过程中又有第二层 TLS(内层加密)。这被称为 TLS-in-TLS

TLS-in-TLS 的问题:

  1. 双重加密操作:服务器需要先解密传输层的 TLS,然后通过 VLESS 转发数据时,需要再加密一次(如果目标也是 HTTPS)。
  2. 特征放大:两次 TLS 握手导致包大小分布、时序特征和普通 HTTPS 明显不同,更容易被检测。
  3. 延迟叠加:两次握手延迟(2-RTT vs 1-RTT)让连接建立更慢。
  4. 5.2 Vision 流量状态机

XTLS Vision 的核心思想:如果 VLESS 隧道内传输的已经是 TLS 加密数据(如 HTTPS),为什么不用一种方式直接复制这些加密数据,而不是解密再加密?

Vision 引入了一个流量状态机(Traffic State Machine)


上游(客户端 → 服务器)方向:

初始阶段 → 第一次 TLS 握手开始 → 填充阶段(Padding Phase)
    └→ 检测到内层 TLS 1.3 握手完成 → 直接原始拷贝模式(Direct Raw Copy)

下游(服务器 → 客户端)方向:

初始阶段 → 填充阶段
    └→ 检测到内层 TLS 1.3 握手完成 → 直接原始拷贝模式

状态切换的关键时刻:当 Vision 检测到客户端发的内层 TLS 数据已经完成握手(进入 Application Data 阶段),就不再对数据做任何改写——直接把加密后的内层 TLS 数据作为 VLESS 的数据体发送。

效果:


传统 VLESS(无 Vision):

  应用 HTTPS 请求 → TLS 加密(内层) → VLESS 写入
  → 传输层 TLS 再次加密(外层) → TCP 发送

Vision 模式:

  应用 HTTPS 请求 → TLS 加密(内层)
  → VLESS Vision 检测到"握手已完成,数据已加密"
  → 直接复制加密数据 → TCP 发送(外层 TLS 之下)

后者在握手完成后没有二次加密操作,所以 CPU 开销接近零。

5.3 unsafe.Pointer:Go 语言特有的实现技巧

Vision 的实现用到了一个 Go 语言的特殊技巧。在 Go 的标准库 crypto/tls 中,TLS 连接的内部状态(握手是否完成、输入缓冲区的数据)是包内私有字段,正常情况下不能被外部代码读取。

Vision 通过 reflectunsafe.Pointer 直接读取了 TLS 连接对象的内存偏移量:


t = reflect.TypeOf(tlsConn.Conn).Elem()
p = uintptr(unsafe.Pointer(tlsConn.Conn))
i, _ := t.FieldByName("input")    // *bytes.Reader
r, _ := t.FieldByName("rawInput") // *bytes.Buffer
input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset))
rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset))

这个代码直接访问了 Go 标准库的内部结构体字段,没有使用任何公开 API。这意味着:

除了零拷贝数据,Vision 还处理一个更隐蔽的问题:流量时序特征

即使是加密的流量,数据包的大小分布和时间间隔也可以作为识别特征。例如,浏览器访问 Google 首页时加载的文件大小、数量和顺序构成了一个特定的"流量指纹"(traffic fingerprint)。

Vision 的填充机制允许在 TLS 握手阶段插入随机长度的填充数据,以及随机时长的延迟。这可以让流量的大小和时序分布更加接近普通 HTTPS。

填充配置示例(在 VLESS Encryption / Addons 中配置):


100-111-1111     ← 100% 概率发送 111-1111 字节的填充
75-0-111         ← 75% 概率等待 0-111 毫秒
50-0-3333        ← 50% 概率等待 0-3333 毫秒

这种"随机填充 + 随机延迟"的组合让基于机器学习(ML)的流量分类更难做出准确判断。

5.5 不同流控模式的适用场景

⚠ 重要限制

模式原理性能提升适用场景
无流控(空)标准代理转发,数据全部经过 VLESS 处理不需要额外优化或协议不兼容时
xtls-rprx-visionTLS 状态机检测 + 直接原始拷贝显著(CPU 降低)TCP + TLS / REALITY,传输的是 TLS 1.3 数据
其他流控模式不同层次的调度器(暂未发布)未来扩展

Vision 的零拷贝优化只对 TLS 1.3 流量有效。如果应用程序发送的是未加密的 HTTP 流量,Vision 也会正常转发,但没有性能提升——因为数据本身不是 TLS 结构,状态机检测不到"握手完成"的切换点。

🛠 本章练习

实践:为什么 Vision 需要在握手阶段插入填充(Padding)?试着从审查者的角度想——如果一次 TLS 握手后数据包的发送模式是高度规则的,审查系统可以从中推断什么?

检查点:Vision 的"直接原始拷贝"模式在什么条件下才会激活?描述状态机切换的条件。


第6章 Fallback 回落机制

本章目标:理解 VLESS 如何通过 Fallback 机制对抗主动探测,以及多级回落路由的设计。

6.1 对抗主动探测

网络审查系统经常使用主动探测(Active Probing):审查节点主动向疑似代理服务器发送连接请求,尝试判断这个端口是否在运行代理服务。

如果代理服务器对每一个连接请求都积极回应(即使是错误的 UUID),审查系统就能判断"这个 IP 上确实有代理服务在运行"。

Fallback 的解决思路:当客户端发送的请求不合法时,把连接转给一个合法的 Web 服务器。


主动探测请求 → VLESS 端口 443
   ↓ 检查 UUID
   ↓ UUID 不匹配 + Fallback 已配置
   ↓ 把连接(包括已经读取的探测数据)转发给 Nginx 80 端口
   ↓ Nginx 返回一个正常的 HTTP 响应
   ↓ 探测者看到的是 Nginx 的 404 页面——和普通 Web 服务器一模一样

对于外部观察者,端口 443 上运行的是一个标准 Web 服务器(Nginx、Caddy)——没有迹象表明 VLESS 也在那里工作。

6.2 多级回落路由

Fallback 不是简单的"如果 UUID 不对就转发到 80 端口"。它是一个多级匹配系统:


连接的 TLS SNI 是什么?
  ├─ "example.com" → ALPN 是什么?
  │    ├─ "h2" → HTTP 路径是什么?
  │    │    ├─ "/api" → 转发到 127.0.0.1:8081
  │    │    └─ 其他 → 转发到 127.0.0.1:8080(Nginx)
  │    └─ "http/1.1" → 转发到 127.0.0.1:8080
  ├─ "cdn.example.com" → 转发到 127.0.0.1:9090
  └─ 无 SNI / 未知域名 → 转发到 127.0.0.1:80

三级路由维度:

  1. SNI(Server Name Indication):TLS 握手中的域名。可以让多个域名共享同一个 IP 和端口。
  2. ALPN(Application-Layer Protocol Negotiation):TLS 协商的应用层协议(h2 代表 HTTP/2,http/1.1 代表 HTTP/1.1)。
  3. HTTP Path:HTTP 请求的路径(仅在 ALPN 为 h2http/1.1 且初始数据能解析出 HTTP 请求行时可用)。
  4. 6.3 配置示例与工作流程

以下是一个带 Fallback 的 VLESS 入站配置:


{
  "inbounds": [
    {
      "port": 443,
      "protocol": "vless",
      "settings": {
        "decryption": "none",
        "fallbacks": [
          {"dest": 80},                                // 默认回落
          {"alpn": "h2", "dest": 8080, "xver": 2}     // HTTP/2 回落
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "tls",
        "tlsSettings": {
          "certificates": [...]
        }
      }
    }
  ]
}

工作流程:

  1. 客户端向端口 443 发起 TLS 连接
  2. TLS 握手完成后,VLESS 读取第一个请求包的字节
  3. 解析 Version 和 UUID
  4. 如果 UUID 匹配 → 正常 VLESS 处理
  5. 如果 UUID 不匹配且 Fallback 已配置 → 根据 SNI/ALPN/Path 查找回退目标
  6. 把连接(包括已读取的字节)原样转发给回退目标
  7. 回退目标的响应直接转发回客户端
  8. 6.4 Fallback 的风险边界

Fallback 不是银弹。以下情况需要特别注意:

目标地址风险:如果回退目标是一个 CDN 节点(如 cloudflare.com),你的服务器实际上变成了这个 CDN 的端口转发器。攻击者扫描到你的服务器后,所有发往 CDN 的请求都会经过你的服务器——这可能让你承担不必要的流量和法律风险。

速率限制:从 Xray-core 1.8.0 开始,REALITY 引入了可选的回退连接速率限制(limitFallbackUpload / limitFallbackDownload),可以在一定数据量之后对回退流量进行限速。

最佳实践:回退目标应该和你部署在同一个 ASN(同一家托管商)的站点,而不是使用免费 CDN。

⚠ 常见陷阱

陷阱:fallbacks 配置为 [{"dest": 80}] 就以为万无一失。

事实:如果 dest: 80 上没有任何 Web 服务在监听(端口未打开),Fallback 连接会失败,客户端收到连接拒绝。需要确保回退目标端口上确实有正常的 HTTP 服务器在运行。

🛠 本章练习

实践:假设你在服务器上有三个网站:blog.example.comshop.example.netapi.example.com。设计一个 Fallback 配置,让未认证的请求回落到一个自己熟悉的 Web 服务器页面。

检查点:主动探测(Active Probing)和被动观察(Passive Observation)有什么区别?Fallback 能防御哪一种?


第7章 UDP 与 FullCone NAT

本章目标:理解 VLESS 如何处理 UDP 流量,以及为什么 UDP 支持对某些应用(如语音通话、在线游戏)如此重要。

7.1 UDP 的挑战

TCP 是有连接的协议——你建立连接、发送数据、接收数据、关闭连接,一切都井然有序。UDP 是无连接的——你发送一个数据包,不保证送达,不保证顺序,不需要连接状态。

这对代理协议提出了挑战:

VLESS 为 UDP 提供了两种承载方式:长度前缀编码XUDP

7.2 长度前缀编码(非 XUDP)

最基本的 UDP 承载方式是在 VLESS 数据体中嵌入带长度前缀的 UDP 包:


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------------------------------+-------------------------------+
|  Length (2 字节, 大端序)       |  UDP Payload (Length 字节)     |
+-------------------------------+-------------------------------+

每个 UDP 包前面加 2 字节的长度前缀。接收端先读 2 字节知道载荷大小,然后读指定字节数的载荷。

局限性:这种方式的 UDP NAT 行为取决于 VLESS 连接本身使用的传输层。如果传输层不支持 FullCone NAT,UDP 应用(如在线游戏)可能无法正常工作。

7.3 XUDP:在 Mux 中封装 UDP

XUDP 是 VLESS 解决 FullCone NAT 问题的方法。它的工作原理:

  1. 当 VLESS 检测到需要 UDP FullCone NAT 时,把 UDP 通信转换成 Mux 多路复用连接
  2. 每个 UDP 包在 Mux 连接中被标记为一个独立的虚拟连接
  3. Mux 连接本身是 TCP(有连接的),所以 NAT 映射可以保持

实现方式:在客户端编码时,将 Command 设置为 Mux(0x03),目标地址设置为魔力值 v1.mux.cool:666。服务器端识别到这个魔力值后:


func isMuxAndNotXUDP(request, first) bool {
    if request.Command != protocol.RequestCommandMux {
        return false
    }
    // XUDP 检测:session ID = 0, network type = UDP (2)
    firstBytes := first.Bytes()
    return !(firstBytes[2] == 0 && firstBytes[3] == 0 && firstBytes[6] == 2)
}

服务器读到 session ID = 0 且 network type = UDP 时,就知道这个 Mux 帧实际承载的是 UDP 数据——并按 UDP 方式处理(而不是多路复用的 TCP 流)。

7.4 FullCone NAT 的实现原理

FullCone NAT(也称为"一对一 NAT")是指:当一个内部地址(客户端)向外部发送 UDP 包并创建了 NAT 映射后,任何外部地址都可以通过这个映射向内部地址发送 UDP 包

代理协议中的 FullCone NAT 效果:


客户端 → 代理服务器 → 游戏服务器(发送 UDP 包)
游戏服务器 → 代理服务器 → 客户端(响应 UDP 包)
                    ↕
代理服务器保持一个 UDP 会话表:
  (客户端IP:端口 ↔ 代理服务器:端口 ↔ 目标IP:端口)

对于需要双向 UDP 通信的应用(如语音通话、在线游戏、BT 下载),FullCone NAT 是必要的。XUDP 通过在 Mux 中封装 UDP 包,让 VLESS 能够维持这个会话表。

⚠ 常见问题

问题:为什么有些 UDP 应用在 VLESS 代理下不好用?

可能原因

  1. VLESS 未配置为 XUDP 模式——使用基本的长度前缀方式
  2. 传输层(TCP/TLS)本身的 UDP 映射限制
  3. 代理服务器的防火墙规则阻止了 UDP 回包
  4. Mux 连接的超时设置过短,导致 UDP 会话在两次包之间过期
  5. 🛠 本章练习

实践:查阅你的 VLESS 客户端的配置,看是否有 muxxudp 相关的选项。尝试开启和关闭后测试在线游戏的延迟变化。

检查点:用两句话解释为什么 UDP FullCone NAT 需要 Mux,而不是直接在 UDP 上传输。


第8章 安全误区与风险清单

本章目标:识别 VLESS 使用者最常见的五个安全误区,并为每个误区提供正确的理解和改进方法。

8.1 误区一:VLESS 自带加密

误解:因为 VLESS 是"代理协议",很多人认为它像 VPN 一样自动加密所有数据。

事实:VLESS 协议层不提供加密。数据是否加密完全取决于你选择的传输层。裸运行 security: none 的 VLESS 线上是明文传输。

检查方法

  1. 查看 VLESS 配置文件中的 streamSettings.security 字段
  2. 如果是 "none" → 无传输层加密
  3. 如果是 "tls""reality" → 有传输层加密
  4. 如果启用了 VLESS Encryption(decryption / encryption 字段非 "none")→ 有协议层加密

正确做法:除非是内网测试环境,否则永远确保 VLESS 运行在 TLS 或 REALITY 之上。

8.2 误区二:UUID 是唯一的防护

误解:"UUID 总共有 128 位,不可能被猜到,所以只靠 UUID 就够了。"

事实:UUID 是身份凭证,不是加密密钥。它防止未经授权的用户连接你的代理服务器,但不保护流量内容

在不加密的 VLESS 连接中:

正确做法:把 UUID 看作用户名/密码,不是 SSL 证书。它控制"谁可以用",不控制"传输是否安全"。

8.3 未认证 SOCKS5 漏洞

这是 VLESS 生态中的一个已知安全漏洞(来源:安全研究人员 use3r-riddl3r)。

问题:许多 VLESS 客户端的 SOCKS5 入站代理默认不要求认证。这意味着:

  1. 你在电脑上启动了 VLESS 客户端,它在本机创建了一个 SOCKS5 代理(通常监听 127.0.0.1:1080
  2. SOCKS5 端口不需要用户名/密码
  3. 如果你电脑上的任何一个程序(或同网络的其他设备)能够访问 127.0.0.1:1080,它就可以通过你的代理访问外部网络
  4. 恶意软件、浏览器扩展、甚至同一网络中的攻击者都可以利用这个缺口

影响范围:几乎所有主流 VLESS/xray 客户端在默认配置下都存在这个问题。

缓解措施

  1. 确认 SOCKS5 监听地址为 127.0.0.1 而不是 0.0.0.0(仅本机访问)
  2. 开启 SOCKS5 用户名/密码认证
  3. 或使用 HTTP 代理入站(而不是 SOCKS5),因为 HTTP 代理有标准的认证机制
  4. 在防火墙规则中限制只能本地访问代理端口
  5. 8.4 配置错误导致的风险

以下是配置中最常见的问题:

8.5 安全配置检查清单

配置错误风险建议
`security: "none"` + 公网使用数据明文传输必须配置 TLS / REALITY
Fallback 目标指向 CDN服务器成为 CDN 转发器回退到本地 Web 服务器
UUID 使用弱随机性(如统一的测试 UUID)他人可以连接到你的服务器使用 `xray uuid` 生成随机 UUID
`decryption` 留空VLESS 报错或降级显式设置为 `"none"`
Mux 超时设置过长资源浪费根据使用场景调整(30-60 秒通常合适)
日志级别设为 `debug`泄露协议信息和目标地址生产环境使用 `warning` 或 `error`

在部署或使用 VLESS 之前,逐项检查以下条目:

实践:打开你的 VLESS 配置文件,对照上面的安全检查清单逐项检查。把你发现的问题记录下来。

检查点:如果你的朋友说"我配置了 VLESS,安全上没问题,因为我设置了 UUID",你会从哪几个角度向他解释为什么仅靠 UUID 是不够的?


总练习:拆解一个 VLESS 协议包

场景

你在网络抓包中捕获了以下十六进制数据(这是一个 VLESS 请求头的前面几个字节):


01 55 0e 84 00 e2 9b 41 d4 a7 16 44 66 55 44 00 00 00 01 00 50 02 0b 67 6f 6f 67 6c 65 2e 63 6f 6d

任务

逐字节/逐字段地解析这个请求头:

偏移字节数字段值(十六进制)解析结果
01Version`01`版本 1(正式版)
1-1616UUID`55 0e 84 00 e2 9b 41 d4 a7 16 44 66 55 44 00 00`UUID: 550e8400-e29b-41d4-a716-446655440000
171Addons Length`00`无 Addons
181Command`01`TCP 连接
19-202Port`00 50`端口 80
211Address Type`02`域名
221域名长度`0b` (11)域名有 11 个字符
23-3311域名`67 6f 6f 67 6c 65 2e 63 6f 6d`"google.com"

结果:这是一个目标为 google.com:80(TCP)的 VLESS 请求。

自检问题

  1. 这个连接的 Addons 字段中是否有流控配置?为什么?
  2. 如果 Command 是 0x03,协议头中是否还有 Port 和 Address 字段?
  3. 如果 Address Type 是 0x01(IPv4),Address 字段应该是多少字节?写出其中 203.0.113.5 的十六进制表示。
  4. 为什么 Addons Length = 0 时不需要 ProtoBuf 的开销?
  5. 在完整协议栈中,这个 VLESS 请求头之上和之下的加密层分别是什么?

继续学习

学完这篇教程后,你可以从以下方向继续深入:

Xray-core 源码阅读:从 proxy/vless/encoding/encoding.go 开始,跟踪 VLESS 请求的编解码过程。这是最直接的理解方式。

REALITY 传输层:深入 REALITY 的实现和 uTLS 库的指纹模拟机制。这是 VLESS 生态中最复杂的部分之一。

uTLS 库:了解 Go 语言如何模拟浏览器的 TLS 指纹(TLS Fingerprint)——这是 REALITY 的底层依赖。

代理协议的流量分析检测:研究基于机器学习的代理协议检测方法(如包大小分布、时序分析),理解数据安全中"检测与对抗"的动态关系。

参考资料