Monitoring

VPN monitoringUptime monitoringIntegrations

Platform

AI assistantsAPI and CLISecurity

More

PricingFree testerDocsBlog Log in

Guides

Hysteria2 protocol explained: QUIC, auth, Brutal, Salamander

Hysteria2 is a TCP and UDP proxy that runs over QUIC. To anyone without the password the server looks like an ordinary HTTP/3 web server: a real client proves itself with one HTTP/3 request to /auth, and only a correct password gets status 233 back. On top of QUIC it adds its own congestion control (Brutal), optional Salamander obfuscation and port hopping, all explained below against the official docs for Hysteria 2.13.0.

ST Muntaha11 min read

This post follows the official documentation at v2.hysteria.network and the source code of release 2.13.0, which shipped in October 2026. Where the docs say something is an internal detail that may change, I say so too. Every config below is in the format the current release reads.

What is Hysteria2?

Hysteria2 (also written Hysteria 2 or hy2) is the second major version of Hysteria, an open-source proxy. The code lives at github.com/HyNetworks/hysteria; the older apernet/hysteria address redirects there. One binary contains both the server and the client.

Version 2 is a new protocol, not an update of version 1. The docs say clearly that Hysteria 2 is not compatible with Hysteria 1.x, so the client and the server must both run 2.x. The specification calls the wire format "v4" internally, which confuses people who read the source, but every 2.x release speaks it.

The job is simple. The client takes TCP connections and UDP packets from your apps, sends them to the server over one encrypted QUIC connection, and the server connects to the real destination. On the client side you can use SOCKS5, an HTTP proxy, TCP and UDP port forwarding, TPROXY or a TUN interface. Many other apps speak the protocol as well: sing-box, Mihomo (Clash Meta), Xray-core, NekoBox, v2rayN, Hiddify, Shadowrocket and Streisand.

Why does Hysteria2 use QUIC and UDP?

Hysteria2 is built on standard QUIC (RFC 9000) with the unreliable datagram extension (RFC 9221). Each proxied TCP connection becomes its own QUIC stream, and each UDP packet travels as a QUIC datagram. Three things follow from that choice.

  1. One connection carries everything. Many TCP connections share one QUIC connection, and a lost packet on one stream does not stall the others.
  2. Congestion control lives in user space. With TCP, the kernel decides how fast to send. With QUIC, the application decides, so Hysteria can ship its own algorithm. Brutal, described below, exists because of this.
  3. QUIC is the transport of HTTP/3. A Hysteria2 connection can look like a browser talking to a web server, and the server can act like one.

There are two costs. First, everything depends on UDP: a network that blocks or slows UDP breaks Hysteria2, even if TCP 443 works fine. Second, QUIC in user space needs more CPU than kernel TCP. The performance page of the docs warns that very low-power devices and cheap CPU-throttled VPS plans can become the bottleneck.

The default listen address is :443, on UDP, because 443 is the HTTP/3 port.

How does Hysteria2 authentication work?

After the QUIC and TLS handshake, the client sends one HTTP/3 request. The protocol specification defines it like this:

:method: POST
:path: /auth
:host: hysteria
Hysteria-Auth: [string]
Hysteria-CC-RX: [uint]
Hysteria-Padding: [string]

Hysteria-Auth carries the credentials. Hysteria-CC-RX is the client's maximum receive rate in bytes per second (0 means unknown). Hysteria-Padding is random filler that hides the size of the request.

If the credentials are right, the server answers with a status code no real website uses:

:status: 233 HyOK
Hysteria-UDP: [true/false]
Hysteria-CC-RX: [uint/"auto"]
Hysteria-Padding: [string]

From then on, the QUIC connection is a proxy connection. If the credentials are wrong, the server must behave like a normal web server that does not understand the request. With no masquerade configured, that means a 404. The client treats any status other than 233 as a failure and reports authentication error, HTTP status code: 404.

The server supports four auth types: password (one shared password), userpass (a map of users, where the client sends username:password), http (the server POSTs addr, auth and tx to your backend, which must reply HTTP 200 with {"ok": true, "id": "..."}), and command (the server runs a program and reads the client ID from its output). One trap: sing-box has no userpass alias, so a sing-box client talking to an official server with userpass must put username:password in its password field.

The server writes client connected with the client's ID only after a successful login. A failed login is not logged at the default info level, because to the server it is just another web request.

What does the masquerade do?

The masquerade decides what the server shows to anyone who is not a valid client: active probes, curious users, a browser. There are three types:

  • file serves a directory of static files.
  • proxy acts as a reverse proxy for another website.
  • string always returns a fixed body, status code and headers.

The docs recommend serving real content, so that probes see a normal site. If you only want the protocol and censorship is not a concern, you can leave the section out and every HTTP request gets a 404.

