Skip to content

Hysteria2 — Xray-core ​

Xray-core supports Hysteria v2 but splits the config across several blocks: the protocol-level settings (version, address/port, users), the transport-level streamSettings.hysteriaSettings (authentication, UDP idle timeout, masquerade), and streamSettings.finalmask for congestion control, bandwidth and port hopping. settings and hysteriaSettings must both be filled in for the outbound to be usable.

Outbound — protocol layer ​

settings for an outbound of "protocol": "hysteria":

FieldTypeDefaultAllowed valuesDescription
versionint32(required)2Must be exactly 2. Any other value is rejected at startup (infra/conf/hysteria.go:19-21).
address*Address(required)<host>Server hostname or IP.
portuint16(required)<port>Server UDP port.

Source: infra/conf/hysteria.go:13-17 · pinned at v26.9.9 (52a412d)

Hysteria v1 is unsupported

The version field must equal 2. HysteriaClientConfig.Build (infra/conf/hysteria.go:19-21) returns errors.New("version != 2") for anything else.

Inbound — protocol layer ​

settings for an inbound of "protocol": "hysteria":

FieldTypeDefaultAllowed valuesDescription
versionint32(required)2Must be 2.
users[]*HysteriaUserConfig[][HysteriaUserConfig]Accepted users. users and clients are interchangeable keys with the same shape.
clients[]*HysteriaUserConfig[][HysteriaUserConfig]Accepted users (alternative key; same shape as users).

Source: infra/conf/hysteria.go:39-43 · pinned at v26.9.9 (52a412d)

clients[] ​

FieldTypeDefaultAllowed valuesDescription
authstring(required)<string>Authentication string.
leveluint320<uint32>Policy level for this user.
emailstring(unset)<string>Tag in stats/logs.

Source: infra/conf/hysteria.go:33-37 · pinned at v26.9.9 (52a412d)

Transport layer — hysteriaSettings ​

Set under streamSettings.hysteriaSettings. Carries authentication, the UDP idle timeout and the inbound masquerade; congestion control, bandwidth and port hopping live in finalmask (below).

FieldTypeDefaultAllowed valuesDescription
versionint32(required)2Hysteria protocol version. Must match the version in settings.
authstring(required on outbound)<string>Outbound auth string. On the inbound this field is forwarded to the validator but per-user auth lives in settings.clients[].auth.
udpIdleTimeoutint6460<2..600 seconds>Seconds of UDP-stream idleness before the QUIC stream is closed. Must be between 2 and 600 inclusive (infra/conf/transport_method.go:764-766).
masqueradeMasquerade(unset)MasqueradeInbound-only: HTTP-response masquerade for unauthenticated traffic.

Source: infra/conf/transport_method.go:752-757 · pinned at v26.9.9 (52a412d)

masquerade ​

FieldTypeDefaultAllowed valuesDescription
typestring(required)file | proxy | stringSelects which sub-block applies.
dirstring(file only)<dir path>Directory served when type: file.
urlstring(proxy only)<URL>Upstream URL when type: proxy.
rewriteHostboolfalsetrue | falseRewrite the Host header when proxying (type: proxy).
xForwardedboolfalsetrue | falseAdd X-Forwarded-For / X-Forwarded-Host / X-Forwarded-Proto headers to proxied requests (type: proxy).
insecureboolfalsetrue | falseSkip TLS verification on the upstream when type: proxy.
contentstring(string only)<text>Body returned when type: string.
headersmap[string]string{}{<header>: <value>}Extra response headers when type: string.
statusCodeint32200<int>Status code returned when type: string.

Source: infra/conf/transport_method.go:737-750 · pinned at v26.9.9 (52a412d)

The type field switches the active sub-block: file uses dir, proxy uses url/rewriteHost/xForwarded/insecure, string uses content/headers/statusCode.

Congestion control & bandwidth — finalmask.quicParams ​

Set under streamSettings.finalmask.quicParams (the QUIC-parameter block shared with other QUIC-based transports). The fields relevant to Hysteria:

