Hysteria2 Protocol
Hysteria2 is a QUIC-based proxy protocol featuring bandwidth negotiation via the Brutal congestion control algorithm, Salamander obfuscation, and HTTP/3 masquerading. sing-box delegates the protocol implementation to sing-quic/hysteria2.
Source: protocol/hysteria2/inbound.go, protocol/hysteria2/outbound.go, sing-quic/hysteria2
Architecture Overview
Both inbound and outbound are thin wrappers around the sing-quic/hysteria2 library:
// 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 Requirement
Hysteria2 unconditionally requires TLS on both sides:
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}Salamander Obfuscation
Salamander is the only supported obfuscation type. It wraps QUIC packets in a layer of obfuscation to prevent deep packet inspection from identifying them as 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)
}
}When Salamander is enabled, the password must match between client and server.
Bandwidth Negotiation (Brutal CC)
Hysteria2's core feature is its Brutal congestion control algorithm, which requires the client to declare its bandwidth. The server can also set bandwidth limits:
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,
})Key bandwidth fields:
- SendBPS / ReceiveBPS: Server's send and receive bandwidth in bits per second, converted from Mbps using
hysteria.MbpsToBps - IgnoreClientBandwidth: When true, the server ignores client-declared bandwidth and uses its own settings
- BrutalDebug: Enables debug logging for congestion control
The outbound similarly declares its bandwidth:
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
// ...
})Masquerade
When non-Hysteria2 traffic arrives (e.g., a web browser), the inbound can serve a masquerade response. Three masquerade types are supported:
File Server
case C.Hysterai2MasqueradeTypeFile:
masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory))Reverse Proxy
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
}
},
}Static String
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))
})Port Hopping
The outbound supports port hopping -- connecting to multiple server ports to evade per-port throttling:
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts, // port range list
HopInterval: time.Duration(options.HopInterval), // how often to switch ports
// ...
})Listener Model
Unlike TCP-based protocols, Hysteria2 listens on UDP (QUIC). The inbound starts by listening for UDP packets and passing them to the QUIC service:
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)
}User Management
Users are identified by integer index, with a parallel name list for logging:
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)Authentication uses the user index stored in context:
userID, _ := auth.UserFromContext[int](ctx)
if userName := h.userNameList[userID]; userName != "" {
metadata.User = userName
}Connection Handling
Both TCP and UDP connections follow the standard sing-box pattern:
func (h *Inbound) NewConnectionEx(ctx, conn, source, destination, onClose) {
// Set metadata fields
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (h *Inbound) NewPacketConnectionEx(ctx, conn, source, destination, onClose) {
// Set metadata fields
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}Outbound Connection
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)
}Interface Update
The outbound implements adapter.InterfaceUpdateListener to handle network changes by closing the QUIC connection:
func (h *Outbound) InterfaceUpdated() {
h.client.CloseWithError(E.New("network changed"))
}Configuration Examples
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"
}
}With Port Hopping
{
"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"
}
}