Skip to content

Hysteria2 — sing-box ​

sing-box's Hysteria2 implementation is the cleanest of the three cores: a single flat block on each side, polymorphic masquerade, and explicit port-hopping fields on the outbound.

Inbound ​

type: "hysteria2" inbound:

FieldTypeDefaultAllowed valuesDescription
up_mbpsint0<Mbps>Estimated uplink bandwidth in Mbps. The server uses this as a hint for congestion control.
down_mbpsint0<Mbps>Estimated downlink bandwidth in Mbps.
obfs*Hysteria2Obfs(disabled)Hysteria2ObfsObfuscation block (salamander or gecko). When set, both sides must match.
users[]Hysteria2User[][Hysteria2User]Accepted users.
ignore_client_bandwidthboolfalsetrue | falseDiscard the client's bandwidth advertisement and use the server's bandwidth settings unilaterally.
masquerade*Hysteria2Masquerade(disabled)Hysteria2MasqueradeHTTP-response masquerade for unauthenticated traffic. Accepts a string URL or a typed object.
bbr_profilestringstandardconservative | standard | aggressiveBBR congestion-control profile, used whenever BBR is selected.
brutal_debugboolfalsetrue | falseLog Brutal congestion-control internals.
realm*Hysteria2InboundRealm(disabled)Hysteria2InboundRealmRegister this server with a Hysteria Realm rendezvous service for NAT traversal: it discovers its public addresses via STUN, registers them under realm_id, and accepts clients by UDP hole-punching — no publicly reachable listen address needed.

Source: option/hysteria2.go:17-30 · pinned at v1.14.2 (af6e64c)

The struct embeds ListenOptions, InboundTLSOptionsContainer and QUICOptions (see QUIC fields below). A TLS configuration is required — Hysteria2 runs on QUIC and there is no plaintext mode.

obfs ​

FieldTypeDefaultAllowed valuesDescription
typestring(required)salamander | geckoObfuscation type. gecko also takes min_packet_size / max_packet_size.
passwordstring(required)<string>Obfuscation password (separate from the user password).

Source: option/hysteria2.go:64-68 · pinned at v1.14.2 (af6e64c)

With type: "gecko" the same object also takes:

FieldTypeDefaultAllowed valuesDescription
min_packet_sizeint512<bytes>Minimum on-wire packet size in bytes. Gecko only.
max_packet_sizeint1200<bytes>Maximum on-wire packet size in bytes. Gecko only.

Source: option/hysteria2.go:59-62 · pinned at v1.14.2 (af6e64c)

users[] ​

FieldTypeDefaultAllowed valuesDescription
namestring(unset)<string>Display name used in stats and logs.
passwordstring(required)<string>User authentication password.

Source: option/hysteria2.go:116-119 · pinned at v1.14.2 (af6e64c)

masquerade ​

The masquerade field is polymorphic (option/hysteria2.go:121-181):

  • A plain string URL. Schemes:
    • file:///var/www — equivalent to { "type": "file", "directory": "/var/www" }.
    • https://upstream.example.com — equivalent to { "type": "proxy", "url": "..." }.
  • An object with a type field selecting one of three shapes:
FieldTypeDefaultAllowed valuesDescription
typestring(unset)file | proxy | stringSelects which sub-block is active.

Source: option/hysteria2.go:121-126 · pinned at v1.14.2 (af6e64c)

type: "file" ​

FieldTypeDefaultAllowed valuesDescription
directorystring(required)<dir path>Local directory served at the masquerade endpoint.

Source: option/hysteria2.go:195-197 · pinned at v1.14.2 (af6e64c)

type: "proxy" ​

FieldTypeDefaultAllowed valuesDescription
urlstring(required)<URL>Upstream URL the masquerade endpoint reverse-proxies to.
rewrite_hostboolfalsetrue | falseRewrite the Host header to match the upstream URL.

Source: option/hysteria2.go:199-202 · pinned at v1.14.2 (af6e64c)

type: "string" ​

FieldTypeDefaultAllowed valuesDescription
status_codeint200<int>HTTP status code returned.
headersbadoption.HTTPHeader{}{<header>: <value>}Extra response headers.
contentstring(required)<text>Response body.

