# Limitations of Hysteria Core: 8 Architectural Constraints in the `core/` Package

> Discover Hysteria 2 core architectural constraints. Learn about limitations in UDP visibility, bandwidth controls, config changes, and QUIC tunability requiring custom solutions.

- Repository: [Aperture Internet Laboratory/hysteria](https://github.com/apernet/hysteria)
- Tags: architecture
- Published: 2026-05-13

---

**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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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:

```go
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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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:

```go
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:

```go
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 `TrafficLogger` implementation.
- **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`](https://github.com/apernet/hysteria/blob/main/core/server/server.go).
- **QUIC tunability is limited** to specific window and timeout parameters defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go).
- **Salamander obfuscation provides only XOR-based traffic shaping** without cryptographic guarantees according to [`PROTOCOL.md`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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.