跳转到内容

Hysteria 2 服务端与客户端

本示例用 etemenanki-app 搭建一套两端完整的 Hysteria 2 部署。服务端监听 UDP 443 端口,为每个用户分配独立的凭据,除 TCP 外也中继 UDP,用 Salamander 混淆隐藏 QUIC 包,并对探测者返回一个普通网页。客户端运行在你自己的机器上,为浏览器和应用提供一个 SOCKS5 端口,并把它们的流量发往服务端。同一个服务端也接受上游 Hysteria 客户端,本页后半部分介绍它的配置。

如果你想从一套能用的服务端和客户端配置起步,就读这一页。每个配置项、上限和超时的参考见 Hysteria 2 协议页;本页解释示例中各项选择的理由,以及会导致部署失败的常见错误。

flowchart LR
    A["浏览器或应用"] -->|"SOCKS5,127.0.0.1:1080"| B["客户端:socks-in"]
    B --> C["客户端:hy2-out"]
    C -->|"QUIC,UDP 443,Salamander"| D["服务端:hy2-in"]
    U["上游 hysteria 客户端"] -->|"QUIC,UDP 443,Salamander"| D
    D --> E["direct"]
    E --> F["互联网"]
    D -->|"私有地址"| G["block"]
  • 服务端有一个监听 UDP 443 端口的 hysteria2 入站、一个名为 direct 的 freedom 出站,以及一个名为 block 的 blackhole 出站,后者阻止客户端访问服务端自身所在的私有网络。
  • 客户端有一个监听 127.0.0.1:1080 的 socks 入站和一个 hysteria2 出站。除了发往私有地址的连接直接出站外,所有连接都经过这个 hysteria2 出站。
  • 同一个客户端所有被代理的 TCP 连接和 UDP 包共用到服务端的一个 QUIC 连接。客户端只在建立这个连接时认证一次。
  • 一台有公网 IP 地址的服务器,以及一个指向它的 DNS 名称。本页使用 proxy.example.com。
  • 服务器和客户端机器上都已按安装中的说明装好 etemenanki-app。
  • 一张 proxy.example.com 的 TLS 证书,其 subjectAltName 中列有该名称。由公共 CA(例如 Let’s Encrypt)签发的证书无需额外设置即可用于所有客户端。自签名证书见使用自签名证书。
  • 服务器上没有其他程序占用 UDP 443 端口。在 TCP 443 上运行的 Web 服务器没有影响,因为这是两个独立的端口;但启用了 HTTP/3 的 Web 服务器也会占用 UDP 443。

两份文件都能通过 etemenanki-app --test。使用前请替换其中的名称、两个用户的密码和混淆密钥。

/etc/etemenanki/config.toml(服务端)
# A Hysteria 2 server on UDP port 443 with two users, UDP relay, Salamander
# obfuscation and an HTML masquerade page, relaying everything directly.
# Replace the certificate paths, the passwords and the obfuscation key.
[log]
level = "info"
[[inbound]]
tag = "hy2-in"
protocol = "hysteria2"
listen = "0.0.0.0" # the default, 127.0.0.1, is unreachable from outside
port = 443 # a UDP port: open 443/udp in the firewall
[inbound.settings]
# The certificate must carry a subjectAltName for the name clients dial.
cert_file = "/etc/etemenanki/tls/fullchain.pem"
key_file = "/etc/etemenanki/tls/privkey.pem"
# Clients authenticate with the string "user:pass", for example
# "alice:replace-with-a-long-random-password".
users = [
{ user = "alice", pass = "replace-with-a-long-random-password" },
{ user = "bob", pass = "replace-with-another-long-random-password" },
]
# UDP relay is off by default.
udp = true
udp_idle_timeout = 60
# Every client needs the same obfs and obfs_password.
obfs = "salamander"
obfs_password = "replace-with-a-long-random-obfs-key"
# What a prober, a browser or a client with a wrong credential receives.
[inbound.settings.masquerade]
status = 404
body = "<html><body><h1>404 Not Found</h1></body></html>\n"
content_type = "text/html; charset=utf-8"
[[outbound]]
tag = "direct"
protocol = "freedom"
[[outbound]]
tag = "block"
protocol = "blackhole"
[route]
default = "direct"
# Keep clients away from the server's own private networks.
[[route.rule]]
outbound = "block"
cidr = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "fc00::/7", "::1/128"]
  1. 放置证书。 服务端示例读取 /etc/etemenanki/tls/fullchain.pem 和 /etc/etemenanki/tls/privkey.pem。fullchain.pem 中先放叶证书,后面跟中间证书。按照运行 etemenanki-app 中针对 systemd 服务用户的说明,把私钥的属主设为 root:etemenanki,权限设为 0640。

  2. 生成密钥。 本示例需要为每个用户生成一个密码,另外生成一个由服务端和所有客户端共用的混淆密钥。任意随机字符串都可以,例如:

    终端窗口
    openssl rand -base64 24 # 每个用户密码运行一次,obfs_password 再运行一次

    在服务端把这些值填入 users 和 obfs_password。在客户端,password 填所连接用户的 user:pass,obfs_password 原样填服务端的密钥。

  3. 检查两份文件。 --test 会构建正常启动时构建的全部内容(包括加载证书),但不绑定任何端口:

    终端窗口
    etemenanki-app --test -c /etc/etemenanki/config.toml
    Configuration OK.

    它能发现证书文件缺失、私钥与证书不匹配、配置项拼写错误,以及故障排查中列出的其他配置错误。它只单独检查每份文件,因此发现不了丢弃数据包的防火墙,也发现不了服务端和客户端之间凭据或混淆密钥不一致。

  4. 开放 UDP 443 端口。 见下文开放 UDP 端口。

  5. 启动服务端。 使用运行 etemenanki-app 中的 systemd 单元时,运行 systemctl start etemenanki。监听器启动后,日志中会出现:

    INFO etemenanki_app::instance: inbound hy2-in listening on udp 0.0.0.0:443

    此时在服务端运行 ss -ulpn 'sport = :443' 应能看到该进程。

  6. 启动客户端,并通过它发送一个请求。

    终端窗口
    etemenanki-app -c config.toml
    curl -x socks5h://127.0.0.1:1080 https://example.com/

    客户端要等第一个流到达时才连接服务端,因此凭据、密钥或证书错误会在第一次请求时出现在客户端日志中,而不是在启动时。

Hysteria 2 运行在 QUIC 之上,而 QUIC 使用的是 UDP。放行 TCP 443 的防火墙规则或云安全组,连一个 Hysteria 2 包都放不进来。请在主机防火墙以及服务器前方的所有云服务商防火墙中放行入站 UDP 443:

终端窗口
ufw allow 443/udp

端口被拦截时,客户端收不到任何回应。它的日志显示的是超时,而不是认证错误:

WARN etemenanki_protocols::hysteria::slot: hysteria2: connect failed: hysteria2: no address answered (203.0.113.10:443: timed out)

绑定 443 端口需要 root 权限或 CAP_NET_BIND_SERVICE capability,运行 etemenanki-app 中的 systemd 单元会授予该 capability。没有它时,服务端启动失败,报错 failed to start: inbound hy2-in bind udp 0.0.0.0:443 failed: Permission denied (os error 13)。

服务端入站使用入站中的通用配置项 tag、listen 和 port,Hysteria 2 自身的配置项放在 [inbound.settings] 中。下表列出本示例设置的配置项;完整列表见协议页。

配置项 类型 默认值 示例取值 原因
listen string "127.0.0.1" "0.0.0.0" 默认值只能从服务器本机访问。
port u16 无,必填 443 这是 UDP 端口,不会与 443 上的 TCP 入站冲突。
cert_file、key_file path 无,必填 /etc/etemenanki/tls/… QUIC 始终使用 TLS 1.3,没有明文模式。
users array of tables 无 两个条目 每个用户一个凭据,以 user:pass 形式发送。另一种方式是使用单个共享的 password。
udp bool false true 不开启时,服务端会告诉客户端它不中继 UDP。
udp_idle_timeout u64 60 60 UDP 会话在两个方向都静默达到这么多秒后关闭。可接受范围为 2 到 600,且仅在 udp = true 时可用。
obfs string (enum) 无 "salamander" 唯一可接受的值,必须完全一致且为小写。
obfs_password string 无 一个随机密钥 至少 4 字节,所有客户端上都必须相同。
masquerade table Go 的纯文本 404 page not found 一个 HTML 404 页面 对所有不是有效认证的请求返回的响应。

[inbound.settings] 和 [inbound.settings.masquerade] 两张表都拒绝未知配置项,因此拼写错误会使配置无法加载,而不是启动一个缺少你想要的功能的服务端。

在没有凭据的人看来,Hysteria 2 服务端就像一个 HTTP/3 Web 服务器。所有不是有效认证的请求都得到同一个固定响应:请求 / 的浏览器、探测者和凭据错误的客户端,看到的都是伪装页。本示例返回一个状态码为 404 的简单 HTML 页面:

[inbound.settings.masquerade]
status = 404
body = "<html><body><h1>404 Not Found</h1></body></html>\n"
content_type = "text/html; charset=utf-8"

省略的配置项保留默认值。status 接受 100 到 999,但不能是 233,因为这是 Hysteria 2 表示认证成功的状态码;设置 status = 233 会报错 inbound hy2-in: hysteria2: 233 is the authentication success status and cannot be used for the masquerade。伪装页是一个固定响应:它不能提供文件,也不能反向代理其他网站。

开启 Salamander 后,没有正确混淆密钥的客户端无法完成 QUIC 握手,因此根本到不了伪装页这一步。

客户端出站使用出站中介绍的通用配置项 server 和 port,Hysteria 2 自身的配置项放在 [outbound.settings] 中:

配置项 类型 默认值 示例取值 原因
server string 无,必填 "proxy.example.com" 服务端的名称或 IP 地址。未设置 server_name 时,证书也必须与这个名称匹配。
port u16 无,必填 443 服务端的 UDP 端口。只能是单个端口,不支持端口范围。
password string 无,必填 "alice:…" 凭据。服务端使用 users 时填 user:pass,使用 password 时填纯密码。
obfs、obfs_password string 无 与服务端相同 必须与服务端完全一致。
server_name string server 的值 未设置 当 server 是 IP 地址而证书中写的是域名时设置它。
ca_file path 无 未设置 额外信任的 CA 证书,用于自签名证书或私有 CA 签发的证书。

该出站没有 [outbound.stream] 块,也没有 TLS 表:server_name、ca_file 和 allow_insecure 都放在 [outbound.settings] 中。SOCKS 入站默认开启 UDP ASSOCIATE,因此 SOCKS5 客户端也可以通过隧道发送 UDP;服务端设置了 udp = true,所以会中继这些 UDP 流量。

如果客户端要通过 IP 地址连接,请保留名称用于证书校验:

[[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"
obfs = "salamander"
obfs_password = "replace-with-a-long-random-obfs-key"

Hysteria 2 客户端在每个 QUIC 连接上发送一次单个字符串,这个字符串在上游客户端中叫 auth,在 etemenanki-app 中叫 password。服务端如何解读这个字符串,取决于它的入站设置了哪个配置项:

服务端 [inbound.settings] 中设置了 etemenanki-app 客户端的 password 上游客户端的 auth 服务端规则
users = [{ user = "alice", pass = "s3cret" }] "alice:s3cret" alice:s3cret 在第一个 : 处拆分。用户名比较时忽略 ASCII 大小写,密码则精确比较。
password = "s3cret" "s3cret" s3cret 整个字符串必须等于 password。

值得了解的后果:

  • Alice:s3cret 同样有效,因为比较前会把用户名转为小写(仅限 ASCII)。出于同样的原因,只有大小写不同的两个用户名会被拒绝:hysteria2: two users share a name once lower-cased。
  • 密码可以包含 :,用户名不行。 alice:pa:ss 会以用户 alice、密码 pa:ss 认证。包含 : 的 user 会报错 hysteria2: a username cannot contain ':' — it separates the two on the wire。
  • 两种形式不能混用。 设置了 users 的服务端拒绝纯密码;设置了 password 的服务端会把整个 alice:s3cret 字符串与其密码比较。两个配置项同时设置会报错 password and users cannot both be set; a credential would have two answers。
  • 可以设置 email,但在这里没有可见效果。 users 条目可以带一个 email,它会代替用户名,作为附加在该用户的流上的身份标识。etemenanki-app 不会记录这个身份,也不按它统计流量或路由。
  • 空值会被拒绝。 user 或 pass 为空的 users 条目会报错 hysteria2: a user needs both a name and a password。

凭据错误时得到的是伪装响应,而不是单独的错误。etemenanki-app 客户端会记录收到的状态码:

WARN etemenanki_protocols::hysteria::slot: hysteria2: connect failed: hysteria2: no address answered (203.0.113.10:443: hysteria2 authentication rejected with status 404 Not Found)

这一行中的状态码就是伪装页的 status,因此使用自定义伪装页的服务端会在这里显示它自己的状态码。

Salamander 用从 obfs_password 派生出的密钥打乱每个 UDP 包,使这些包看起来不再像 QUIC。它不是加密,加密已由 QUIC 自身的 TLS 提供。两端都会对每个包(包括第一个握手包)进行混淆,因此两端必须完全一致:

服务端 客户端 结果
salamander,密钥 K salamander,密钥 K 正常工作。
salamander,密钥 K salamander,其他密钥 没有回应:客户端超时。
salamander 未设置 obfs 没有回应:客户端超时。
未设置 obfs salamander 没有回应:客户端超时。

混淆不一致的表现与端口被拦截完全相同。如果客户端超时而防火墙已开放,先比对两端的 obfs_password。

两端的启动检查都是 fail closed(出错即拒绝)的,因此拼写错误绝不会悄悄关闭混淆。服务端的报错以 inbound hy2-in: 开头,客户端的报错以 outbound hy2-out: 开头:

设置 报错
obfs_password 短于 4 字节或缺失 obfs_password must be at least 4 bytes for salamander
设置了 obfs_password 但没有 obfs obfs_password is set but obfs is not; did you mean obfs = "salamander"?
obfs = "Salamander" 或其他任何值 unknown obfs "Salamander" (expected "salamander")
上游风格的 [outbound.settings.obfs] 表 invalid settings: invalid type: map, expected a string

服务端可以与上游 Hysteria 客户端,以及其他遵循 Hysteria 2 协议规范的客户端配合使用。下面这份上游客户端配置以用户 alice 的身份与服务端示例对应:

config.yaml(上游 hysteria 客户端)
server: proxy.example.com:443
auth: alice:replace-with-a-long-random-password
obfs:
type: salamander
salamander:
password: replace-with-a-long-random-obfs-key
tls:
sni: proxy.example.com
socks5:
listen: 127.0.0.1:1080
http:
listen: 127.0.0.1:8080

支持导入分享链接的客户端使用 hysteria2:// URI 形式表示相同的值,凭据放在 user-info 部分:

hysteria2://alice:replace-with-a-long-random-password@proxy.example.com:443/?obfs=salamander&obfs-password=replace-with-a-long-random-obfs-key&sni=proxy.example.com

密码或密钥中凡是在 URL 里有特殊含义的字符,例如 @、/、?、#、& 或 +,都要进行百分号编码。

为这个服务端配置上游客户端时:

  • 不要设置 bandwidth。 etemenanki-app 没有带宽设置。服务端在认证应答中把自己的接收速率声明为 auto,上游客户端随后会忽略自身的 bandwidth 值,改用其配置的拥塞控制算法。固定速率的 Brutal 模式永远不会启用。
  • 使用单个端口。 etemenanki-app 只绑定一个 UDP 端口,即入站的 port,也没有端口跳跃选项。请把这个端口告诉客户端,而不是 proxy.example.com:20000-30000 这样的范围。
  • 混淆类型使用 salamander。 这是服务端实现的唯一一种混淆。
  • 使用 auth: user:pass:服务端设置 users 时如此,设置 password 时则填纯密码,见客户端如何认证中的表格。
  • 使用自签名证书时,把 tls.ca 设为服务端证书的一份副本;测试时也可以设置 tls.insecure: true。

除非以 lazy: true 运行,上游客户端会在启动时就连接服务端。凭据错误时,这第一次连接会失败,客户端也不会打开它的 SOCKS 端口。

客户端会依据系统信任库校验服务端证书。如果证书是你自己创建的,创建时不要带 CA 标志,并加上 subjectAltName,然后给每个客户端一份副本:

终端窗口
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout privkey.pem -out fullchain.pem -days 365 -subj /CN=proxy.example.com \
-addext subjectAltName=DNS:proxy.example.com \
-addext basicConstraints=critical,CA:FALSE

在 etemenanki-app 客户端上,把这份副本添加为受信任的 CA:

[outbound.settings]
password = "alice:replace-with-a-long-random-password"
ca_file = "/etc/etemenanki/proxy-cert.pem"
obfs = "salamander"
obfs_password = "replace-with-a-long-random-obfs-key"

ca_file 是在系统信任库之外追加,而不是替换它,因此客户端仍然需要系统 CA 证书包,例如 Debian 和 Ubuntu 上的 ca-certificates 软件包。allow_insecure = true 会完全跳过校验;它不能与 ca_file 同时使用,仅供测试。

etemenanki-app 只在构建 generation(一代实例)时读取证书。替换磁盘上的文件并不会让它们重新加载,所以每次续期后都要修改配置文件以触发重载,具体见热重载中关于更换证书或 geodata 文件的说明。

重载会重建整个服务端,并关闭所有 QUIC 连接。etemenanki-app 客户端会记录 hysteria2: connection closed, reconnecting,并在下一个流到来时重新连接;上游客户端会自行重连。如果新证书有问题,重载会被拒绝,服务端继续使用旧证书运行。

在启动时或使用 --test 时出现的报错。使用 --test 时,报错跟在 configuration invalid: 之后;正式启动时,跟在 failed to start: 之后:

报错 原因和解决方法
inbound hy2-in: hysteria2 needs both cert_file and key_file 两个配置项都要设置。
No such file or directory (os error 2) cert_file、key_file 或客户端 ca_file 的路径有误。报错中不会指明是哪个文件。相对路径以工作目录为基准解析,而不是配置文件所在的目录。
hysteria2: certificate and key do not match: … key_file 属于另一张证书。
inbound hy2-in: hysteria2 needs password or users 添加 users 表或 password。
invalid settings: unknown field … 配置项拼写错误,或使用了 etemenanki-app 不支持的配置项,例如 up、down 或 hop_interval。报错中会列出可接受的配置项。
invalid type: string "20000-30000", expected u16 在 port 中写了端口范围。请使用单个端口。
failed to start: inbound hy2-in bind udp 0.0.0.0:443 failed: Address already in use (os error 98) 另一个进程,或同一文件中的另一个入站,占用了 UDP 443。
failed to start: … Permission denied (os error 13) 绑定 443 端口需要 root 权限或 CAP_NET_BIND_SERVICE。

在客户端上,第一次请求时出现的报错。每一行都以 hysteria2: connect failed: hysteria2: no address answered ( 开头,后面是服务端地址和原因。当名称解析出多个地址时,客户端会逐个尝试,并列出每个地址的原因,以 ; 分隔:

原因 原因和解决方法
timed out 该地址在 10 秒内没有回应:UDP 443 被拦截、服务端未运行,或两端的 obfs 设置不一致。
hysteria2 authentication rejected with status 404 Not Found 凭据错误。服务端设置了 users 时,请使用 user:pass。
the cryptographic handshake failed: error 48: invalid peer certificate: UnknownIssuer 证书不是由受信任的 CA 签发的。请使用公共 CA 签发的证书,或设置 ca_file。
the cryptographic handshake failed: error 42: invalid peer certificate: certificate not valid for name "…" server_name 中的名称(未设置 server_name 时为 server 中的名称)不在证书的 subjectAltName 中。
the cryptographic handshake failed: error 46: invalid peer certificate: Other(OtherError(CaUsedAsEndEntity)) 服务端证书被标记为 CA。请按使用自签名证书中的做法,用 basicConstraints=critical,CA:FALSE 重新创建证书。
hysteria2: no system root certificates could be loaded 客户端机器上没有系统 CA 证书包。请安装一个,例如 ca-certificates 软件包。ca_file 不能代替它。

一次尝试失败后,客户端会等待一段时间再重试,等待时间从 2 秒开始,每次翻倍,最长 30 秒。在此期间到来的流会立即失败,报错 hysteria2: connection is down, waiting before the next attempt,而不会发起新的尝试。hysteria2: connect failed 警告每次尝试只出现一次,而不是每个流一次。

如果 TCP 正常而 UDP 不通,很可能是服务端设置了 udp = false。客户端会记录一次:

WARN etemenanki_protocols::hysteria::connector: hysteria2: hysteria2: the server does not relay UDP; datagrams routed to this outbound are dropped