Source: option/hysteria2.go:204-208 · pinned at v1.14.2 (af6e64c)

Outbound ​

type: "hysteria2" outbound:

FieldTypeDefaultAllowed valuesDescription
server_portsbadoption.Listable[string][]<range>Port-hopping list. Each entry is a port (e.g. "20001") or a hyphenated range (e.g. "20001-20100").
hop_intervalbadoption.Duration30s<duration>How often to switch to a new port. Accepts Go-style durations.
hop_interval_maxbadoption.Duration(unset)<duration>Upper bound for randomized port hopping: each hop waits a random time between hop_interval and this value.
up_mbpsint0<Mbps>Estimated uplink bandwidth in Mbps.
down_mbpsint0<Mbps>Estimated downlink bandwidth in Mbps.
obfs*Hysteria2Obfs(disabled)Hysteria2ObfsObfuscation (salamander or gecko); must match the server.
passwordstring(required)<string>User auth password.
networkNetworkList(tcp+udp)tcp | udp | Restrict to TCP-only or UDP-only.
bbr_profilestringstandardconservative | standard | aggressiveBBR congestion-control profile, used whenever BBR is selected.
brutal_debugboolfalsetrue | falseLog Brutal CC internals on the client side.
disable_chrome_parrotboolfalsetrue | falseTurn off Chrome QUIC-handshake parroting, which is on by default. Parroting applies Chrome's QUIC parameters (idle_timeout fixed at 30s; Chrome's max_concurrent_streams and initial_packet_size; receive windows start at Chrome's initial values) and fails against servers using Ed25519 certificates.
realm*Hysteria2Realm(disabled)Hysteria2RealmReach the server through a Hysteria Realm: query the realm for the addresses registered under realm_id, hole-punch, then run the normal QUIC handshake. Conflicts with server, server_port and server_ports.

Source: option/hysteria2.go:210-227 · pinned at v1.14.2 (af6e64c)

Embeds DialerOptions, ServerOptions (server, server_port), OutboundTLSOptionsContainer (tls — required) and QUICOptions.

Realm (NAT traversal) ​

A server behind NAT sets realm on its inbound and registers with a Hysteria Realm service; clients set realm on the outbound (instead of server) to find it. Both sides share this shape:

FieldTypeDefaultAllowed valuesDescription
server_urlstring(required)<URL>Realm rendezvous service URL.
tokenstring(unset)<string>Bearer token; must match one of the realm's users[].token.
realm_idstring(required)<id>Slot identifier. The server registers under it and clients must use the same value; 1–64 characters matching ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$.
stun_serversbadoption.Listable[string](required)<host[:port]> | …STUN servers used to discover public addresses. On the outbound, domain names are resolved with domain_resolver from the dial fields.
ip_versionint(both)4 | 6Restrict STUN, hole punching and the resulting QUIC path to one IP version.
port_mapping*Hysteria2RealmPortMapping(disabled)Hysteria2RealmPortMappingKeep a UDP port mapping on the local gateway via UPnP or NAT-PMP; failures are non-fatal. Requires IPv4.
http_client*HTTPClientOptions(default)<tag> | HTTPClientOptionsHTTP client used to talk to the realm (an inline object or the tag of an http_clients entry).

Source: option/hysteria2.go:32-40 · pinned at v1.14.2 (af6e64c)

The inbound form adds:

FieldTypeDefaultAllowed valuesDescription
stun_domain_resolver*DomainResolveOptions(default resolver)<dns server tag> | DomainResolveOptionsInbound only: resolver for STUN server domain names (same format as domain_resolver).

Source: option/hysteria2.go:54-57 · pinned at v1.14.2 (af6e64c)

port_mapping:

FieldTypeDefaultAllowed valuesDescription
enabledboolfalsetrue | falseEnable port mapping.
timeoutbadoption.Duration10s<duration>Timeout for gateway discovery and mapping operations.
lifetimebadoption.Duration10m<duration>Lease lifetime of the mapping; it is renewed at half of it.

