RFC: The Hysteria 2 Protocol Specification Category: Informational Date: April 2026
Hysteria 2 协议规范 (The Hysteria 2 Protocol Specification)¶
摘要 (Abstract)¶
Hysteria 是一款基于 QUIC 的 TCP 和 UDP 代理协议,专为速度、安全性和抗审查性而设计。本文档描述了从 2.0.0 版本(内部有时称为 "v4" 协议)开始 Hysteria 所使用的协议。它详细说明了底层传输格式、用于身份验证的 HTTP/3 伪装机制、代理请求多路复用、拥塞控制信令以及可选的混淆层。
1. 简介 (Introduction)¶
Hysteria 协议利用 QUIC 提供安全且多路复用的代理连接。通过将其初始身份验证伪装成标准的 HTTP/3 流量,它旨在挫败主动探测和深度包检测 (DPI) 启发式算法。认证通过后,该协议在建立的 QUIC 连接上多路复用 TCP 流和数据报 (UDP)。
2. 约定和术语 (Conventions and Terminology)¶
本文档中的关键词“必须”(MUST)、“绝对不能”(MUST NOT)、“要求”(REQUIRED)、“将”(SHALL)、“绝对不将”(SHALL NOT)、“应该”(SHOULD)、“不应该”(SHOULD NOT)、“建议”(RECOMMENDED)、“可以”(MAY) 和“可选”(OPTIONAL) 需按照 [RFC 2119] 中的描述进行解释。
3. 底层协议与传输格式 (Underlying Protocol & Wire Format)¶
Hysteria 协议必须建立在标准的 QUIC 传输协议 [RFC 9000] 之上,并且必须支持不可靠数据报扩展 [RFC 9221]。
- 字节序 (Byte Order): 所有多字节数字均使用大端序 (Big Endian) 格式。
- 变长整数 (Variable-Length Integers): 所有变长整数("varints")的编码和解码均按照 QUIC [RFC 9000] 第 16 节的定义进行。
4. 认证与 HTTP/3 伪装 (Authentication & HTTP/3 Masquerading)¶
Hysteria 协议的一个核心特性是,对于未经身份验证的观察者(无论中间盒还是主动探测器),Hysteria 代理服务器的行为完全像一个标准的 HTTP/3 Web 服务器。其加密流量与正常的 HTTP/3 流量无法区分。
因此,Hysteria 服务端必须实现 HTTP/3 服务端(如 [RFC 9114] 所定义),并像常规 Web 服务器一样处理标准的 HTTP 请求。为了防止模式检测,服务端应该托管实际内容或作为其他 Web 服务的反向代理。
4.1. 客户端认证请求¶
建立 QUIC 连接后,实际的 Hysteria 客户端必须向服务端发送特定的 HTTP/3 请求:
:method: POST
:path: /auth
:host: hysteria
Hysteria-Auth: [string]
Hysteria-CC-RX: [uint]
Hysteria-Padding: [string]
Hysteria-Auth: 认证凭据。Hysteria-CC-RX: 客户端的最大接收速率(以字节/秒为单位)。值0表示速率未知。Hysteria-Padding: 用于混淆的随机生成的变长填充字符串。
4.2. 服务端认证响应¶
Hysteria 服务端必须识别此特殊请求。它不能提供内容或将其转发到上游,而是必须尝试使用提供的凭据验证客户端。
如果认证成功,服务端必须发送以下 HTTP/3 响应:
:status: 233 HyOK
Hysteria-UDP: [true/false]
Hysteria-CC-RX: [uint/"auto"]
Hysteria-Padding: [string]
:status: 必须精确为233。Hysteria-UDP: 指示服务端是否支持 UDP 中继。Hysteria-CC-RX: 服务端的最大接收速率(以字节/秒为单位)。值0表示无限制;字面量字符串"auto"表示服务端拒绝提供速率值,并要求客户端自主使用拥塞控制来确定速率。Hysteria-Padding: 随机生成的变长填充字符串。
注:Hysteria-Padding 标头是可选的 (OPTIONAL),其唯一目的是混淆请求/响应的大小,接收端应忽略此标头。
4.2.1. 认证失败¶
如果认证失败,服务端的行为必须与不理解该请求的标准 Web 服务器完全一致(例如,返回 404 或 403 状态),或将请求转发到上游站点并返回其响应。
客户端必须检查 :status 状态码。如果状态码不是 233,客户端必须视为认证失败,并必须立即断开与服务端的连接。
4.2.2. 认证后¶
在(且仅在)客户端成功认证后,服务端必须转变 QUIC 连接状态,将其视为 Hysteria 代理连接,并开始处理代理请求。
5. 代理请求 (Proxy Requests)¶
5.1. TCP 代理¶
对于每个新的 TCP 连接,客户端必须打开一个新的 QUIC 双向流并发送 TCPRequest 消息。
5.1.1. TCPRequest 消息¶
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x401 (varint) | 地址长度 (varint) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 地址字符串 (host:port) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 填充长度 (varint) | 随机填充 ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
5.1.2. TCPResponse 消息¶
服务端必须在同一流上回复 TCPResponse 消息:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Status | 消息长度 (varint) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 消息字符串 ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 填充长度 (varint) | 随机填充 ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- Status: 1 字节 (uint8)。
0x00= OK,0x01= Error。
如果 Status 为 0x00 (OK),服务端必须立即在客户端 QUIC 流和指定的 TCP 目标之间开始全双工的数据转发,直到任一方关闭连接。如果 Status 为 0x01 (Error),服务端必须关闭该 QUIC 流。
5.2. UDP 代理¶
UDP 数据包必须被封装在 UDPMessage 中,并通过 QUIC 不可靠数据报通道 (Unreliable Datagram) 发送(双向均如此)。如果服务端不支持 UDP 中继(在认证期间指示),它应该静默丢弃从客户端接收到的所有 UDP 消息。
5.2.1. UDPMessage 格式¶
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 会话 ID (Session ID) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据包 ID | 分片 ID | 分片总数 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 地址长度 (varint) | 地址字符串 (host:port) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 载荷 (Payload) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 会话 ID (Session ID): 4 字节 (uint32)。客户端必须为每个逻辑 UDP 会话使用唯一的会话 ID。服务端应该为每个会话 ID 分配一个唯一的出站 UDP 端口(除非使用对称 NAT 等机制)。
- 数据包 ID (Packet ID): 2 字节 (uint16)。被分片数据包的标识符。
- 分片 ID (Fragment ID): 1 字节 (uint8)。以 0 为起始索引的分片编号。
- 分片总数 (Fragment Count): 1 字节 (uint8)。分片的总数量。
5.2.2. UDP 会话生命周期¶
没有显式机制来关闭 UDP 会话。客户端可以无限期地保留并重用会话 ID。服务端应该实现超时机制,以释放并重新分配与不活跃会话 ID 关联的端口。如果服务端收到未识别或已过期的会话 ID 的 UDP 消息,必须将其视为新会话并分配新的出站端口。
5.2.3. 分片 (Fragmentation)¶
由于 QUIC 数据报大小的限制,大型 UDP 数据包必须被分片或丢弃。
- 对于未分片的数据包,
分片总数必须设置为1。此时,数据包 ID和分片 ID的值无关紧要。 - 对于分片的数据包,所有分片必须携带相同的
数据包 ID。分片 ID指示当前的索引。两端必须缓冲分片,并等待数据包的所有部分到达后再进行处理。如果丢失任何一个分片,整个数据包必须被丢弃。
6. 拥塞控制信令 (Congestion Control Signaling)¶
Hysteria 允许在认证期间显式地交换 Tx/Rx(上传/下载)速率信令,以优化拥塞控制。
- 客户端通过
Hysteria-CC-RX标头发送其 Rx 速率。 - 服务端通过响应中的
Hysteria-CC-RX标头发送其 Rx 速率。
特殊信令值:
1. 客户端发送 0: 客户端不知晓其 Rx 限制。服务端必须依赖标准的拥塞控制算法(例如 BBR、Cubic)来管理其传输速率。
2. 服务端发送 0: 服务端没有带宽限制。客户端可以以任意所需的速率传输。
3. 服务端发送 "auto": 服务端拒绝指定限制。客户端必须使用标准的拥塞控制算法来管理其传输速率。
7. "Salamander" 混淆层 (可选)¶
该协议定义了一个代号为 "Salamander" (蝾螈) 的可选混淆层。启用后,Salamander 将封装所有底层的 QUIC 数据包。
7.1. 封装格式¶
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Salt (8 字节) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 已混淆的载荷 (Obfuscated Payload) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
7.2. 混淆算法¶
对于发送的每个 QUIC 数据包:
1. 发送方必须生成一个随机的 8 字节 Salt。
2. 发送方必须计算一个 32 字节的哈希值。该哈希值是对预共享密钥 (PSK) 与生成的 Salt 拼接后的结果使用 BLAKE2b-256 算法计算得出的:
hash = BLAKE2b-256(PSK + Salt)
3. 发送方必须使用计算出的哈希值,通过循环异或 (XOR) 的方式来混淆原始的 QUIC 载荷:
7.3. 去混淆算法¶
对于接收到的每个数据包:
1. 接收方提取出 8 字节的 Salt 和 已混淆的载荷。
2. 接收方必须使用已知的 PSK 和提取出的 Salt,通过相同的 BLAKE2b-256 算法计算出 32 字节的哈希值。
3. 接收方必须使用相同的循环异或操作对载荷进行去混淆。
4. 如果解出的载荷不是一个有效的 QUIC 数据包,则必须静默丢弃整个数据包。