To check what a prober sees, use a browser forced onto QUIC, as the docs describe (chrome --origin-to-force-quic-on=vpn.yourdomain.com:443, with Chrome fully closed first). If your curl build lists HTTP3 under curl -V, this works too:

curl --http3-only -sI https://vpn.yourdomain.com/

The listenHTTP and listenHTTPS options add normal TCP web servers on 80 and 443, because real HTTP/3 sites also serve TCP. The docs say there is no evidence that firewalls use a missing TCP site to detect Hysteria, so this is optional.

What is Brutal congestion control, and when does it hurt?

Brutal is Hysteria's own congestion control. It sends at a fixed rate that you set, and it does not slow down when packets are lost or when latency rises. If it falls short of the target, it measures the loss rate and sends faster to make up for it. On a congested, best-effort network, that is how it takes bandwidth that loss-based algorithms give up.

Hysteria2 has three controllers, and the bandwidth section decides which one each direction uses:

Controller When it is used Behaviour
BBR Default when no bandwidth is set for that direction Estimates bandwidth and RTT; needs no settings. Profiles: standard, conservative, aggressive
Reno When you set congestion.type: reno The quic-go default; simpler and more cautious than BBR
Brutal When the client sets a bandwidth for that direction Fixed target rate with loss compensation

The negotiation goes in this order. If the server has ignoreClientBandwidth: true, both directions use the non-Brutal controller. Otherwise, a direction where the client set a value uses Brutal, at the lower of the client value and the server limit. A direction with no client value uses BBR (or Reno). Server-side bandwidth values are per-client limits and only apply to Brutal. Remember that the server's upload is the client's download.

# client: Brutal in both directions
bandwidth:
  up: 30 mbps
  down: 150 mbps
# server: cap each client, or uncomment the last line to ignore client values
bandwidth:
  up: 500 mbps
  down: 500 mbps
# ignoreClientBandwidth: true

Units are bps, kbps, mbps, gbps and tbps, in base 1000.

Brutal hurts in two cases. The first and most common is a value higher than the real path can carry. The extra packets are lost, Brutal reads the loss as a reason to send even faster, and the link fills with packets that never arrive. The docs are blunt: set it too high and you get "a slow, unstable connection and wasted data". A value a little under a real speed test on that network is safe, and a value below the link speed simply acts as a limiter. The second case is a shared server where users pick their own numbers; ignoreClientBandwidth or server limits protect everyone else. Since 2.10.0, bandwidth.disableLossCompensation: true turns off the compensation part, which the changelog says can improve stability in some cases. When in doubt, leave bandwidth out and let BBR work.

What does Salamander obfuscation hide?

By default, Hysteria2 traffic looks like HTTP/3. Salamander is for networks that block QUIC or HTTP/3 specifically, but still let other UDP through. It wraps every QUIC packet like this:

[8 bytes] Salt
[bytes]   Payload

For each packet, the sender picks a random 8-byte salt, computes BLAKE2b-256(key + salt) with the shared password as the key, and XORs the payload with that hash. Without the password, nothing inside is readable: not the QUIC header, and not the TLS ClientHello with its server name, which plain QUIC exposes to anyone who reads the first packets.

The cost is the disguise. The server config reference states that with obfuscation enabled, the server is no longer compatible with standard QUIC and no longer works as a valid HTTP/3 server. The masquerade is useless at that point, because a prober cannot even start a QUIC handshake. You trade "looks like HTTP/3" for "looks like random UDP".

# same block on the server and every client
obfs:
  type: salamander
  salamander:
    password: 7Hc2pQx9LmVr4sKe

A wrong obfuscation password does not produce an error. The server drops the packets, and the client times out exactly as if the server were down.

Release 2.9.2 added an experimental second type, Gecko. It runs on top of Salamander and also splits QUIC handshake packets into 2 to 8 pieces of random size with random padding, so the handshake shape is less obvious. Clients need support for it too: sing-box from 1.14.0, and current Mihomo.

How does port hopping work?

Port hopping spreads one connection over many UDP ports. The port hopping page explains why: some ISPs block or throttle a long-lived UDP flow, but only on the port in use.

On the client, write the server address with several ports or ranges:

vpn.yourdomain.com:20000-50000
vpn.yourdomain.com:443,5000-6000,7044

The client picks a random port, then moves to another one every 30 seconds by default. You can set a fixed interval or a random one:

transport:
  udp:
    minHopInterval: 15s
    maxHopInterval: 45s

On a Linux server, put the range in listen (listen: :20000-50000). The server listens on the first port and adds nftables or iptables rules that redirect the rest to it, then removes them on shutdown. This needs nft or iptables on the system and root or CAP_NET_ADMIN. On other systems, or if you prefer, write the redirect yourself:

iptables -t nat -A PREROUTING -i eth0 -p udp --dport 20000:50000 -j REDIRECT --to-ports 443

