Appearance
Hysteria2 落地与订阅编排
跨境链路慢与不稳定的诊断给出了结论:在有丢包和抖动的跨境路径上,TCP 套 TCP 的代理隧道会放大问题。本页记录对应的落地方案:用 Hysteria2(QUIC)承载主要流量,保留 Shadowsocks 加 WebSocket 通道做故障切换兜底,并用脚本把“开户、同步、校验、回滚”做成可重复的流程。
方案取舍
| 方案 | 传输 | 丢包链路表现 | 部署复杂度 | 适用 |
|---|---|---|---|---|
| Shadowsocks 直连 | TCP | 一般,无额外封装层 | 最低 | 基础代理、学习 |
| Shadowsocks + v2ray-plugin + TLS/WebSocket | TCP | 较差,三层同跑在 TCP 上 | 中,需要入口网关和证书 | 需要复用 443 端口、与其他站点共用入口 |
| Trojan + TLS | TCP | 一般 | 中 | 需要与 TLS 站点形态一致 |
| Hysteria2 | QUIC / UDP | 较好,丢包只影响对应的流 | 中,需要放行 UDP,客户端须为 Mihomo 内核 | 跨境、高丢包或高抖动链路 |
Hysteria2 不替代 TCP 通道。UDP 在部分网络中会被限速或丢弃,所以最终形态是“QUIC 优先,TCP 兜底”,由客户端的故障切换组自动选择。
几个关键机制
- QUIC 流独立:一条连接里的多个请求各自成流,某个流丢包只重传该流,不会阻塞其他流。
- 固定速率发送:客户端声明上传和下载带宽后,服务端按声明速率发送,不会因为偶发丢包把发送窗口降到很低。代价是它不会主动让位,需要在服务端容量上留出余量。
- 混淆(salamander):给 UDP 数据包加一层混淆,避免被识别为标准 QUIC 流量;服务端和客户端必须使用相同的混淆密码。
- 伪装(masquerade):未通过认证的探测请求会被转发到一个普通 HTTPS 站点,服务端对外表现为正常网站。
整体架构
数据面从左到右:应用流量进入 Mihomo 内核,内核按规则决定直连或交给 PROXY 组;PROXY 组是故障切换组,优先使用 QUIC 通道,探测失败时回落到 TLS 加 WebSocket 通道;两条通道在服务器上各有入口,最终都从同一个出口访问公网目标或本机的中转服务。控制面在最下方:脚本生成账号和个人订阅文件,以 HTTPS 发布,客户端定期刷新。
服务端要点
绑定监听地址并修正回程路径
现象:开启混淆后,客户端握手总是超时;关闭混淆能握手但数据传不动。
抓包证据:请求从网卡 A 进入(目的地址为 A 的内网地址),回包却从网卡 B 发出,源地址是 B 的内网地址。客户端的 UDP 套接字只接受来自它连接对象的回包,源地址不一致的包被直接丢弃。
根因:服务器有两张网卡,两条默认路由优先级相同。TCP 有连接状态,内核能按连接选路;监听在通配地址上的 UDP 套接字没有连接,回包的源地址由路由查询决定,落到另一张网卡上。
处理两步一起做:
- 把监听地址绑定到对外网卡的内网地址,不监听通配地址。
- 增加源地址策略路由,让从某个地址发出的包走对应网卡的路由表:
bash
# 10.0.0.12 / 10.0.0.4 为两张网卡的内网地址;表 201 / 202 各含对应网卡的默认路由
ip rule add from 10.0.0.12 lookup 201 pref 10101
ip rule add from 10.0.0.4 lookup 202 pref 10102规则需要做成 systemd 服务(oneshot,开机执行),否则重启后失效。客户端节点里的 server 写该网卡对应的公网 IP,而不是一个可能解析到另一块网卡的域名。
放行 UDP 端口
云安全组和主机防火墙都要放行 UDP 端口(本例为 443)。只放行其中一个,握手包看起来像“服务端没响应”。验证方法是在服务器上抓包,确认客户端的 UDP 包到达并有回包:
bash
tcpdump -ni any udp port 443 -c 20关闭路径 MTU 探测
握手成功但数据传输早期就卡住,是路径 MTU 的典型表现:QUIC 的 UDP 包超过路径能承载的大小时,被中间设备丢弃,又收不到可用的 ICMP 反馈。服务端配置里关闭 QUIC 的路径 MTU 探测,使用保守的包大小:
yaml
quic:
disablePathMTUDiscovery: true客户端必须声明带宽
服务端日志里每条连接记录会带一个 tx 字段,表示客户端声明的接收速率。tx 为 0 说明客户端没有声明带宽,服务端退回 BBR 拥塞控制,在丢包链路上速度明显偏低。声明方式取决于客户端实现。Mihomo 的节点里使用数值写法,例如 up: '16'、down: '30',单位为 Mbit/s;测试中带单位的字符串写法(如 20 mbps)出现过 tx 为 0 的情况,改成数值后才正常。上线前以日志里的 tx 非零为准。
完整示例(占位值)
yaml
listen: 10.0.0.4:443 # 绑定对外网卡的内网地址,不写 :443
tls:
cert: /etc/letsencrypt/live/proxy.example.com/fullchain.pem
key: /etc/letsencrypt/live/proxy.example.com/privkey.pem
obfs:
type: salamander
salamander:
password: <混淆密码>
auth:
type: userpass # 每个账号一组独立密码
userpass:
user01: <随机密码>
masquerade:
type: proxy
proxy:
url: https://127.0.0.1
rewriteHost: false
insecure: true
quic:
disablePathMTUDiscovery: trueuserpass 不支持热加载:增加或删除账号后必须重启服务,已有 QUIC 连接会断开重连。所以自动化脚本只在凭据列表发生变化时才重启,重复执行同一个账号不会触发重启。
订阅模板
个人订阅文件里同时包含两个节点,并把 PROXY 组设为故障切换:
yaml
proxies:
- name: 张三-HY2
type: hysteria2
server: 203.0.113.10 # 对外网卡对应的公网 IP
port: 443
password: user01:<随机密码> # 格式为“用户名:密码”
sni: proxy.example.com
obfs: salamander
obfs-password: <混淆密码>
up: '16'
down: '30'
skip-cert-verify: false
- name: 张三 # 原 Shadowsocks 兜底节点
type: ss
# 省略,沿用原配置
proxy-groups:
- name: PROXY
type: fallback
url: https://www.gstatic.com/generate_204
interval: 60
lazy: false
interrupt-exist-connections: true
proxies:
- 张三-HY2
- 张三三个选择值得单独说明:
- 故障切换而不是手动选择:用户不需要理解节点差异,QUIC 不可用时自动回落。
interrupt-exist-connections: true:节点切换时打断已有连接,让它们按新的选择重建。没有这一项,已建立的长连接会一直留在兜底线路上,见下文的事故复盘。- 中转服务域名也走
PROXY:此前访问中转 API 的规则是直连,直连正是丢包最严重的路径;改成走 QUIC 隧道后,大请求体上传不再超时。
速率与容量
出口总带宽是有限的(本例为 150 Mbit/s 级),而 Hysteria2 按声明速率发送,不会自动让位。设置单账号速率时要估算同时活跃的账号数:
- 上行 16、下行 30 Mbit/s 足够网页、视频和中转 API 调用,实测下行约 24 Mbit/s。
- 账号数乘以下行上限超过出口总带宽时,要依赖“同时活跃的账号只占一小部分”这一假设,并监控出口使用率。
- 不要把测试时不限速得到的峰值(约 60 Mbit/s)当作生产能力。
账号自动化与下发
开户脚本在创建 Shadowsocks 账号之后,自动执行 Hysteria2 同步:生成该账号的独立凭据,改写个人订阅文件,必要时重启服务,最后用真实解析器校验。
同步脚本有三个性质,缺一个都会出事故:
- 幂等:重复执行不改变已有凭据,订阅文件按固定模板重新生成。
- 写前备份:每次改动前把受影响的订阅文件和服务端配置复制到带时间戳的备份目录。
- 自带校验:同步完成后立即校验,失败返回非零。校验项包括:每份订阅恰好一个 HY2 节点和一个 SS 节点、中转域名走
PROXY、故障切换组存在、订阅里的密码与服务端账号一致、订阅链接 HTTP 返回 200。
批量下发的顺序:先对自己的订阅同步并实测,再对全部账号同步,最后由管理员通知用户在 Clash Verge Rev 里刷新订阅。订阅链接不变,用户不需要重新导入。
bash
hy2-sync --accounts user01 # 新增或更新单个账号
hy2-sync --all # 对所有账号重新生成订阅
hy2-sync --verify # 只校验,不修改事故复盘
| 事故 | 现象 | 根因 | 处理与防范 |
|---|---|---|---|
| 订阅缩进不一致 | 十余份订阅无法被客户端解析,报 did not find expected key | 原订阅里节点列表缩进 2 格,新节点按 0 缩进生成,两种风格混在同一份文件 | 从备份恢复后,按原文件的缩进生成新节点;下发后用真实内核对每份订阅做配置检查,而不只是看结构 |
| 备份文件被误发布 | 备份目录里的服务端配置被复制进公网可访问的订阅目录 | 恢复脚本用通配符复制备份目录中的所有 *.yaml | 发现后立即删除并核对访问日志,确认没有成功下载;备份目录与发布目录分开,恢复只按文件名白名单 |
| 重启后长连接留在兜底线路 | 一段时间内大请求体上传仍然很慢 | 服务重启期间,故障切换组把新连接切到 TCP 通道;已建立的长连接不会自己切回 | 订阅加 interrupt-exist-connections: true;脚本改为仅在凭据变化时重启;必要时手动关闭旧连接让它们重建 |
| 并发修改服务端 | 测试连接被意外中断 | 多个会话同时改配置并重启服务 | 约定同一时刻只有一个写入者,改动前先看 journalctl 和 sudo 日志确认没有别人在操作 |
客户端兼容性
| 客户端内核 | 是否支持 hysteria2 | 说明 |
|---|---|---|
| Mihomo(Clash Meta),如 Clash Verge Rev | 支持 | 推荐 |
| Clash for Windows、旧版 ClashX | 不支持 | 订阅里出现 hysteria2 节点会导致整份配置解析失败 |
| Clash Premium 内核 | 不支持 | 同上 |
下发前要确认所有使用者的客户端。不支持的客户端不是“少一个节点”,而是原本可用的订阅也加载不了。
验收清单
- 同一探针经 QUIC 通道和 TCP 通道各跑多轮,比较完成率和耗时分布。
- 服务端日志里该账号连接的
tx非零。 PROXY组当前选中 HY2 节点;人为停止服务后能回落到兜底节点,恢复后新连接回到 HY2。- 中转服务的长连接在故障切换后能被打断重建。
- 订阅链接 HTTP 返回 200,且订阅目录里没有备份或服务端配置文件。
- 服务重启次数与凭据变更次数一致,没有多余重启。
适用边界
- UDP 被限速或封禁的网络里,QUIC 通道不可用,只能依靠兜底通道,这时体验会回到原来的水平。
- 性能数字只代表测试时的网络环境,换运营商、时段或地区后要重新用探针验证。
- 单机双网卡、固定速率发送和 UDP 防火墙三件事是这个方案特有的运维点,部署前先逐项核对,再谈调优。
