Hysteria 2 nodes
A Hysteria 2 node carries proxy traffic over QUIC on a UDP port. katana serves one when a node’s [node.api].node_type is Hysteria2 (also accepted: hysteria, hy2, in any case). The panel still supplies the users and receives the traffic reports, as it does for every node. The listener itself needs a few settings that no panel sends, so they live in the node’s [node.hysteria] table.
This page covers both ways to run such a node: with the panel describing it, or with katana describing it from the config file. It lists every key under [node.hysteria], what the panel must send, how an official Hysteria 2 client authenticates, and the errors you are likely to meet. For the protocol itself as etemenanki-app serves it, see Hysteria 2 in etemenanki-app.
How a Hysteria 2 node differs
Section titled “How a Hysteria 2 node differs”- It listens on UDP. katana binds
listen_ip:portas a UDP socket, so a Hysteria 2 node can share its port number with a TCP node on the same address, for example a Trojan node on TCP 443 and a Hysteria 2 node on UDP 443. - It always uses TLS. The TLS handshake is part of QUIC, so there is no plaintext mode. Every Hysteria 2 node needs
[node.controller.cert]withmode = "file", whatever the panel says about TLS. - Its user table is swapped in place. When the panel’s user list changes, katana replaces only the authenticator. The UDP socket stays bound and the remaining users stay connected.
--testcan check it. The settings under[node.hysteria]are local, sokatana --testchecks them and reads the certificate and key. For other node types,--testcannot check what the panel will send.
Two ways to describe the node
Section titled “Two ways to describe the node”The key [node.hysteria].port decides where the node’s port and obfuscation come from:
flowchart LR
A["node_type = Hysteria2"] --> B{"[node.hysteria] port"}
B -->|"0"| C["Panel node config: port, obfs"]
B -->|"non-zero"| D["[node.hysteria]: port, obfs"]
C --> L["QUIC listener"]
D --> L
E["[node.hysteria]: credential, udp, masquerade"] --> L
F["[node.controller.cert]"] --> L
G["Panel user list"] --> L
Panel-described (port = 0) |
Locally described (port non-zero) |
|
|---|---|---|
| Listening port | The panel’s port | [node.hysteria].port |
| Obfuscation | The panel’s obfs and obfs-password |
[node.hysteria].obfs and obfs_password |
Node-config request (/UniProxy/config, /mod_mu/nodes/…/info) |
Sent on every poll | Never sent |
| User list and traffic reports | From and to the panel | From and to the panel |
SSPanel’s node-level node_speedlimit |
Applied | Not read. [node.api].speed_limit still applies |
Xboard audit rules from the node’s routes |
Loaded | Not loaded; only rule_list_path applies |
credential, udp, udp_idle_timeout, masquerade |
[node.hysteria] |
[node.hysteria] |
Describe the node locally when the panel cannot describe it, for example an SSPanel installation where you do not want to use custom_config, or when you want the port and obfuscation kept in the config file. The panel must still know the node: katana asks it for the users of node_id and reports their traffic under that ID. On Xboard the lookup also uses the node type, so the node must exist there as a Hysteria node.
A locally described node
Section titled “A locally described node”This file serves one Hysteria 2 node on UDP port 443 with Salamander obfuscation and UDP relay. Users and traffic reports go through an Xboard panel.
# katana Hysteria 2 node described locally: the port and the obfuscation# come from [node.hysteria], so katana never asks the panel for the node's# config. The panel still supplies the user list and receives the traffic# reports for node_id 1.
[log]level = "info"
[[node]]panel_type = "NewV2board"
[node.api]host = "https://panel.example.com"node_id = 1key = "replace-with-the-panel-key"# Sent to the panel as node_type=hysteria2, which Xboard maps to its# "hysteria" node type.node_type = "Hysteria2"
[node.controller]listen_ip = "0.0.0.0"update_periodic = 60
# Every Hysteria 2 node needs a certificate file. Clients that verify it# also need a subjectAltName for the name they use as SNI.[node.controller.cert]mode = "file"cert_file = "/etc/katana/cert/fullchain.pem"key_file = "/etc/katana/cert/privkey.pem"
[node.hysteria]# Non-zero: this node is described here. Clients connect to UDP port 443.port = 443# The client's auth string is the user's UUID (the default).credential = "uuid"# Relay UDP as well as TCP; a quiet UDP session ends after 60 seconds.udp = trueudp_idle_timeout = 60# Salamander obfuscation. Clients need the same password.obfs = "salamander"obfs_password = "replace-with-a-long-random-password"
# What anyone without a valid credential sees.[node.hysteria.masquerade]status = 404body = "<html><body><h1>404 Not Found</h1></body></html>\n"content_type = "text/html; charset=utf-8"-
Check the file.
--testreads the certificate and key and checks every key under[node.hysteria]. It does not contact the panel.Terminal window katana --test -c /etc/katana/config.tomlA valid file prints
Configuration OKand exits with status0. An invalid one prints the reason to stderr and exits with status1:configuration error: node 1: udp_idle_timeout must be between 2 and 600 seconds -
Start katana. Once the panel has returned at least one user, the node binds its socket and logs:
INFO katana::manager::node: node 1: listening on 0.0.0.0:443With an empty user list, katana binds nothing and serves no one until users appear. If the node cannot come up, for example because the panel is unreachable or the port is taken, katana logs why and tries again. Common errors lists the messages.
-
Open the port. Allow inbound UDP 443 in the host firewall and in any cloud security group. A rule for TCP 443 does not cover it.
-
Connect a client. Give clients the user’s UUID as the auth string, the obfuscation password and the server name on the certificate. Authenticating clients has a complete client config.
To let the panel describe the same node instead, set port = 0 (or leave it out) and remove obfs and obfs_password:
[node.hysteria]port = 0udp = trueSettings
Section titled “Settings”[node.hysteria]
Section titled “[node.hysteria]”katana reads this table only when node_type is a Hysteria 2 value, and ignores it for any other node. Unknown keys are refused, so a typo such as obsf stops --test with unknown field `obsf`, expected one of `port`, `credential`, `udp`, `udp_idle_timeout`, `obfs`, `obfs_password`, `masquerade` .
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
port | u16 | no | 0 | The UDP port to listen on. A non-zero value makes this a locally described node: katana builds the node from this table and never calls the panel's node-config endpoint. 0 asks the panel for the port and the obfuscation, as for every other node type. |
credential | string (enum) | no | "" | How the client's auth string identifies a user. "" or uuid: the whole string is the user's UUID. user_pass: the string is label:uuid, split on the first colon, where the label is <uuid>@v2board.user on Xboard/V2board and the numeric user ID on SSPanel; the label is compared case-insensitively. The value itself is case-sensitive; anything else fails with unknown hysteria credential kind. |
udp | bool | no | false | Relay UDP as well as TCP. Off by default, unlike the upstream server: the listener then tells clients that UDP is disabled and relays TCP only. |
udp_idle_timeout | u64 | no | 60 | Seconds a UDP session may carry no datagrams before katana closes it. Accepted range 2 to 600. The default 60 applies only when udp = true: the key is only valid with UDP on, and setting it while udp is off fails with udp_idle_timeout is set but udp is not enabled. |
obfs | string (enum) | no | — | Packet obfuscation for a locally described node. The only accepted value is salamander, in lowercase. When port = 0 the panel's obfuscation applies and this key is ignored at run time, although --test still checks it. |
obfs_password | string | depends | — | The Salamander key shared with every client, at least 4 bytes. Required when obfs is set. Setting it without obfs is refused, so a typo cannot quietly turn obfuscation off. Same local-node rule as obfs. |
masquerade | table | no | {} | The HTTP/3 response given to every request that does not carry a valid credential, written as [node.hysteria.masquerade]. Applies to both local and panel-described nodes. |
[node.hysteria.masquerade]
Section titled “[node.hysteria.masquerade]”A Hysteria 2 server looks like an HTTP/3 web server. A request with a wrong or missing credential, or aimed at any other path, gets this fixed response, so a wrong password looks exactly like a wrong URL. Leaving the table out, or setting only some keys, gives the missing keys the defaults below. Those defaults match the reply of an unconfigured upstream server.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
status | u16 | no | 404 | The HTTP status code. Any code from 100 to 999 except 233, which is the Hysteria 2 authentication success status and would tell the prober it had authenticated. Out-of-range values fail with is not an HTTP status code. |
body | string | no | "404 page not found\n" | The response body, sent as is with a matching Content-Length. |
content_type | string | no | "text/plain; charset=utf-8" | The value of the Content-Type header. katana does not check it. A value that is not a valid HTTP header value, for example one containing a newline, makes the masquerade a bare 200 response with no headers, so keep it to printable characters. |
katana serves the masquerade only inside QUIC. It opens no TCP listener for it. With Salamander on, a client without the obfuscation password cannot finish the QUIC handshake, so it never reaches the masquerade at all.
The certificate
Section titled “The certificate”The certificate comes from the node’s [node.controller.cert], never from the panel. For a Hysteria 2 node:
| Key | Requirement |
|---|---|
mode |
Must be "file". The default "none" and the ACME modes "dns", "http" and "tls" all fail with hysteria2 node requires cert.mode = "file". |
cert_file |
PEM certificate chain, leaf first. Both paths must be set, or the node fails with TLS node requires cert.cert_file and cert.key_file. A DER file fails with hysteria2: the certificate file contains no certificates. |
key_file |
PEM private key in PKCS#8, PKCS#1 or SEC1 form, matching the certificate. |
reject_unknown_sni |
Must stay false. true fails with node requests kernel-unsupported feature: cert.reject_unknown_sni. |
The listener speaks TLS 1.3 only and offers the ALPN h3.
katana reads the files when it builds the listener. A renewed certificate saved to the same paths is not served until the listener is rebuilt, so restart katana after each renewal. Hot reload explains the alternatives.
Panel-described nodes
Section titled “Panel-described nodes”With port = 0, katana takes the port and the obfuscation from the panel on every poll. katana does not use anything else the panel sends about the node: its TLS settings, server name, bandwidth figures (up_mbps, down_mbps) and port-hopping settings have no effect on the listener. katana still compares some of these unused values between polls, though: a change to the node’s host or server_name on Xboard, or to a custom_config field such as host or path on SSPanel, rebuilds the listener at the next poll and drops every connection.
Create a node of type Hysteria in the panel and set its protocol version to 2. katana reads these fields from GET /api/v1/server/UniProxy/config:
| Field | Panel setting | katana uses it as |
|---|---|---|
server_port |
The node’s listening port (not the port shown to users) | The UDP port. 0 fails with newV2board: server port must be > 0. |
obfs |
salamander when obfuscation is on, null when off |
The obfuscation type |
obfs-password |
The obfuscation password | The Salamander key |
In the katana config, set panel_type = "NewV2board" and node_type = "Hysteria2" or "hysteria". katana sends node_type to the panel lowercased, and Xboard maps hysteria2 to its hysteria type. Xboard does not know hy2, so it rejects every request from a node configured that way.
Xboard subscription links put the user’s UUID in the Hysteria 2 password, which matches katana’s default credential.
SSPanel has no Hysteria 2 node type of its own, so katana reads the node from its custom_config JSON. The panel must be version 2021.11 or later, and [node.api].disable_custom_config must stay false. Otherwise katana uses the legacy server string, which cannot describe a Hysteria 2 node, and the node fails with sspanel: a hysteria2 node needs custom_config; the legacy server string cannot describe one.
{ "offset_port_node": "443", "obfs": "salamander", "obfs-password": "replace-with-a-long-random-password"}| Field | Meaning |
|---|---|
offset_port_node |
The UDP port, written as a JSON string. A JSON number fails with parse sspanel custom_config, and a string that is not a port number fails with invalid offset_port_node. |
obfs |
"salamander", or leave it out for no obfuscation |
obfs-password |
The Salamander key, hyphenated as UniProxy panels spell it |
A missing or null custom_config fails with custom_config is empty, disable custom config. katana does not use the other custom_config fields for a Hysteria 2 node. The node’s node_speedlimit applies as the node speed limit, unless [node.api].speed_limit is set, which replaces it.
katana checks the panel’s values when it builds the listener, with the same rules as the local keys: unknown obfs for any type other than salamander, and obfs_password must be at least 4 bytes for salamander for a short key. The local obfs and obfs_password keys have no effect on a panel-described node, although --test still checks them.
Authenticating clients
Section titled “Authenticating clients”A Hysteria 2 client sends one auth string. [node.hysteria].credential decides how katana finds the user in it:
credential |
Auth string on Xboard / V2board | Auth string on SSPanel |
|---|---|---|
"" or "uuid" (default) |
<uuid> |
<uuid> |
"user_pass" |
<uuid>@v2board.user:<uuid> |
<user id>:<uuid> |
- In
uuidmode the whole string is compared byte for byte with each user’s UUID as the panel lists it, so letter case matters. Use this mode unless your clients were set up for upstream’suserpassauthentication. - In
user_passmode katana splits the string at the first colon. The part before it is the user’s label, compared case-insensitively:<uuid>@v2board.useron UniProxy panels, and the numeric user ID on SSPanel. The part after it is the UUID. This is the upstream server’suserpassformat, so a client written for an upstream server works unchanged. - The same label identifies the user in katana’s traffic accounting in both modes.
A wrong auth string gets the masquerade response. The official client then reports the masquerade’s status code, for example authentication error, HTTP status code: 404.
server: proxy.example.com:443auth: 11111111-2222-3333-4444-555555555555obfs: type: salamander salamander: password: replace-with-a-long-random-passwordtls: sni: proxy.example.comsocks5: listen: 127.0.0.1:1080server: proxy.example.com:443auth: 11111111-2222-3333-4444-555555555555@v2board.user:11111111-2222-3333-4444-555555555555obfs: type: salamander salamander: password: replace-with-a-long-random-passwordtls: sni: proxy.example.comsocks5: listen: 127.0.0.1:1080server: proxy.example.com:443auth: 42:11111111-2222-3333-4444-555555555555obfs: type: salamander salamander: password: replace-with-a-long-random-passwordtls: sni: proxy.example.comsocks5: listen: 127.0.0.1:1080Here 42 is the user’s ID in SSPanel.
Leave out the obfs block when the node has no obfuscation. A client whose obfuscation setting does not match the node’s never completes the QUIC handshake. It reports a timeout, not an authentication error.
Behaviour details
Section titled “Behaviour details”UDP relay
Section titled “UDP relay”udp is off by default. katana then tells each client, in the authentication reply, that UDP is disabled, and relays TCP only. The upstream server enables UDP by default, so set udp = true if your users expect it. With UDP on, katana closes a UDP session that has carried no datagrams for udp_idle_timeout seconds, checked once per second.
Bandwidth and congestion control
Section titled “Bandwidth and congestion control”katana does not implement Hysteria’s Brutal congestion control. It answers every client with Hysteria-CC-RX: auto and ignores both the bandwidth a client announces and the panel’s up_mbps and down_mbps. The only speed limit a Hysteria 2 user gets is katana’s per-user token bucket, described in Speed limits.
Fixed connection limits
Section titled “Fixed connection limits”These limits apply to every Hysteria 2 node and cannot be changed:
| Limit | Value | When it is reached |
|---|---|---|
| QUIC connections per node | 4,096 | A new connection is refused as it arrives. |
| Live proxy streams and UDP sessions per node | 65,536 | A new stream or session is rejected. The client’s other streams continue. |
| Concurrent streams per QUIC connection | 1,024 | The client waits for a free stream before opening another. |
| QUIC idle timeout | 30 s | A connection that receives no packets for 30 seconds, or for the client’s own idle timeout if that is shorter, is closed. Client keep-alives count as packets. |
None of them is per user. Refusals are logged at debug level, for example hysteria2: refusing a connection; the listener is full.
Sniffing, routing and audit
Section titled “Sniffing, routing and audit”A Hysteria 2 stream goes through the same path as a flow on any other node: admission, routing, audit and metering. [node.controller].disable_sniffing = true turns off sniffing for Hysteria 2 streams too. A locally described node on Xboard never fetches the node config, so it has no audit rules from the panel’s routes; only the file named by rule_list_path applies.
Changing settings on a running node
Section titled “Changing settings on a running node”| Change | When it takes effect |
|---|---|
| The panel’s user list | At the next poll, without rebinding. Remaining users stay connected. |
The panel’s port or obfuscation (port = 0) |
At the next poll, as a listener rebuild that drops every connection |
Any key under [node.hysteria] in the file: port, credential, udp, udp_idle_timeout, obfs, obfs_password or the masquerade |
At once, as a listener rebuild that drops every connection. Setting or clearing port switches between the local and the panel description. |
[node.controller].listen_ip, [node.controller].disable_sniffing or [node.controller.cert] in the file |
At once, as a listener rebuild |
[node.api].speed_limit in the file |
At once, without rebinding. Open connections stay; flows opened after the change run at the new rate. |
A listener rebuild closes the old listener before it builds the new one. A config reload refuses unknown keys under [node.hysteria], but katana checks their values only when it builds the listener, so a saved edit that does not build, for example udp_idle_timeout = 1, leaves the node without a listener: katana logs node 1: rebuild failed: … and tries again at every poll until you fix the file. Run katana --test before you save.
Hot reload describes the whole reload process.
Common errors
Section titled “Common errors”Errors from --test and from a starting node are prefixed with the node, for example configuration error: node 1: ….
| Message | Cause | Fix |
|---|---|---|
hysteria2 node requires cert.mode = "file" |
No certificate is configured, or an ACME mode is set. | Set [node.controller.cert] with mode = "file", cert_file and key_file. |
No such file or directory (os error 2) |
The certificate or key path does not exist. | Check both paths; use absolute paths. |
hysteria2: the certificate file contains no certificates |
cert_file is not a PEM certificate. |
Point it at the PEM chain, not a DER file or the key. |
hysteria2: the key file contains no private key |
key_file holds no PEM private key, for example because it points at the certificate. |
Point it at the PEM key. |
hysteria2: certificate and key do not match: … |
key_file belongs to another certificate. |
Use the key issued with this certificate. |
obfs_password is set but obfs is not; did you mean obfs = "salamander"? |
A password without a type, locally or from Xboard with obfuscation switched off. | Add obfs = "salamander", or remove the password. |
unknown obfs "Salamander" (expected "salamander") |
Wrong spelling or case, or an Xboard node on protocol version 1. | Write salamander in lowercase; set the Xboard node to version 2. |
obfs_password must be at least 4 bytes for salamander |
The key is shorter than 4 bytes or missing. | Use a long random value, for example from openssl rand -base64 24. |
udp_idle_timeout is set but udp is not enabled |
udp_idle_timeout without udp = true. |
Set udp = true, or remove the timeout. |
udp_idle_timeout must be between 2 and 600 seconds |
The value is outside 2 to 600. |
Pick a value in range. |
unknown hysteria credential kind "UUID" (expected "uuid" or "user_pass") |
The value is not one of the accepted strings. It is case-sensitive. | Use uuid or user_pass. |
hysteria2: 233 is the authentication success status and cannot be used for the masquerade |
The masquerade status is 233. |
Use another status, such as 404. |
hysteria2: 1000 is not an HTTP status code |
The masquerade status is outside 100 to 999. |
Use a real HTTP status code. |
newV2board: server port must be > 0 or panel returned port 0 |
The panel sent port 0: Xboard’s server_port, or SSPanel’s offset_port_node. |
Set the node’s port in the panel, or describe the node locally. |
Address already in use (os error 98) |
Another process or node already holds that UDP port on listen_ip. |
Free the port or choose another. |
When a node cannot come up, katana logs why and tries again. node 1: node_info failed: …; retrying in <N>s means the panel’s node config could not be fetched or read, node 1: user_list failed: …; retrying in <N>s means the same for the user list, and node 1: initial start failed: …; retrying in <N>s means the listener could not be built. The first retry comes after 1 second, and the wait doubles after each failure up to 60 seconds, or up to update_periodic if that is shorter. Each attempt asks the panel again for the user list and, on a panel-described node, for the node config. Saving an edit to the node’s [[node]] entry retries at once, so you can fix the cause in the file without restarting katana. A rebuild that fails later, for example after the panel changes the obfuscation, logs node 1: rebuild failed: …, and katana tries again at the next poll.
On the client side:
| Client symptom | Cause |
|---|---|
authentication error, HTTP status code: 404 (or your masquerade status) |
The auth string does not match the credential mode, or the user is not in the panel’s list for this node. |
| A timeout, no authentication error | The obfuscation setting or password differs from the node’s, the UDP port is blocked, or the node is not listening. |
x509: certificate relies on legacy Common Name field, use SANs instead |
The certificate has no subjectAltName. |