Hysteria realm — sing-box
hysteria-realm is a rendezvous service for Hysteria2 NAT traversal. A Hysteria2 server behind NAT discovers its public addresses via STUN and registers them under a realm ID on this service; clients ask the realm for those addresses, both sides punch a UDP hole, and the QUIC connection is then made directly. The realm only carries control-plane signaling — proxy traffic never passes through it.
Options
type: "hysteria-realm" under services[]:
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
users | []HysteriaRealmUser | (required) | [HysteriaRealmUser] | Accounts allowed to use the realm. At least one is required; startup fails otherwise. |
Source: option/hysteria2.go:235-240 · pinned at v1.14.2 (af6e64c)
The service also embeds ListenOptions (see Inbounds), an inbound tls block (see TLS) and the HTTP/2 fields below. With tls it serves HTTP/2 over TLS (h2 is added to the ALPN list automatically); without it, plain HTTP — HTTP/1.1, with cleartext HTTP/2 accepted too.
users[]
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
name | string | (required) | <string> | Account name, used in logs and as the key of the per-user realm quota. |
token | string | (required) | <string> | Bearer token that Hysteria2 inbounds and outbounds present as Authorization: Bearer <token> (their realm.token). |
max_realms | int | 0 | <int> | How many realm slots this account may hold at once. 0 means unlimited; beyond the limit, registration is refused with HTTP 429. |
Source: option/hysteria2.go:229-233 · pinned at v1.14.2 (af6e64c)
HTTP/2 fields
These tune the service's HTTP/2 server and only matter when clients speak HTTP/2.
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
idle_timeout | badoption.Duration | (Go default) | <duration> | Close HTTP/2 connections that stay idle this long. |
keep_alive_period | badoption.Duration | (disabled) | <duration> | Send an HTTP/2 PING when nothing has been read for this long, to detect dead connections. |
stream_receive_window | *byteformats.MemoryBytes | (Go default) | <size> | Per-stream upload buffer (flow-control window), as a memory size. |
connection_receive_window | *byteformats.MemoryBytes | (Go default) | <size> | Per-connection upload buffer, as a memory size. |
max_concurrent_streams | int | (Go default) | <int> | Maximum concurrent HTTP/2 streams per connection. |
Source: option/http.go:14-20 · pinned at v1.14.2 (af6e64c)
How Hysteria2 uses the realm
- Server side: set
realmon the Hysteria2 inbound withserver_urlpointing at this service, atokenfromusers[], arealm_idandstun_servers. The inbound registers its STUN-discovered addresses under that ID and keeps the registration alive with heartbeats. - Client side: set
realmon the Hysteria2 outbound (instead ofserver) with the sameserver_url, a validtokenand the samerealm_id. The outbound looks the addresses up, hole-punches, then runs the normal QUIC handshake. - Field details (
ip_version,port_mapping,http_client,stun_domain_resolver) are on the Hysteria2 page.
Minimal example
The realm service, on a host with a stable public address:
{
"services": [
{
"type": "hysteria-realm",
"tag": "realm",
"listen": "::",
"listen_port": 8443,
"tls": {
"enabled": true,
"certificate_path": "/etc/ssl/realm.pem",
"key_path": "/etc/ssl/realm.key"
},
"users": [
{ "name": "home", "token": "<token>", "max_realms": 2 }
]
}
]
}The matching realm block on the Hysteria2 inbound behind NAT (the outbound uses the same server_url, token and realm_id):
"realm": {
"server_url": "https://realm.example.com:8443",
"token": "<token>",
"realm_id": "home-server",
"stun_servers": ["stun.l.google.com:19302"]
}Notes
- Requires the
with_quicbuild tag; without it the type exists but fails at startup. usersmust be non-empty, and every entry needs bothnameandtoken.- A realm ID can be held by only one registration at a time: a second server registering the same ID gets HTTP 409 (
realm_taken). - The API lives under
/v1/<realm_id>— keep that path intact if you put a reverse proxy in front. - Only the realm needs to be publicly reachable; the Hysteria2 server behind NAT needs no port forwarding.
Cross-core notes
- mihomo has an equivalent rendezvous listener, Hysteria2 realm, with one shared
tokenplusmax-realms/max-realms-per-ipcaps instead of per-user tokens. - Xray-core has no realm server; its
realmfinalmask UDP mask is the endpoint-side counterpart. See Hysteria2 — Xray-core.
Source: option/hysteria2.go:229-240 · v1.14.2 (af6e64c)
