Hysteria2
Hysteria2 是一种基于 QUIC 的代理协议,专为高吞吐量、高丢包率的网络设计。Xray-core 使用 apernet/quic-go 库(一个修改版的 QUIC 实现)集成该协议,具有自定义拥塞控制("Brutal")和 QUIC 数据报支持以实现 UDP 代理。
概述
- 方向:入站 + 出站
- 传输:QUIC(基于 UDP)
- 加密:QUIC TLS 1.3(由传输层处理)
- 认证:基于密码(由传输层处理)
- TCP 代理:通过 QUIC 流
- UDP 代理:通过 QUIC 数据报(不可靠),支持分片
- 拥塞控制:Brutal(固定带宽)或标准 QUIC 拥塞控制
架构
Xray-core 中的 Hysteria2 分为两层:
- 传输层(
transport/internet/hysteria/):处理 QUIC 连接建立、TLS、认证、拥塞控制 - 代理层(
proxy/hysteria/):处理应用层协议(TCP 请求/响应帧封装、UDP 消息格式)
graph TD
subgraph "代理层 (proxy/hysteria/)"
A[客户端处理器] --> B[TCP 请求/响应]
A --> C[UDP 消息帧封装]
D[服务端处理器] --> B
D --> C
end
subgraph "传输层 (transport/internet/hysteria/)"
E[QUIC 连接] --> F[Brutal 拥塞控制]
E --> G[TLS 1.3]
E --> H[认证验证]
end
B --> E
C --> E线路格式
TCP 请求
TCP 代理使用 QUIC 流。每个 TCP 连接是一个新的 QUIC 流,请求格式如下:
+-------------------+-------------------+--------------------+-------------------+
| Address Length | Address | Padding Length | Padding |
| QUIC varint | bytes | QUIC varint | bytes |
+-------------------+-------------------+--------------------+-------------------+源码:proxy/hysteria/protocol.go:28-62
| 字段 | 编码 | 约束 |
|---|---|---|
| Address Length | QUIC 可变长整数 | 1 到 2048 |
| Address | UTF-8 字符串(如 "example.com:443") | host:port 格式 |
| Padding Length | QUIC 可变长整数 | 0 到 4096 |
| Padding | 随机字节 | 读取时丢弃 |
func WriteTCPRequest(w io.Writer, addr string) error {
padding := tcpRequestPadding.String() // 64-512 random bytes
// varint(addrLen) + addr + varint(paddingLen) + padding
}源码:proxy/hysteria/protocol.go:64-77
TCP 响应
+--------+-------------------+-------------------+--------------------+-------------------+
| Status | Message Length | Message | Padding Length | Padding |
| 1B | QUIC varint | bytes | QUIC varint | bytes |
+--------+-------------------+-------------------+--------------------+-------------------+源码:proxy/hysteria/protocol.go:79-142
| 字段 | 大小 | 描述 |
|---|---|---|
| Status | 1 字节 | 0x00 = 成功、0x01 = 错误 |
| Message Length | QUIC 可变长整数 | 0 到 2048 |
| Message | 字节 | 错误消息(可选) |
| Padding Length | QUIC 可变长整数 | 0 到 4096 |
| Padding | 字节 | 随机填充 |
响应头之后,QUIC 流承载原始的代理 TCP 数据。
默认填充
var (
tcpRequestPadding = padding.Padding{Min: 64, Max: 512}
tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
)源码:proxy/hysteria/config.go:7-9
UDP 消息
UDP 使用 QUIC 数据报(不可靠、无序)。每个数据报包含:
+------------+----------+---------+-----------+-------------------+---------+------+
| Session ID | Packet ID| Frag ID | Frag Count| Address Length | Address | Data |
| 4B BE | 2B BE | 1B | 1B | QUIC varint | bytes | ... |
+------------+----------+---------+-----------+-------------------+---------+------+源码:proxy/hysteria/protocol.go:144-216
type UDPMessage struct {
SessionID uint32 // 4 bytes, big-endian
PacketID uint16 // 2 bytes, big-endian
FragID uint8 // Fragment index (0-based)
FragCount uint8 // Total fragments (1 = no fragmentation)
Addr string // "host:port" with varint length prefix
Data []byte // Remaining bytes
}源码:proxy/hysteria/protocol.go:153-160
| 字段 | 大小 | 描述 |
|---|---|---|
| Session ID | 4 字节 | 标识 UDP 会话(当前设置为 0) |
| Packet ID | 2 字节 | 标识数据包,用于分片重组 |
| Fragment ID | 1 字节 | 分片索引(0 到 FragCount-1) |
| Fragment Count | 1 字节 | 总分片数(1 = 未分片) |
| Address Length | QUIC 可变长整数 | 地址字符串长度 |
| Address | 字节 | "host:port" 格式的目标地址 |
| Data | 剩余字节 | 实际的 UDP 载荷 |
QUIC 可变长整数编码
Hysteria 使用 QUIC 的可变长整数编码(RFC 9000):
| 范围 | 前缀位 | 字节数 |
|---|---|---|
| 0-63 | 00 | 1 |
| 64-16383 | 01 | 2 |
| 16384-1073741823 | 10 | 4 |
| 1073741824-4611686018427387903 | 11 | 8 |
func varintPut(b []byte, i uint64) int {
if i <= 63 { b[0] = uint8(i); return 1 }
if i <= 16383 { b[0] = uint8(i>>8) | 0x40; b[1] = uint8(i); return 2 }
if i <= 1073741823 { b[0] = uint8(i>>24) | 0x80; /* ... */ return 4 }
// ...8-byte encoding
}源码:proxy/hysteria/protocol.go:220-249
UDP 分片
当 UDP 消息超过 QUIC 数据报 MTU 时,会被拆分为分片:
func FragUDPMessage(m *UDPMessage, maxSize int) []UDPMessage {
if m.Size() <= maxSize {
return []UDPMessage{*m}
}
maxPayloadSize := maxSize - m.HeaderSize()
fragCount := (len(fullPayload) + maxPayloadSize - 1) / maxPayloadSize
// Split data across fragments, sharing same PacketID
}源码:proxy/hysteria/frag.go:3-27
重组器
Defragger 重组分片的 UDP 消息。它一次只处理一个数据包 ID——如果在前一个数据包的所有分片到达之前收到新数据包,之前的状态会被丢弃:
type Defragger struct {
pktID uint16
frags []*UDPMessage
count uint8
size int
}
func (d *Defragger) Feed(m *UDPMessage) *UDPMessage {
if m.FragCount <= 1 { return m } // No fragmentation
if m.PacketID != d.pktID || m.FragCount != uint8(len(d.frags)) {
// New packet, reset state
d.pktID = m.PacketID
d.frags = make([]*UDPMessage, m.FragCount)
}
// Collect fragment, assemble when complete
}源码:proxy/hysteria/frag.go:29-73
出站处理器(客户端)
文件:proxy/hysteria/client.go
TCP 流程
func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
// For TCP: open QUIC stream
conn, err := dialer.Dial(hyCtx.ContextWithRequireDatagram(ctx, isUDP), c.server.Destination)
if target.Network == net.Network_TCP {
// Write TCP request header
WriteTCPRequest(bufferedWriter, target.NetAddr())
// Read TCP response header
ok, msg, err := ReadTCPResponse(conn)
// Bidirectional copy
}
}源码:proxy/hysteria/client.go:49-117
UDP 流程
对于 UDP,客户端使用 QUIC 数据报。需要 InterUdpConn 类型(来自 hysteria 传输层):
if target.Network == net.Network_UDP {
iConn := stat.TryUnwrapStatsConn(conn)
_, ok := iConn.(*hysteria.InterUdpConn)
if !ok {
return errors.New("udp requires hysteria udp transport")
}
writer := &UDPWriter{Writer: conn, buf: make([]byte, MaxUDPSize), addr: target.NetAddr()}
reader := &UDPReader{Reader: conn, buf: make([]byte, MaxUDPSize), df: &Defragger{}}
}源码:proxy/hysteria/client.go:119-164
UDPWriter 自动处理分片:
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
msg := &UDPMessage{SessionID: 0, FragCount: 1, Addr: addr, Data: b.Bytes()}
err := w.sendMsg(msg)
var errTooLarge *quic.DatagramTooLargeError
if go_errors.As(err, &errTooLarge) {
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
w.sendMsg(&fMsg)
}
}
}源码:proxy/hysteria/client.go:190-235
入站处理器(服务端)
文件:proxy/hysteria/server.go
服务端同时处理 QUIC 流连接(TCP)和 QUIC 数据报连接(UDP):
func (s *Server) Process(ctx context.Context, network net.Network,
conn stat.Connection, dispatcher routing.Dispatcher) error {
iConn := stat.TryUnwrapStatsConn(conn)
if _, ok := iConn.(*hysteria.InterUdpConn); ok {
// UDP mode: read datagrams, defragger, dispatch
for {
msg, _ := ParseUDPMessage(b[:n])
dfMsg := df.Feed(msg)
if dfMsg != nil {
dest, _ := net.ParseDestination("udp:" + dfMsg.Addr)
// Dispatch via DispatchLink
}
}
} else {
// TCP mode: read request, dispatch, write response
addr, _ := ReadTCPRequest(conn)
dest, _ := net.ParseDestination("tcp:" + addr)
WriteTCPResponse(bufferedWriter, true, "")
dispatcher.DispatchLink(ctx, dest, &transport.Link{...})
}
}源码:proxy/hysteria/server.go:81-192
用户认证
用户在传输层进行验证。服务端从连接中提取用户信息:
type User interface{ User() *protocol.MemoryUser }
if v, ok := conn.(User); ok {
inbound.User = v.User()
}源码:proxy/hysteria/server.go:88-95
account.Validator 支持运行时用户管理(添加/移除/获取)。
源码:proxy/hysteria/server.go:57-75
常量
const (
MaxAddressLength = 2048
MaxMessageLength = 2048
MaxPaddingLength = 4096
MaxUDPSize = 4096
)源码:proxy/hysteria/protocol.go:13-17
实现说明
QUIC 依赖:Hysteria2 依赖
github.com/apernet/quic-go,这是 quic-go 的一个分支,包含针对 Brutal 拥塞控制和其他 Hysteria 特定功能的修改。传输与协议分离:与大多数 Xray 协议中代理层处理加密不同,Hysteria 将所有加密操作委托给 QUIC/TLS 传输层。代理层仅处理请求/响应帧封装。
网络类型报告为 TCP:入站将其网络报告为 TCP(
net.Network_TCP),尽管底层传输是 UDP/QUIC。这是因为从 Xray 的角度来看,QUIC 流的行为类似于 TCP 连接。
源码:proxy/hysteria/server.go:77-79
- 数据报要求:对于 UDP 代理,客户端传递
ContextWithRequireDatagram(ctx, true)以通知传输层需要 QUIC 数据报支持。
源码:proxy/hysteria/client.go:59
简单的重组机制:
Defragger一次只跟踪一个数据包。如果来自不同数据包的分片交错到达,重组将失败。这是一种有意的简单性权衡——在实践中,QUIC 数据报在单个连接中通常不会被重新排序。地址为字符串格式:与 VMess/Trojan/Shadowsocks 中的二进制编码地址不同,Hysteria 使用纯文本
"host:port"字符串作为地址,带有 QUIC 可变长整数长度前缀。这简化了实现,但增加了少量开销。Session ID:当前在客户端中硬编码为
0。该字段为未来的多会话多路复用而保留,但目前未使用。
源码:proxy/hysteria/client.go:204
- 填充:请求和响应都包含随机长度的填充以抵抗流量指纹识别。请求填充为 64-512 字节,响应填充为 128-1024 字节。