Limitations of Hysteria Core: 8 Architectural Constraints in the `core/` Package
Hysteria 2’s core architecture imposes strict constraints including single-packet UDP hook visibility, global-only bandwidth controls, mandatory process restarts for configuration changes, and limited QUIC tunability, which require custom implementations for fine-grained traffic management.
Hysteria is a high-performance, QUIC-based proxy framework maintained in the apernet/hysteria repository. While the core/ package delivers robust connectivity and congestion control, its design enforces specific limitations on deep packet inspection, traffic shaping, and operational flexibility. Understanding these architectural boundaries is essential for developers deploying production proxies or extending the protocol.
UDP Packet Inspection Limitations
First-Packet-Only Hook Visibility
The RequestHook interface can only inspect the first UDP packet of a session. According to the source comments in core/server/config.go (lines 144-146), the hook is called after the initial packet arrives but cannot access subsequent packets or "put back" data into the stream. This design prevents deep packet inspection (DPI) or protocol sniffing beyond the opening transmission.
No Data Replay or Modification
Because the first UDP packet is forwarded as-is before the hook executes, you cannot prepend, modify, or replay the initial payload. Any alterations must occur after the first transmission, limiting the hook’s ability to implement protocol-level transformations or injections.
Bandwidth and Traffic Control Constraints
Global-Only Rate Limiting
The server configuration exposes only global bandwidth controls. The BandwidthConfig struct in core/server/config.go (lines 107-111) provides MaxTx and MaxRx fields that apply to the entire server instance. There are no native per-connection or per-stream rate limits in the core API.
Implementing Per-Connection Limits via TrafficLogger
To enforce granular throttling, you must implement a custom TrafficLogger. The interface receives byte counts per connection ID, allowing you to terminate sessions that exceed thresholds:
package main
import (
"log"
"github.com/apernet/hysteria/core/server"
)
type limiter struct {
maxTx uint64
maxRx uint64
}
func (l *limiter) LogTraffic(id string, tx, rx uint64) bool {
if tx > l.maxTx || rx > l.maxRx {
return false // disconnect this client
}
return true
}
func (l *limiter) LogOnlineState(id string, online bool) {}
func (l *limiter) TraceStream(s server.HyStream, st *server.StreamStats) {}
func (l *limiter) UntraceStream(s server.HyStream) {}
func main() {
// Inside server config:
// TrafficLogger: &limiter{maxTx: 10*1024*1024, maxRx: 20*1024*1024},
}
Network Behavior and Protocol Limitations
Full-Cone NAT by Default
The default outbound UDP implementation uses net.ListenUDP without binding to a specific remote address (core/server/config.go, lines 78-84). This creates a full-cone NAT behavior where any client can send packets to the same port, simplifying routing but potentially bypassing source-address validation requirements.
UDP Fragmentation Requirements
Packets larger than the QUIC unreliable-datagram size must be fragmented according to the protocol specification (PROTOCOL.md, lines 113-120). The core validates fragment IDs but does not maintain reassembly buffers beyond simple validation; losing any fragment discards the entire UDP payload.
Limited Obfuscation Security
The optional "Salamander" obfuscation layer applies XOR encryption using a BLAKE2b-256-derived keystream (PROTOCOL.md, lines 29-52). While this effectively disguises traffic patterns from simple deep packet inspection, it provides no forward secrecy or authenticated encryption, making it insufficient for strict cryptographic requirements.
Configuration and Operational Constraints
Restricted QUIC Tunability
Only a specific subset of QUIC parameters are exposed through the QUICConfig struct (core/server/config.go, lines 23-32). You can adjust initial/maximum stream windows, connection windows, idle timeout, max incoming streams, and MTU discovery. However, deeper QUIC tweaks are inaccessible, and the underlying library may override certain fields on unsupported platforms.
No Configuration Hot-Reload
The core initializes all settings at startup and does not watch for file changes. According to the initialization flow in core/server/server.go, modifying server options—such as bandwidth limits, request hooks, or authentication parameters—requires a complete process restart.
Implementation Patterns and Workarounds
Despite these limitations, the core API supports custom middleware through its hook and logging interfaces.
Logging First UDP Packets with Custom Hooks
While you cannot inspect all packets, hooks remain useful for logging initial session metadata:
package main
import (
"log"
"net"
"github.com/apernet/hysteria/core/server"
)
type logHook struct{}
func (h *logHook) Check(isUDP bool, reqAddr string) bool { return isUDP }
func (h *logHook) TCP(_ server.HyStream, _ *string) ([]byte, error) { return nil, nil }
func (h *logHook) UDP(data []byte, reqAddr *string) error {
log.Printf("first UDP packet from %s, %d bytes", *reqAddr, len(data))
return nil // allow the packet to continue
}
func main() {
cfg := &server.Config{
Bind: ":443",
QUIC: server.DefaultQUICConfig(),
Bandwidth: server.BandwidthConfig{MaxTx: 0, MaxRx: 0}, // unlimited
RequestHook: &logHook{},
Authenticator: server.NoAuth{},
Outbound: &server.DefaultOutbound{},
TrafficLogger: server.NoTrafficLogger{},
EventLogger: server.NoEventLogger{},
}
srv, err := server.New(cfg)
if err != nil {
log.Fatalf("failed to create server: %v", err)
}
if err := srv.Serve(); err != nil {
log.Fatalf("server stopped: %v", err)
}
}
Establishing Client Connections
The client implementation respects server-advertised rates but maintains independent congestion control when receiving "auto" bandwidth settings:
package main
import (
"log"
"github.com/apernet/hysteria/core/client"
)
func main() {
cfg := &client.Config{
Server: "hysteria.example.com:443",
Auth: "my-secret",
QUIC: client.DefaultQUICConfig(),
TLSInsecureSkipVerify: true, // for testing only
}
c, err := client.New(cfg)
if err != nil {
log.Fatalf("client init error: %v", err)
}
stream, err := c.DialTCP("example.org:80")
if err != nil {
log.Fatalf("dial error: %v", err)
}
_ = stream // Use stream.Read/Write as net.Conn
}
Summary
- Request hooks only see the first UDP packet and cannot replay or modify data, limiting DPI capabilities to session initialization.
- Bandwidth controls are global-only; per-connection throttling requires a custom
TrafficLoggerimplementation. - UDP operates in full-cone mode by default with mandatory fragmentation for large payloads, where fragment loss discards the entire packet.
- Configuration hot-reloading is not supported; changes require process restarts according to
core/server/server.go. - QUIC tunability is limited to specific window and timeout parameters defined in
core/server/config.go. - Salamander obfuscation provides only XOR-based traffic shaping without cryptographic guarantees according to
PROTOCOL.md.
Frequently Asked Questions
Can Hysteria core inspect all UDP packets in a session?
No. The RequestHook interface only processes the first UDP packet per session. As documented in core/server/config.go (lines 144-146), subsequent packets bypass the hook entirely, and the system cannot re-inject or modify the initial packet after inspection has occurred.
How do I implement per-user bandwidth limits in Hysteria?
Since core/server/config.go only supports global MaxTx/MaxRx settings (lines 107-111), you must create a custom TrafficLogger implementation. Track byte counts per connection ID in the LogTraffic method and return false to disconnect clients exceeding their allocated bandwidth thresholds.
Does Hysteria support configuration hot-reloading?
No. The core initializes all settings at startup according to the initialization flow in core/server/server.go. Modifying server options—such as bandwidth limits, hooks, or authentication—requires stopping and restarting the Hysteria process, as no file-watching mechanism exists.
Is the Salamander obfuscation cryptographically secure?
No. According to PROTOCOL.md (lines 29-52), Salamander uses XOR obfuscation with a BLAKE2b-256-derived keystream. While this effectively disguises traffic patterns from simple deep packet inspection, it provides no forward secrecy or authenticated encryption against sophisticated cryptographic analysis.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →