Hysteria 2
Hysteria 2 is a proxy protocol that runs over QUIC, so it travels on UDP instead of TCP. To anyone without a credential, a Hysteria 2 server looks like an HTTP/3 web server: it completes a TLS 1.3 handshake with ALPN h3 and answers requests with an ordinary page. A client proves itself with one HTTP/3 request per QUIC connection, and after that every proxied TCP connection is a QUIC stream and every UDP packet a QUIC datagram on that same connection.
etemenanki-app implements both sides:
- as an inbound (
protocol = "hysteria2"under[[inbound]]), it serves Hysteria 2 clients, including the upstream reference client, on a UDP port; - as an outbound (
protocol = "hysteria2"under[[outbound]]), it is a Hysteria 2 client that carries TCP and UDP flows to a Hysteria 2 server.
The protocol names hysteria and hy2 are accepted as aliases, in lower case only. Both mean Hysteria 2; there is no Hysteria 1 support. katana’s Hysteria 2 nodes use the same listener, but take their settings from katana’s own node configuration, which the katana guide describes.
At a glance
Section titled “At a glance”| Inbound | Outbound | |
|---|---|---|
| Carried over | QUIC on one UDP port | QUIC to one UDP port |
| TLS | rustls, TLS 1.3 only, ALPN h3, keys in [inbound.settings] |
rustls, keys in [outbound.settings] |
| Credentials | one shared password, or a users table sent as user:pass |
one password string |
| TCP | yes | yes |
| UDP | only with udp = true (off by default) |
yes, if the server relays UDP |
| Obfuscation | salamander |
salamander |
Transport ([.stream]) |
refused | refused |
| Unix socket listener | refused | not applicable |
| Balancer member | not applicable | refused |
How a connection works
Section titled “How a connection works”sequenceDiagram
participant C as Client
participant S as Hysteria 2 inbound
participant O as Routed outbound
C->>S: QUIC handshake, TLS 1.3, ALPN h3
C->>S: HTTP/3 POST /auth with the Hysteria-Auth header
alt Credential matches
S-->>C: Status 233, and whether UDP is relayed
C->>S: One QUIC stream per TCP connection
S->>O: Route and dial each target
C->>S: UDP packets as QUIC datagrams, if udp = true
else Wrong credential, or any other request
S-->>C: The masquerade response, 404 by default
end
Authentication happens once per QUIC connection, not once per proxied connection. A request that is not a valid authentication, including one with a wrong credential, gets exactly the same answer as a request for any other URL: the masquerade response.
A working example
Section titled “A working example”This pair runs a Hysteria 2 server with one shared password and UDP enabled, and a local client that offers a SOCKS5 port and sends everything through the server.
[[inbound]]tag = "hy2-in"protocol = "hysteria2"listen = "0.0.0.0" # the default is 127.0.0.1port = 443 # a UDP port
[inbound.settings]cert_file = "/etc/etemenanki/cert.pem"key_file = "/etc/etemenanki/key.pem"password = "replace-with-a-long-random-password"udp = true
[[outbound]]tag = "direct"protocol = "freedom"[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "hy2-out"protocol = "hysteria2"server = "proxy.example.com"port = 443
[outbound.settings]password = "replace-with-a-long-random-password"To bring the pair up:
-
Get a certificate for the server’s name. A certificate from a public CA, such as Let’s Encrypt, works with every client without extra settings. It must carry a
subjectAltNamefor the name clients connect to; see Certificates. -
Replace the password in both files with the same random value, for example the output of
openssl rand -base64 24. -
Check both files.
--testreads and parses the certificate and key, so it catches a missing file or a key that does not match the certificate.Terminal window etemenanki-app --test -c server.tomletemenanki-app --test -c client.toml -
Open the UDP port on the server’s firewall and in any cloud security group. A rule for TCP 443 does not cover it. With ufw, for example:
Terminal window ufw allow 443/udp -
Start the server, then the client, and send a request through the client’s SOCKS port:
Terminal window curl -x socks5h://127.0.0.1:1080 https://example.com/
The client’s outbound connects lazily: nothing is dialled until the first flow is routed to it, so a wrong password or certificate shows up in the client’s log at the first request, not at startup. The Hysteria 2 recipe builds a complete deployment with per-user credentials and obfuscation.
Inbound settings
Section titled “Inbound settings”The inbound uses the common [[inbound]] keys (tag, listen, port, sniffing), described on Inbounds. Its own keys go in [inbound.settings]:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
cert_file | path | yes | — | PEM certificate chain the QUIC listener presents, leaf first. It is read and parsed with rustls when the config is built, so --test catches a missing or unreadable file. The listener itself accepts a certificate without a subjectAltName, but rustls-based clients (including the hysteria2 outbound) and current upstream clients refuse one, so give it a DNS or IP SAN. |
key_file | path | yes | — | PEM private key for cert_file (PKCS#8, PKCS#1 or SEC1). It may be the same file as cert_file. Missing either key gives hysteria2 needs both cert_file and key_file. |
password | string | depends | — | One credential shared by every client. Set exactly one of password and users. It must not be empty, and should be printable ASCII, because a credential with other characters never matches. The client sends it unchanged as its auth string. |
users | array of tables | depends | — | Per-user credentials, { user, pass, email }. Set exactly one of password and users; an empty list counts as unset. The client sends user:pass as its auth string. |
users[].user | string | yes | — | User name. Must be non-empty, should be printable ASCII, and must not contain :, which separates name and password on the wire. It is compared case-insensitively (ASCII), so two names that differ only in case are refused. |
users[].pass | string | yes | — | The user's password. Must be non-empty and should be printable ASCII. Compared exactly, and it may contain :, because the server splits the credential at the first colon. |
users[].email | string | no | "" | A label carried with the user's flows as their user name. When empty, user is used instead. It is never sent on the wire; etemenanki-app has no route rule that reads it. |
udp | bool | no | false | Relay UDP over QUIC datagrams as well as TCP. Off by default, unlike the socks inbound. The server tells each client whether UDP is available when it authenticates. |
udp_idle_timeout | u64 | no | 60 | Seconds a UDP association may stay silent in both directions before it is closed. Accepted range is 2 to 600. Setting it while udp is false is an error, not a no-op. |
max_connections | integer | no | 4096 | Concurrent QUIC connections this listener serves. A client beyond the limit has its handshake refused at once. Must be at least 1. |
max_circuits | integer | no | 65536 | Live circuits across the whole listener, where each TCP stream and each UDP association counts as one. A stream beyond the limit is reset and a new association is dropped. Must be at least 1. |
obfs | string (enum) | no | — | Packet obfuscation beneath QUIC. The only accepted value is salamander, exact and lowercase; anything else fails with unknown obfs. Clients must use the same obfs and obfs_password. |
obfs_password | string | depends | — | Pre-shared key for salamander, at least 4 bytes. Required when obfs is set; setting it without obfs is an error, so a typo in obfs cannot silently turn obfuscation off. |
masquerade | table | no | — | The HTTP/3 response given to any request that is not a valid authentication, including a wrong credential. See the masquerade table below. |
Unknown keys are rejected. A misspelling such as obfuscation = "salamander" fails with invalid settings: unknown field and a list of the accepted keys, instead of starting a server without obfuscation.
A server with two users, UDP, obfuscation, lower limits and an HTML masquerade page:
[[inbound]]tag = "hy2-in"protocol = "hysteria2"listen = "0.0.0.0"port = 443
[inbound.settings]cert_file = "/etc/etemenanki/cert.pem"key_file = "/etc/etemenanki/key.pem"users = [ { user = "alice", pass = "replace-with-a-long-random-password", email = "alice@example.com" }, { user = "bob", pass = "replace-with-another-long-random-password" },]udp = trueudp_idle_timeout = 120max_connections = 2048max_circuits = 32768obfs = "salamander"obfs_password = "replace-with-a-shared-obfs-key"
[inbound.settings.masquerade]status = 404body = "<html><body><h1>Not Found</h1></body></html>\n"content_type = "text/html; charset=utf-8"
[[outbound]]tag = "direct"protocol = "freedom"The listener is UDP
Section titled “The listener is UDP”The inbound binds a UDP socket on listen and port. That has a few consequences:
listendefaults to127.0.0.1, as for every inbound. A server that clients reach over the network needslisten = "0.0.0.0"or an explicit address.- The port is a UDP port. It does not collide with a TCP inbound on the same number, so a Hysteria 2 inbound on UDP 443 can run next to a TLS-based inbound on TCP 443.
- A Unix socket path is refused:
hysteria2 listens on UDP and cannot use a unix socket. - There is no
[inbound.stream]. QUIC carries its own TLS, and the certificate goes in[inbound.settings]. An[inbound.stream]block with anynetworkother thantcpor anysecurityother thannonefails withprotocol hysteria2 does not support stream network "…"or… stream security "…". The check reads only those two keys: an[inbound.stream.tls]table passes--testand has no effect, so put the certificate in[inbound.settings].
Certificates
Section titled “Certificates”cert_file and key_file are required, and there is no plaintext mode. Both are PEM; the key may be PKCS#8, PKCS#1 (RSA) or SEC1 (EC), and may sit in the same file as the certificate. The listener speaks TLS 1.3 only and negotiates ALPN h3.
The certificate is loaded when the config is built, so --test and a reload both fail on a broken one:
| Problem | Error |
|---|---|
| Either key missing | hysteria2 needs both cert_file and key_file |
| File does not exist | No such file or directory (os error 2) (the message does not name the file) |
No PEM certificate in cert_file |
hysteria2: the certificate file contains no certificates |
No PEM private key in key_file |
hysteria2: the key file contains no private key |
| Key belongs to another certificate | hysteria2: certificate and key do not match: … |
Relative paths are resolved against the working directory of the process, not against the config file’s directory.
Credentials: password or users
Section titled “Credentials: password or users”Set exactly one of password and users. Neither fails with hysteria2 needs password or users, and both fail with password and users cannot both be set; a credential would have two answers. An empty users = [] counts as unset.
The protocol carries a single string, the client’s auth value. How the server reads it depends on which key you set:
| Inbound has | Client sends | Matching |
|---|---|---|
password = "s3cret" |
s3cret |
The whole string must equal password. |
users = [{ user = "alice", pass = "s3cret" }] |
alice:s3cret |
Split at the first :. The name is compared case-insensitively, the password exactly. |
Because the split is at the first colon, a user name cannot contain :, but a password can. So Alice:pa:ss authenticates as user alice with password pa:ss. The rules for a users table are checked when the config is built:
| Rule | Error |
|---|---|
user and pass present in every entry |
invalid settings: missing field pass … |
| Neither is empty | hysteria2: a user needs both a name and a password |
No : in user |
hysteria2: a username cannot contain ':' — it separates the two on the wire |
| Names unique after lower-casing | hysteria2: two users share a name once lower-cased |
A shared password must not be empty either: hysteria2: the password must not be empty.
Keep user names and passwords to printable ASCII. The inbound reads the Hysteria-Auth header only when it holds visible ASCII characters and spaces; any other character, such as a non-ASCII letter, makes the credential read as empty, so it never matches. The config check does not catch this.
Each user’s email, or the user name when email is empty, travels with that user’s flows as a label. etemenanki-app has no route rule that matches on it. Flows authenticated with a shared password carry an empty label.
UDP relay
Section titled “UDP relay”UDP is off by default. With udp = false, the server tells each client at authentication that it does not relay UDP, and ignores any datagrams it receives.
With udp = true, the client sends each packet as a QUIC datagram tagged with a session ID it chooses. The inbound opens one association per session and routes each packet on its own, so different packets of one session can take different outbounds, as with every other UDP inbound. The protocol has no message that closes a session; the inbound closes it after udp_idle_timeout seconds without a packet in either direction, checked once per second.
udp_idle_timeout accepts 2 to 600 seconds and defaults to 60. It is refused unless udp = true:
| Setting | Error |
|---|---|
udp_idle_timeout = 1 or 601 |
udp_idle_timeout must be between 2 and 600 seconds |
udp_idle_timeout with udp false or missing |
udp_idle_timeout is set but udp is not enabled |
Packets larger than a QUIC datagram are split into fragments and reassembled on the other side, in both directions. A reassembled packet may be at most 4096 bytes, the same limit the upstream implementation uses.
Obfuscation (Salamander)
Section titled “Obfuscation (Salamander)”obfs = "salamander" wraps every QUIC packet on the wire: each packet gets a random 8-byte salt and is XORed with a keystream derived from obfs_password and that salt. Without the key, the traffic no longer looks like QUIC. It is obfuscation, not encryption; QUIC’s own TLS still protects the contents. With obfs on, a client that does not use the same key cannot even complete the QUIC handshake, so the masquerade page is no longer reachable either.
Both ends must use the same obfs_password. The checks are the same on the inbound and the outbound, and they fail closed, so a typo never turns obfuscation off:
| Setting | Result |
|---|---|
No obfs, no obfs_password |
No obfuscation. |
obfs = "salamander", obfs_password of 4 bytes or more |
Salamander with that key. |
obfs = "salamander" with a shorter or missing obfs_password |
obfs_password must be at least 4 bytes for salamander |
obfs_password without obfs |
obfs_password is set but obfs is not; did you mean obfs = "salamander"? |
Any other obfs, including "Salamander" or "" |
unknown obfs "Salamander" (expected "salamander") |
A client with the wrong key, or without obfuscation against an obfuscated server, gets no answer at all. It fails with a timeout rather than an authentication error.
Masquerade
Section titled “Masquerade”The masquerade is the fixed HTTP/3 response that the inbound gives to every request that is not a successful authentication: a wrong credential, a missing one, or a browser asking for /. By default it is Go’s plain 404 page not found, the same answer an upstream server gives when no masquerade is configured.
Set it in [inbound.settings.masquerade]. Any key you leave out keeps its default:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
status | u16 | no | 404 | HTTP status code of the response. Any code from 100 to 999 is accepted except 233, which is the Hysteria authentication success status and would tell the prober it had authenticated. Anything below 100 or above 999 fails with is not an HTTP status code. |
body | string | no | "404 page not found\n" | Response body, sent as is with a matching Content-Length. The default is byte for byte what Go's http.NotFound returns. |
content_type | string | no | "text/plain; charset=utf-8" | Value of the Content-Type response header. |
The server example above serves a small HTML page with status 404.
status = 233 is refused with hysteria2: 233 is the authentication success status and cannot be used for the masquerade: 233 is the status a Hysteria 2 server answers a good credential with, so serving it to a prober would tell it that it had authenticated. The masquerade is a fixed response only; there is no option to serve files or to proxy another web site.
Connection and circuit limits
Section titled “Connection and circuit limits”Two limits protect the server, and both must be at least 1 (max_connections must be at least 1, max_circuits must be at least 1):
| Key | Default | Counts | Beyond the limit |
|---|---|---|---|
max_connections |
4096 | QUIC connections on this listener, authenticated or not | The new connection’s handshake is refused. |
max_circuits |
65536 | TCP streams plus UDP associations, across all connections on this listener | A new stream is reset; a new association’s packets are dropped. |
A circuit holds its slot for its whole life, until the relay ends or the association times out. Two further bounds are fixed: each client connection may have at most 1024 streams open at once (QUIC flow control makes the client wait for more), and at most 256 UDP associations.
When the proxy reply is sent
Section titled “When the proxy reply is sent”For each proxied TCP connection, the inbound normally dials the target first and then answers the client. The client learns that a target is unreachable from a refusal, instead of from a stream that opens and then ends.
The exception is a request that names an IP address while sniffing is on, which is the default. A client sends nothing before it gets the reply, so the inbound answers “connected” at once, reads a TLS server name or HTTP Host from the first bytes (up to 4 KiB, for at most 300 ms), and uses that domain for routing. A failed dial then shows up as a stream that closes. Set sniffing = false on the inbound if you prefer the refusal to the sniffed domain.
Routing
Section titled “Routing”Flows from this inbound carry the inbound’s tag for inbound_tag rules. A source_cidr rule matches the address of the client that opened the QUIC connection, not the listener’s address, although one UDP socket serves every client. TCP streams match network = "tcp" and relayed packets network = "udp".
Outbound settings
Section titled “Outbound settings”The outbound needs the common server and port keys: the Hysteria 2 server’s name or address, and its UDP port. missing server and missing port are build errors. address_family controls how the server’s name is resolved, and the name goes through the resolver configured in DNS. The rest goes in [outbound.settings]:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
password | string | yes | — | The credential sent in the Hysteria-Auth header once per QUIC connection. Must not be empty. For a server with a users table, write it as user:pass. |
server_name | string | no | — | TLS server name (SNI) and the name the certificate is verified against. Defaults to the outbound's server. Set it when server is an IP address and the certificate names a domain. |
allow_insecure | bool | no | false | Accept any server certificate without checking its chain or name. Cannot be combined with ca_file. Use it only for testing. |
ca_file | path | no | — | PEM file of extra CA certificates, added to the system trust store rather than replacing it. The file is read by --test, but its contents are only parsed at the first connection, so a file with no certificates fails then with hysteria2: the CA file contains no certificates. |
obfs | string (enum) | no | — | Packet obfuscation beneath QUIC. The only accepted value is salamander; anything else fails with unknown obfs. Must match the server. |
obfs_password | string | depends | — | Pre-shared key for salamander, at least 4 bytes, identical to the server's. Required when obfs is set; setting it without obfs is an error. |
max_concurrent_streams | integer | no | 102400 | TCP flows this outbound may have open at once on its single shared connection, for all users together. A flow beyond the limit fails at once with connection is at its concurrent-stream limit. Must be at least 1; there is no upper bound. |
Unknown keys are rejected. There are no bandwidth keys: up = "100 mbps" fails with invalid settings: unknown field up.
[[outbound]]tag = "hy2-out"protocol = "hysteria2"server = "203.0.113.10"port = 443
[outbound.settings]password = "alice:replace-with-a-long-random-password"server_name = "proxy.example.com" # the name on the certificateobfs = "salamander"obfs_password = "replace-with-a-shared-obfs-key"One shared connection
Section titled “One shared connection”Every flow routed to a hysteria2 outbound shares a single QUIC connection. Each TCP flow opens a stream on it and each UDP flow a session on its datagram channel; the credential is sent once, when the connection is made. This is how the protocol works. The vmess, vless, trojan and shadowsocks outbounds, by contrast, send their credential with every flow.
- The connection is made on the first flow, not at startup. If the server’s name resolves to several addresses, they are tried in order, with 10 seconds each for the QUIC handshake and the authentication.
- While the connection is up, keep-alives every 10 seconds hold it open.
- When the connection drops, the next flow reconnects. After a failed attempt, or a connection that lasted less than 10 seconds, the outbound waits before trying again: 2 seconds, doubling to at most 30. Flows routed to it meanwhile fail at once with
hysteria2: connection is down, waiting before the next attempt. max_concurrent_streamscaps the TCP flows open on the connection at once, for all users together. A flow beyond it fails at once. UDP sessions are capped separately at 256.
Certificate verification
Section titled “Certificate verification”The outbound verifies the server’s certificate with rustls against the system trust store. The name it checks is server_name, or server when server_name is not set. When that name is a DNS name, it is also sent as SNI; an IP address is checked but not sent.
| You set | Trusted | Use it for |
|---|---|---|
| nothing | The system CA store | A certificate from a public CA. |
ca_file |
The system CA store plus the certificates in ca_file |
A private CA, or a self-signed certificate created as shown below. |
allow_insecure = true |
Any certificate, any name | Testing only. |
allow_insecure and ca_file cannot both be set: allow_insecure and ca_file cannot both be set. The TLS keys live in [outbound.settings], not in [outbound.stream.tls]; see No stream block.
When server is an IP address, the certificate must list that IP address, or you set server_name to a DNS name the certificate does list. Otherwise the handshake fails with certificate not valid for name "203.0.113.10".
Unless allow_insecure is set, the outbound needs a system trust store, even with ca_file. On a host without CA certificates, such as a minimal container image, every connection fails with hysteria2: no system root certificates could be loaded; install the distribution’s CA bundle (ca-certificates on Debian and Ubuntu).
--test reads ca_file but does not parse it. A file without certificates passes the check and fails at the first connection with hysteria2: the CA file contains no certificates.
UDP through the outbound
Section titled “UDP through the outbound”The outbound carries UDP flows as QUIC datagrams, fragmenting packets that do not fit. It needs a server that relays UDP: if the server said it does not, every UDP flow routed to the outbound fails, and the outbound logs one warning, hysteria2: the server does not relay UDP; datagrams routed to this outbound are dropped. Against an etemenanki-app server, that means the server’s inbound needs udp = true.
No stream block, no balancer
Section titled “No stream block, no balancer”The outbound owns its UDP socket, so it takes no [outbound.stream] transport. A network other than tcp or a security other than none fails with protocol hysteria2 does not support stream security "tls", and so on.
A hysteria2 outbound cannot be a balancer member. Balancers check their members with a TCP connect, and a Hysteria 2 server only listens on UDP, so the member would always look down. The config is refused with balancer pool: outbound hy2-out has no upstream a TCP health probe can reach, so it cannot be balanced.
Using the upstream Hysteria client
Section titled “Using the upstream Hysteria client”The inbound works with the upstream Hysteria 2 client and other clients that follow the protocol specification. This client config matches the inbound with a users table and Salamander described above:
server: proxy.example.com:443auth: alice:replace-with-a-long-random-passwordobfs: type: salamander salamander: password: replace-with-a-shared-obfs-keytls: sni: proxy.example.comsocks5: listen: 127.0.0.1:1080For a server with a shared password, auth is that password alone. For a self-signed test certificate, add insecure: true under tls.
Differences from upstream Hysteria
Section titled “Differences from upstream Hysteria”| Area | Upstream Hysteria 2 | etemenanki-app |
|---|---|---|
| Congestion control | Brutal, which sends at a fixed rate, when bandwidth is set; otherwise a configurable controller, BBR by default |
No bandwidth settings. The outbound reports its receive rate as unknown, the inbound always answers auto, and etemenanki-app uses quinn’s default congestion control (Cubic) on both sides. Because of the auto answer, an upstream client ignores its own bandwidth setting against this server and uses its configured controller instead of Brutal. |
| Port hopping | The client can hop across a port range | The outbound dials one server and port. |
| Fast open | Optional in the client | Not implemented: the outbound waits for the server’s reply before relaying. |
| Authentication | password, userpass, HTTP and command back ends |
password or a users table (userpass). |
| Masquerade | Serve files, proxy a site, or a fixed string | A fixed status, body and content_type. |
| QUIC and TLS stack | quic-go and Go’s TLS | quinn and rustls. |
Fixed limits and timeouts
Section titled “Fixed limits and timeouts”These values are built in and cannot be configured:
| What | Value |
|---|---|
| QUIC idle timeout, both sides | 30 s |
| Client keep-alive interval | 10 s |
| Stream receive window / connection receive window | 8 MiB / 20 MiB |
| Open streams per client connection (inbound) | 1024 |
| UDP associations per connection (both sides) | 256 |
| Largest UDP packet after reassembly | 4096 bytes |
| UDP idle check interval (inbound) | 1 s |
| Outbound connect attempt, per resolved address | 10 s |
| Outbound opening one stream | 5 s |
| Outbound reconnect backoff | 2 s, doubling to 30 s |
The general limits on Limits apply on top of these.
Hot reload
Section titled “Hot reload”Any change to the config file rebuilds the whole generation, as described in Hot reload. For a Hysteria 2 inbound that means every QUIC connection is closed. The old listener waits up to a few seconds for the UDP port to be released before the new one binds it, so the new generation can reuse the same port. Clients reconnect on their own. A config that fails to build is logged and the running generation is kept.
Common errors
Section titled “Common errors”Build errors, reported by --test, at startup and on reload:
| Error | Cause and fix |
|---|---|
hysteria2 needs both cert_file and key_file |
The inbound has no certificate or no key. There is no plaintext mode. |
hysteria2 needs password or users |
Set password, or a non-empty users table. |
password and users cannot both be set; a credential would have two answers |
Keep one of the two. |
hysteria2 listens on UDP and cannot use a unix socket |
listen is a path. Use an IP address. |
port is required |
The inbound has no port. |
protocol hysteria2 does not support stream network "ws" |
Remove the [inbound.stream] or [outbound.stream] block. |
unknown protocol "Hysteria2" |
Protocol names are case-sensitive: hysteria2, hysteria or hy2. |
invalid settings: unknown field … |
A misspelled key in [inbound.settings], [inbound.settings.masquerade] or [outbound.settings]. The message lists the accepted keys. |
hysteria2 password must not be empty |
The outbound’s password is "". |
max_concurrent_streams must be at least 1 |
Leave the key out for the default, or set a positive value. |
Run-time errors from the outbound, logged as hysteria2: connect failed: hysteria2: no address answered (…) with the reason inside the parentheses:
| Reason | Meaning |
|---|---|
hysteria2 authentication rejected with status 404 Not Found |
The server did not accept the credential; the status is its masquerade status. Check password, and use user:pass for a server with a users table. |
invalid peer certificate: UnknownIssuer |
The certificate is not signed by a trusted CA. Set ca_file, or use a certificate from a public CA. |
invalid peer certificate: certificate not valid for name "…" |
server_name (or server) is not on the certificate. |
invalid peer certificate: Other(OtherError(CaUsedAsEndEntity)) |
The server’s certificate is a CA certificate. Recreate it with basicConstraints=critical,CA:FALSE. |
timed out |
No QUIC answer within 10 seconds: the UDP port is blocked, the server is down, or the obfs settings differ between the two ends. |