Source: option/hysteria2.go:48-52 · pinned at v1.14.2 (af6e64c)

QUIC fields ​

Both sides embed the QUIC parameters shared with Hysteria, TUIC and HTTP/3 clients (QUICOptions, which in turn embeds HTTP2Options). With Chrome parroting enabled (the client default), idle_timeout is fixed at 30 seconds and max_concurrent_streams / initial_packet_size are replaced by Chrome's values.

FieldTypeDefaultAllowed valuesDescription
initial_packet_sizeint(QUIC default)<bytes>Initial QUIC packet size.
disable_path_mtu_discoveryboolfalsetrue | falseDisable QUIC path MTU discovery.

Source: option/http.go:22-26 · pinned at v1.14.2 (af6e64c)

FieldTypeDefaultAllowed valuesDescription
idle_timeoutbadoption.Duration(default)<duration>Idle connection timeout.
keep_alive_periodbadoption.Duration(default)<duration>Keep-alive period.
stream_receive_window*byteformats.MemoryBytes(default)<size>Per-stream flow-control receive window, as a memory size (e.g. 64 MB).
connection_receive_window*byteformats.MemoryBytes(default)<size>Per-connection flow-control receive window, as a memory size.
max_concurrent_streamsint(default)<int>Maximum concurrent streams per connection.

Source: option/http.go:14-20 · pinned at v1.14.2 (af6e64c)

Examples ​

Inbound with port hopping not exposed (server side just listens on one port), Salamander obfs, and a file-masquerade:

json
{
  "inbounds": [
    {
      "type": "hysteria2",
      "tag": "hy2-in",
      "listen": "::",
      "listen_port": 443,
      "users": [
        { "name": "alice", "password": "<password>" }
      ],
      "obfs": { "type": "salamander", "password": "<obfs>" },
      "tls": {
        "enabled": true,
        "alpn": ["h3"],
        "certificate_path": "/etc/ssl/cert.pem",
        "key_path": "/etc/ssl/key.pem"
      },
      "masquerade": "file:///var/www"
    }
  ]
}

Outbound with port hopping (20000-20100, switch every 30 seconds):

json
{
  "outbounds": [
    {
      "type": "hysteria2",
      "tag": "hy2-out",
      "server": "example.com",
      "server_port": 443,
      "server_ports": ["20000-20100"],
      "hop_interval": "30s",
      "password": "<password>",
      "obfs": { "type": "salamander", "password": "<obfs>" },
      "up_mbps": 100,
      "down_mbps": 300,
      "tls": { "enabled": true, "server_name": "example.com" }
    }
  ]
}

Notes ​

  • Bandwidth values are plain integer Mbps here — no unit string. Xray and mihomo accept suffixed strings ("100mbps"), sing-box does not.
  • obfs.type is salamander or gecko. Configs that omit obfs use the unobfuscated path.
  • The client parrots Chrome's QUIC handshake by default. Chrome does not advertise Ed25519, so a server with an Ed25519 certificate fails the handshake — use an ECDSA or RSA certificate, or set disable_chrome_parrot: true on the client.
  • On the outbound, leaving up_mbps / down_mbps unset selects BBR (tunable with bbr_profile) instead of Brutal.
  • masquerade accepts both the polymorphic typed object and a plain string URL — both are unmarshaled to the same internal representation (option/hysteria2.go:145-164).
  • ignore_client_bandwidth: true is the recommended setting for servers whose admin already knows the real bandwidth — it prevents a malicious client from understating its capacity to extract more from the server.

Cross-core notes ​

  • Xray-core supports Hysteria2 but splits config between settings and streamSettings.hysteriaSettings, with bandwidth, congestion control and port hopping under streamSettings.finalmask. See Hysteria2 — Xray-core.
  • mihomo uses a single-block outbound with up/down as strings (with unit suffixes like Xray). Port-hopping is ports + hop-interval. Users on the inbound are a map[string]string (username → password) rather than an object list. See Hysteria2 — mihomo.

Source: option/hysteria2.go:17-227 · v1.14.2 (af6e64c)

Core Tutorial by Argsment