Skip to content

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[]:

FieldTypeDefaultAllowed valuesDescription
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[] ​

FieldTypeDefaultAllowed valuesDescription
namestring(required)<string>Account name, used in logs and as the key of the per-user realm quota.
tokenstring(required)<string>Bearer token that Hysteria2 inbounds and outbounds present as Authorization: Bearer <token> (their realm.token).
max_realmsint0<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.

FieldTypeDefaultAllowed valuesDescription
idle_timeoutbadoption.Duration(Go default)<duration>Close HTTP/2 connections that stay idle this long.
keep_alive_periodbadoption.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_streamsint(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 realm on the Hysteria2 inbound with server_url pointing at this service, a token from users[], a realm_id and stun_servers. The inbound registers its STUN-discovered addresses under that ID and keeps the registration alive with heartbeats.
  • Client side: set realm on the Hysteria2 outbound (instead of server) with the same server_url, a valid token and the same realm_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:

json
{
  "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):

json
"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_quic build tag; without it the type exists but fails at startup.
  • users must be non-empty, and every entry needs both name and token.
  • 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 token plus max-realms / max-realms-per-ip caps instead of per-user tokens.
  • Xray-core has no realm server; its realm finalmask UDP mask is the endpoint-side counterpart. See Hysteria2 — Xray-core.

Source: option/hysteria2.go:229-240 · v1.14.2 (af6e64c)

Core Tutorial by Argsment