Port hopping does nothing if the network blocks UDP as a whole. For that case, 2.12.0 added mimic, which disguises the connection as TCP (Linux only, both sides must enable it). Mimic and port hopping cannot be used together.

What does a minimal Hysteria2 server and client config look like?

On a Linux server with systemd, the official script installs the binary to /usr/local/bin/hysteria, creates hysteria-server.service running as the user hysteria, and reads /etc/hysteria/config.yaml:

bash <(curl -fsSL https://get.hy2.sh/)

A minimal server with a free ACME certificate. The domain must already point at the server:

listen: :443

acme:
  domains:
    - vpn.yourdomain.com
  email: [email protected]

auth:
  type: password
  password: 9f2kQ7xLm4Rv8TzW

masquerade:
  type: proxy
  proxy:
    url: https://yourdomain.com/
    rewriteHost: true
systemctl enable --now hysteria-server.service
journalctl --no-pager -e -u hysteria-server.service

Look for server up and running in the log.

A minimal client. With no bandwidth section it uses BBR:

server: vpn.yourdomain.com:443
auth: 9f2kQ7xLm4Rv8TzW

tls:
  sni: vpn.yourdomain.com

socks5:
  listen: 127.0.0.1:1080

http:
  listen: 127.0.0.1:8080

Run it with hysteria -c client.yaml and look for connected to server.

If you have no domain, hysteria cert --host vpn.yourdomain.com writes server.crt and server.key (an ECDSA P-256 key, valid for 365 days by default) and prints a sample TLS config with the pinSHA256 value. Clients then use insecure: true together with that pinSHA256. Use ECDSA or RSA certificates in general: since 2.11.0 the client imitates Chrome's QUIC handshake by default, and Chrome does not offer Ed25519, so a server with an Ed25519 certificate fails the handshake.

Recent releases also added Encrypted Client Hello (2.10.0) and Realms (2.9.0), which lets a server behind NAT accept direct connections through a rendezvous service. Neither is needed for a basic setup.

How do I read a hysteria2:// URI?

The URI scheme page defines the format:

hysteria2://[auth@]hostname[:port]/?[key=value]&[key=value]...[#name]

A real example with Salamander and port hopping:

hysteria2://[email protected]:20000-50000/?obfs=salamander&obfs-password=7Hc2pQx9LmVr4sKe&sni=vpn.yourdomain.com#hy2-fra-1
Part Example Meaning
Scheme hysteria2 or hy2 Both are valid
Auth 9f2kQ7xLm4Rv8TzW The password, or username:password for userpass; percent-encode special characters
Host and port vpn.yourdomain.com:20000-50000 Port defaults to 443; multi-port format allowed
obfs salamander Or gecko
obfs-password 7Hc2pQx9LmVr4sKe Must match the server
sni vpn.yourdomain.com TLS server name
insecure 1 Skip certificate verification
pinSHA256 ba884517a1... SHA-256 of the server's leaf certificate
ech base64 ECH config list, must match the server
Fragment #hy2-fra-1 Display name only

By design, the URI never carries bandwidth values or client modes: those depend on each user's network. To print a link from a working client config, run hysteria share -c client.yaml (add --qr for a QR code). Since 2.6.4, pinSHA256 matches only the leaf certificate, not a CA in the chain.

How do I check that a Hysteria2 server is really up?

Not with a port check. Like WireGuard, a Hysteria2 server gives nothing useful to a random UDP probe, so a "port open" result tells you nothing. I covered why in the WireGuard port monitoring post, and the same logic applies here.

These are the checks I run, from inside to outside:

  1. On the server, systemctl status hysteria-server.service and ss -ulpn | grep hysteria. You want the process active and a UDP socket on your port. The log should show server up and running, and later client connected lines with client IDs.

  2. From another machine, with a real client config, run the built-in ping. It connects, authenticates, then opens a TCP connection through the server and prints the time:

    hysteria -c client.yaml ping www.gstatic.com:443
    
  3. Send real traffic. Start the client, then:

    curl --socks5-hostname 127.0.0.1:1080 -sS -o /dev/null -w '%{http_code}\n' https://www.gstatic.com/generate_204
    

    204 means the tunnel works end to end, including the server's own DNS and outbound route.

  4. If you enabled the Traffic Stats API (trafficStats with a secret), ask the server who is online: curl -H 'Authorization: your_secret' http://127.0.0.1:9999/online.

Steps 2 and 3 are the ones that matter, because they test what users do: QUIC handshake, the /auth exchange, then a real request. Run them from a network that is not the server's own.

Full disclosure: this is what TunnelHQ does on a schedule. You paste the hysteria2:// link, and every 1 to 5 minutes (depending on plan) a checker completes the QUIC handshake and the auth exchange, then opens a stream through the server, with alerts to 20 channels when it fails. To test one link once, paste it into the free tester, no account needed. Field details are in the docs.