Hysteria2 协议
Hysteria2 是一种基于 QUIC 的代理协议,具有通过 Brutal 拥塞控制算法进行带宽协商、Salamander 混淆和 HTTP/3 伪装等特性。sing-box 将协议实现委托给 sing-quic/hysteria2。
源码: protocol/hysteria2/inbound.go, protocol/hysteria2/outbound.go, sing-quic/hysteria2
架构概览
inbound 和 outbound 都是 sing-quic/hysteria2 库的轻量级包装:
// Inbound
type Inbound struct {
inbound.Adapter
router adapter.Router
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
service *hysteria2.Service[int]
userNameList []string
}
// Outbound
type Outbound struct {
outbound.Adapter
logger logger.ContextLogger
client *hysteria2.Client
}TLS 要求
Hysteria2 在两端都无条件要求 TLS:
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}Salamander 混淆
Salamander 是唯一支持的混淆类型。它在 QUIC 包外层包裹一层混淆,防止深度包检测识别其为 QUIC:
var salamanderPassword string
if options.Obfs != nil {
if options.Obfs.Password == "" {
return nil, E.New("missing obfs password")
}
switch options.Obfs.Type {
case hysteria2.ObfsTypeSalamander:
salamanderPassword = options.Obfs.Password
default:
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
}
}当启用 Salamander 时,客户端和服务端的密码必须匹配。
带宽协商(Brutal CC)
Hysteria2 的核心特性是其 Brutal 拥塞控制算法,要求客户端声明其带宽。服务端也可以设置带宽限制:
service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{
Context: ctx,
Logger: logger,
BrutalDebug: options.BrutalDebug,
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
SalamanderPassword: salamanderPassword,
TLSConfig: tlsConfig,
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
UDPTimeout: udpTimeout,
Handler: inbound,
MasqueradeHandler: masqueradeHandler,
})关键带宽字段:
- SendBPS / ReceiveBPS:服务端的发送和接收带宽(比特每秒),通过
hysteria.MbpsToBps从 Mbps 转换 - IgnoreClientBandwidth:为 true 时,服务端忽略客户端声明的带宽并使用自身设置
- BrutalDebug:启用拥塞控制的调试日志
outbound 类似地声明其带宽:
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
// ...
})伪装
当收到非 Hysteria2 流量(如 Web 浏览器)时,inbound 可以提供伪装响应。支持三种伪装类型:
文件服务器
case C.Hysterai2MasqueradeTypeFile:
masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory))反向代理
case C.Hysterai2MasqueradeTypeProxy:
masqueradeURL, _ := url.Parse(options.Masquerade.ProxyOptions.URL)
masqueradeHandler = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(masqueradeURL)
if !options.Masquerade.ProxyOptions.RewriteHost {
r.Out.Host = r.In.Host
}
},
}静态字符串
case C.Hysterai2MasqueradeTypeString:
masqueradeHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if options.Masquerade.StringOptions.StatusCode != 0 {
w.WriteHeader(options.Masquerade.StringOptions.StatusCode)
}
w.Write([]byte(options.Masquerade.StringOptions.Content))
})端口跳跃
outbound 支持端口跳跃——连接到多个服务器端口以规避基于端口的限速:
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts, // 端口范围列表
HopInterval: time.Duration(options.HopInterval), // 切换端口的频率
// ...
})监听器模型
与基于 TCP 的协议不同,Hysteria2 监听 UDP(QUIC)。inbound 通过监听 UDP 包并将它们传递给 QUIC 服务来启动:
func (h *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
h.tlsConfig.Start()
packetConn, err := h.listener.ListenUDP()
if err != nil {
return err
}
return h.service.Start(packetConn)
}用户管理
用户通过整数索引标识,配有一个并行的名称列表用于日志记录:
userList := make([]int, 0, len(options.Users))
userNameList := make([]string, 0, len(options.Users))
userPasswordList := make([]string, 0, len(options.Users))
for index, user := range options.Users {
userList = append(userList, index)
userNameList = append(userNameList, user.Name)
userPasswordList = append(userPasswordList, user.Password)
}
service.UpdateUsers(userList, userPasswordList)认证使用存储在 context 中的用户索引:
userID, _ := auth.UserFromContext[int](ctx)
if userName := h.userNameList[userID]; userName != "" {
metadata.User = userName
}连接处理
TCP 和 UDP 连接都遵循标准的 sing-box 模式:
func (h *Inbound) NewConnectionEx(ctx, conn, source, destination, onClose) {
// 设置 metadata 字段
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (h *Inbound) NewPacketConnectionEx(ctx, conn, source, destination, onClose) {
// 设置 metadata 字段
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}Outbound 连接
func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP:
return h.client.DialConn(ctx, destination)
case N.NetworkUDP:
conn, err := h.ListenPacket(ctx, destination)
return bufio.NewBindPacketConn(conn, destination), nil
}
}
func (h *Outbound) ListenPacket(ctx, destination) (net.PacketConn, error) {
return h.client.ListenPacket(ctx)
}网络接口更新
outbound 实现了 adapter.InterfaceUpdateListener 以在网络变化时关闭 QUIC 连接:
func (h *Outbound) InterfaceUpdated() {
h.client.CloseWithError(E.New("network changed"))
}配置示例
Inbound
{
"type": "hysteria2",
"tag": "hy2-in",
"listen": "::",
"listen_port": 443,
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
"type": "salamander",
"password": "obfs-password"
},
"users": [
{ "name": "user1", "password": "user-password" }
],
"tls": {
"enabled": true,
"certificate_path": "/path/to/cert.pem",
"key_path": "/path/to/key.pem"
},
"masquerade": {
"type": "proxy",
"proxy": {
"url": "https://www.example.com",
"rewrite_host": true
}
}
}Outbound
{
"type": "hysteria2",
"tag": "hy2-out",
"server": "example.com",
"server_port": 443,
"up_mbps": 50,
"down_mbps": 100,
"password": "user-password",
"obfs": {
"type": "salamander",
"password": "obfs-password"
},
"tls": {
"enabled": true,
"server_name": "example.com"
}
}使用端口跳跃
{
"type": "hysteria2",
"tag": "hy2-hop",
"server": "example.com",
"server_ports": "443,8443-8500",
"hop_interval": "30s",
"password": "user-password",
"tls": {
"enabled": true,
"server_name": "example.com"
}
}