跳转至

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 载荷:

for i in range(0, len(payload)):
    obfuscated_payload[i] = payload[i] ^ hash[i % 32]

7.3. 去混淆算法

对于接收到的每个数据包: 1. 接收方提取出 8 字节的 Salt已混淆的载荷。 2. 接收方必须使用已知的 PSK 和提取出的 Salt,通过相同的 BLAKE2b-256 算法计算出 32 字节的哈希值。 3. 接收方必须使用相同的循环异或操作对载荷进行去混淆。 4. 如果解出的载荷不是一个有效的 QUIC 数据包,则必须静默丢弃整个数据包。