FieldTypeDefaultAllowed valuesDescription
congestionstring(empty)brutal | force-brutal | bbr | renoCongestion control for Hysteria. Empty / brutal: Brutal at min(brutalUp, the peer's advertised receive rate) when both are known, otherwise BBR. force-brutal: always Brutal at brutalUp (requires brutalUp). bbr: always BBR. reno: plain Reno.
bbrProfilestringstandardconservative | standard | aggressiveBBR tuning profile, used whenever BBR is selected. Unknown values fail at config build.
brutalUpBandwidth(unset)<bandwidth>This side's send rate for Brutal, as a unit string (see “Bandwidth syntax” below). Must be at least 65536 bytes/s (512 kbps) when set.
brutalDownBandwidth(unset)<bandwidth>Receive rate this side advertises to the peer during the Hysteria handshake; the peer caps its Brutal send rate to it. Same syntax and minimum as brutalUp.
brutalDisableLossCompensationboolfalsetrue | falseStop Brutal from raising its send rate to compensate for measured packet loss — it then sends at exactly the configured rate.

Source: infra/conf/transport_finalmask.go:993-1011 · pinned at v26.9.9 (52a412d)

Port hopping — udphop in finalmask.udp ​

Port hopping is a client-side UDP mask: add an entry with "type": "udphop" to streamSettings.finalmask.udp and put the options below under its settings. It must be the first entry of finalmask.udp, and it is outbound-only — an inbound rejects it with udphop: client only. The server still listens on a single port; forward the hop range to it (e.g. with an iptables DNAT / REDIRECT rule).

FieldTypeDefaultAllowed valuesDescription
sockopt*SocketConfig(unset)SocketConfigSocket options for the new local sockets intervalLocal opens on each hop.
modestring(required)intervalLocal | intervalRemote | perConnRemote | <comma-separated combination>What changes on each hop (case-insensitive, comma-separated). intervalLocal opens a fresh local UDP socket (new source port) every interval; intervalRemote picks a new destination port / IP from remotePorts / remoteIPs every interval; perConnRemote picks one random destination once per connection. Empty or unknown values fail at config build. Replies are only read on sockets opened by an intervalLocal hop, so include it (e.g. "intervalLocal,intervalRemote").
intervalInt32Range(required)<seconds> | "<min>-<max>"Seconds between hops; a range picks a random value each time. Both ends must be at least 5 — otherwise the connection fails at dial time with invalid interval.
remotePortsPortList(keep original port)<port / range list>Destination ports to hop across, e.g. "20000-50000" or [443, "8000-9000"]. Used by intervalRemote / perConnRemote.
remoteIPs[]string(keep original IP)<IP or CIDR> | …Destination addresses to hop across. A CIDR picks a random address inside the prefix. Used by intervalRemote / perConnRemote.

Source: infra/conf/transport_finalmask.go:911-917 · pinned at v26.9.9 (52a412d)

Bandwidth syntax ​

brutalUp and brutalDown are parsed by the helper in infra/conf/transport_method.go:696-735. Accepted suffixes:

SuffixMultiplier
(empty), b, bps1
k, kb, kbps1024
m, mb, mbps1 048 576
g, gb, gbps1 073 741 824
t, tb, tbps1 099 511 627 776

The numeric part is parsed as a float64 and the result divided by 8 (bytes-per-second is what the protobuf carries, but the source unit name is bps).

Examples ​

Outbound:

json
{
  "outbounds": [
    {
      "tag": "hy2-out",
      "protocol": "hysteria",
      "settings": {
        "version": 2,
        "address": "example.com",
        "port": 443
      },
      "streamSettings": {
        "network": "hysteria",
        "security": "tls",
        "tlsSettings": { "serverName": "example.com" },
        "hysteriaSettings": {
          "version": 2,
          "auth": "<password>",
          "udpIdleTimeout": 120
        },
        "finalmask": {
          "quicParams": {
            "brutalUp": "100mbps",
            "brutalDown": "300mbps"
          },
          "udp": [
            {
              "type": "udphop",
              "settings": {
                "mode": "intervalLocal,intervalRemote",
                "remotePorts": "20000-50000",
                "interval": "5-30"
              }
            }
          ]
        }
      }
    }
  ]
}

Inbound with two users and an HTTP-file masquerade:

json
{
  "inbounds": [
    {
      "tag": "hy2-in",
      "listen": "0.0.0.0",
      "port": 443,
      "protocol": "hysteria",
      "settings": {
        "version": 2,
        "clients": [
          { "auth": "<alice>", "email": "alice" },
          { "auth": "<bob>",   "email": "bob"   }
        ]
      },
      "streamSettings": {
        "network": "hysteria",
        "security": "tls",
        "tlsSettings": { "certificates": [{ "certificateFile": "/etc/ssl/cert.pem", "keyFile": "/etc/ssl/key.pem" }] },
        "hysteriaSettings": {
          "version": 2,
          "masquerade": {
            "type": "file",
            "dir": "/var/www"
          }
        }
      }
    }
  ]
}

Notes ​

  • A common gotcha: setting auth only inside settings (as if it were a username/password field). Xray reads outbound auth from streamSettings.hysteriaSettings.auth. Inbound users use settings.clients[].auth (per-user) and ignore the transport-level auth for matching.
  • congestion, up, down and udphop are not hysteriaSettings fields. Xray does not reject unknown keys, so values placed there are silently ignored. Set them with finalmask.quicParams.congestion / brutalUp / brutalDown and a udphop entry in finalmask.udp.
  • Xray-core implements Hysteria 2 only: version must be 2, and any other value is a hard failure.
  • udpIdleTimeout < 2 or > 600 triggers a startup error (infra/conf/transport_method.go:764-766).

Cross-core notes ​

  • sing-box uses a single, much flatter block — no transport split. Bandwidth is in plain int Mbps (no unit string), and masquerade supports a polymorphic shape (string URL or typed object). See Hysteria2 — sing-box.
  • mihomo is also single-block, with port hopping driven by ports (range syntax) plus hop-interval. mihomo accepts unit-suffixed strings for up/down, like Xray's brutalUp/brutalDown. See Hysteria2 — mihomo.

Source: infra/conf/hysteria.go:13-43 · v26.9.9 (52a412d)

Core Tutorial by Argsment