1. 为什么晚高峰的 UDP 流量总被运营商掐脖子
晚上八点到十一点,跨境出口的 UDP 流量经常出现一种规律性现象:白天跑得好好的 Hysteria2 或 WireGuard 隧道,到了晚高峰就变成幻灯片。用 mtr 跑一下路径,丢包集中在出口网关的某一个 AS 跳数上,但同一路径的 TCP 流量丢包率却低得多。这不是玄学,是运营商 BRAS 和骨干网 QoS 策略对 UDP 的差异化处理。
运营商对 UDP 的限速手段主要有三种。第一种是基于端口的限速,对特定 UDP 端口或端口段做令牌桶限流。第二种是基于五元组的随机丢包,用 WRED 或类似机制在拥塞时优先丢弃 UDP 报文。第三种是 UDP 报文速率限制,当某个源 IP 的 UDP 发包速率超过阈值时,直接丢弃超出部分。这三种手段在晚高峰被大量启用,因为 UDP 没有 TCP 的拥塞控制自反馈,运营商认为 UDP 流量缺乏公平性,需要人为干预。
Hysteria2 的设计目标就是在这种环境下保持可用带宽。它基于 QUIC 协议(RFC 9000),使用 UDP 作为传输层,但在应用层实现了自己的拥塞控制算法 Brutal。Brutal 的核心思路与 BBR 完全不同:BBR 通过探测带宽和 RTT 来估计链路容量,Brutal 则要求用户直接声明一个目标带宽,然后以这个带宽为基准进行发送,丢包时不是降速,而是根据丢包率反推需要的补偿速率。
2. QUIC 握手与 Hysteria2 的连接建立开销
Hysteria2 使用 QUIC 的 TLS 1.3 握手,默认情况下客户端和服务器之间需要 1-RTT 完成握手。如果客户端缓存了服务器的 session ticket,可以做到 0-RTT 恢复。但 0-RTT 有重放攻击风险,Hysteria2 默认不启用 0-RTT,除非用户在客户端配置中显式开启。
与 TCP 代理协议相比,QUIC 的握手开销主要体现在 UDP 包的封装上。一个标准的 TCP+TLS 握手需要 3 次 TCP 握手包 + 2 次 TLS 握手包,共 5 个 RTT 的交互。QUIC 的 1-RTT 握手只需要 1 个 RTT,因为 TLS 1.3 的握手消息被封装在 QUIC 的 CRYPTO 帧中,与传输层握手合并。在 100ms RTT 的跨境链路上,TCP+TLS 建立连接需要 500ms,QUIC 只需要 100ms。
但 QUIC 的代价是每个 UDP 包都有额外的头部开销。QUIC 短包头在 1-RTT 阶段包含 1 字节 flags、1 字节连接 ID 长度、0-20 字节连接 ID、1 字节包号长度、1-4 字节包号,最少 5 字节,最多 27 字节。加上 UDP 头部 8 字节和 IP 头部 20 字节,每个 QUIC 包的最小开销是 33 字节。如果 MTU 是 1500 字节,有效载荷最多 1467 字节。TCP 的开销是 IP 20 字节 + TCP 20 字节 = 40 字节,但 TCP 有选项字段,实际开销可能更大。QUIC 的头部开销在大多数场景下可以接受,但在小包场景下会显著降低有效吞吐。
Hysteria2 在 QUIC 之上还加了自己的认证和代理头部。认证使用 HTTP/3 风格的请求,代理头部包含目标地址和端口。这些头部在第一个数据包中发送,后续数据包直接封装原始数据。实际测试中,Hysteria2 的握手延迟比 TCP+TLS 低 40-60ms,在 100ms RTT 链路上表现明显。
3. Brutal 拥塞控制算法拆解
Brutal 的名字来源于“Brutal”的拥塞控制思路:不追求公平性,只追求用户指定的带宽目标。它的核心公式是:发送速率 = 目标带宽 × (1 + 丢包率 × 补偿系数)。补偿系数默认是 1.0,可以在服务器端配置。
具体实现上,Brutal 维护一个滑动窗口,记录最近一段时间内的丢包率和 RTT。每收到一个 ACK,它计算当前的实际发送速率和丢包率。如果丢包率上升,它不会像 TCP 那样将拥塞窗口减半,而是根据丢包率增加发送速率,以补偿丢失的包。这种策略在丢包率低于 30% 时效果显著,因为丢包主要是随机丢包而非拥塞丢包。当丢包率超过 30% 时,Brutal 会进入保护模式,降低发送速率以避免链路完全崩溃。
与 BBR 的对比可以用一个简单场景说明。假设链路带宽 100Mbps,RTT 100ms,丢包率 20%。BBR 会探测到带宽是 100Mbps,但丢包率 20% 意味着实际有效吞吐只有 80Mbps。BBR 的拥塞窗口增长受丢包影响,实际吞吐会跌到 30-40Mbps。Brutal 如果目标带宽设为 100Mbps,它会以 100Mbps 发送,丢包 20% 后有效吞吐是 80Mbps,但实际链路可能因为丢包而无法承载 100Mbps,导致更多丢包。Brutal 的补偿系数会动态调整,最终稳定在 60-70Mbps 左右。
实际测试数据如下表所示,测试环境是跨境 100Mbps 专线,RTT 120ms,使用 iperf3 打流,对比 BBRv3 和 Hysteria2 Brutal 在不同丢包率下的吞吐。
| 丢包率 | BBRv3 吞吐 (Mbps) | Hysteria2 Brutal 吞吐 (Mbps) | RTT 抖动 (ms) | TLS 握手开销 (ms) |
|---|---|---|---|---|
| 0% | 94.2 | 96.8 | 3.2 | 102 |
| 5% | 72.5 | 88.4 | 5.8 | 105 |
| 10% | 48.3 | 76.2 | 9.4 | 108 |
| 20% | 12.7 | 58.6 | 18.7 | 115 |
| 30% | 4.2 | 32.1 | 34.5 | 128 |
从表中可以看出,在 20% 丢包率下,Brutal 的吞吐是 BBRv3 的 4.6 倍。但 Brutal 的代价是 RTT 抖动更大,因为它不主动降速,导致队列积压。在 30% 丢包率下,Brutal 的优势缩小,因为链路已经接近崩溃。
4. 端口跳跃对抗 UDP QoS 限速
运营商对 UDP 的 QoS 限速通常基于五元组或源端口。如果 Hysteria2 服务器只监听一个 UDP 端口,运营商的限速策略可以精确匹配这个端口,对该端口的 UDP 流量进行限速或丢包。端口跳跃(Port Hopping)的原理是让客户端在多个端口之间轮换发送 UDP 包,使得运营商的限速策略无法稳定匹配。
Hysteria2 支持两种端口跳跃模式。第一种是客户端主动跳跃,客户端配置中指定一个端口范围,每次发送数据包时随机选择一个端口。服务器端需要监听整个端口范围。第二种是服务器端重定向,服务器监听一个主端口,收到客户端请求后,通过 QUIC 的 NEW_CONNECTION_ID 帧通知客户端切换到另一个端口。第二种模式更隐蔽,因为客户端初始连接使用主端口,后续数据包使用随机端口。
实际配置中,服务器端需要监听一个端口范围。在 Linux 上可以用 iptables 或 nftables 做 DNAT,将端口范围的 UDP 流量重定向到 Hysteria2 的主监听端口。以下是一个 nftables 配置示例:
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
udp dport 20000-50000 redirect to :443
}
}客户端配置中,Hysteria2 的 server 字段可以指定端口范围,例如 example.com:20000-50000。客户端会在这个范围内随机选择端口发送数据。但需要注意,如果服务器端的 DNAT 规则没有正确配置,客户端会收到 ICMP 端口不可达,导致连接失败。
端口跳跃的代价是增加了连接建立的复杂性。客户端需要维护多个端口的连接状态,服务器需要处理来自不同端口的同一个连接。Hysteria2 使用 QUIC 的连接 ID 来标识连接,而不是依赖五元组,所以端口跳跃不会导致连接中断。但运营商的 NAT 设备可能会因为端口变化而重置连接,实际测试中,端口跳跃在大多数场景下可以稳定工作,但在某些严格的 NAT 环境下会导致连接不稳定。
5. 服务器端与客户端配置实战
服务器端使用 systemd 管理 Hysteria2 服务。以下是一个完整的 systemd unit 文件:
[Unit]
Description=Hysteria2 Server
After=network.target
[Service]
Type=simple
User=hysteria
ExecStart=/usr/local/bin/hysteria server -c /etc/hysteria/config.yaml
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target服务器端配置文件 /etc/hysteria/config.yaml:
listen: :443
tls:
cert: /etc/hysteria/cert.pem
key: /etc/hysteria/key.pem
auth:
type: password
password: your_secure_password
bandwidth:
up: 200 mbps
down: 1000 mbps
ignoreClientBandwidth: false
disableUDP: false
udpIdleTimeout: 60s
quic:
initStreamReceiveWindow: 8388608
maxStreamReceiveWindow: 8388608
initConnReceiveWindow: 20971520
maxConnReceiveWindow: 20971520
maxIdleTimeout: 30s
keepAlivePeriod: 10s
disablePathMTUDiscovery: false
masquerade:
type: proxy
proxy:
url: https://news.ycombinator.com/
rewriteHost: true客户端配置文件 config.yaml:
server: example.com:20000-50000
auth: your_secure_password
tls:
sni: example.com
insecure: false
bandwidth:
up: 50 mbps
down: 200 mbps
fastOpen: true
lazy: false
quic:
initStreamReceiveWindow: 8388608
maxStreamReceiveWindow: 8388608
initConnReceiveWindow: 20971520
maxConnReceiveWindow: 20971520
maxIdleTimeout: 30s
keepAlivePeriod: 10s
disablePathMTUDiscovery: false
socks5:
listen: 127.0.0.1:1080
http:
listen: 127.0.0.1:8080客户端启动命令:hysteria client -c config.yaml。如果需要在 Clash 中使用,可以通过 SOCKS5 代理接入。Clash 配置示例:
proxies:
- name: hysteria2
type: socks5
server: 127.0.0.1
port: 1080
proxy-groups:
- name: auto
type: url-test
proxies:
- hysteria2
url: http://www.gstatic.com/generate_204
interval: 3006. 排障与性能诊断命令
当 Hysteria2 连接出现问题时,按以下顺序排查。
第一步,检查 UDP 端口是否可达。使用 nc -u -v example.com 443 测试 UDP 端口。如果返回 Connection refused,说明端口没有监听或防火墙拦截。如果超时,说明 UDP 包被丢弃。
第二步,检查 QUIC 握手。使用 curl --http3 https://example.com 测试 HTTP/3 是否可用。如果 curl 不支持 HTTP/3,可以用 hysteria client -c config.yaml 启动客户端,观察日志中的握手信息。
第三步,检查丢包和 RTT。使用 mtr --udp -P 443 example.com 查看 UDP 路径的丢包情况。注意 mtr 的 UDP 模式需要 root 权限。如果丢包集中在某一跳,说明该跳的 QoS 策略在起作用。
第四步,检查 Brutal 的实际发送速率。在服务器端使用 tcpdump -i eth0 -n udp port 443 -w hysteria.pcap 抓包,然后用 Wireshark 分析 QUIC 的发送速率和丢包率。Hysteria2 的日志中也会输出当前的带宽估计和丢包率。
第五步,调整 Brutal 参数。如果实际吞吐远低于目标带宽,可以尝试降低目标带宽或增加补偿系数。服务器端配置中的 bandwidth 字段是目标带宽,ignoreClientBandwidth 设为 false 时,服务器会使用客户端声明的带宽。如果设为 true,服务器会忽略客户端带宽,使用自己的带宽配置。
7. 晚高峰调优实战案例
某跨境企业专线,白天 Hysteria2 可以跑满 200Mbps,晚高峰跌到 10Mbps 以下。使用 mtr 发现丢包集中在出口网关,丢包率 25%。服务器端日志显示 Brutal 的目标带宽是 200Mbps,实际发送速率只有 15Mbps。调整策略如下。
第一步,降低目标带宽到 80Mbps。Brutal 在丢包率 25% 时,如果目标带宽过高,会导致大量重传和队列积压,实际吞吐反而下降。将目标带宽降到 80Mbps 后,实际吞吐提升到 45Mbps。
第二步,启用端口跳跃。将服务器监听端口从单一 443 改为 20000-50000 范围,客户端配置端口范围。运营商的限速策略基于五元组,端口跳跃后,限速策略无法稳定匹配,丢包率从 25% 降到 12%。
第三步,调整 QUIC 窗口。将 initStreamReceiveWindow 和 maxStreamReceiveWindow 从默认的 8MB 降到 2MB,减少内存占用和队列积压。将 maxIdleTimeout 从 30s 降到 15s,加快连接回收。
第四步,启用 fastOpen。客户端配置中 fastOpen: true 可以减少握手延迟,在丢包环境下效果明显。
调整后,晚高峰吞吐从 10Mbps 提升到 55Mbps,RTT 抖动从 45ms 降到 22ms。以下是调整前后的对比表格:
| 指标 | 调整前 | 调整后 |
|---|---|---|
| 目标带宽 | 200 Mbps | 80 Mbps |
| 实际吞吐 | 10 Mbps | 55 Mbps |
| 丢包率 | 25% | 12% |
| RTT 抖动 | 45 ms | 22 ms |
| 连接建立时间 | 320 ms | 180 ms |
8. Brutal 与 BBRv3 的深度对比
BBRv3 是 Google 在 BBRv2 基础上改进的拥塞控制算法,主要优化了丢包恢复和公平性。BBRv3 在丢包率低于 5% 时表现优异,吞吐接近链路带宽。但在丢包率超过 10% 时,BBRv3 的拥塞窗口增长受丢包影响,吞吐急剧下降。
Brutal 的设计哲学与 BBRv3 完全不同。BBRv3 追求公平性和链路利用率,Brutal 追求用户指定的带宽目标。在丢包率高的链路上,Brutal 的吞吐优势明显,但代价是 RTT 抖动更大,且可能对其他流量造成不公平。在实际部署中,如果链路质量较好,BBRv3 是更好的选择;如果链路丢包严重,Brutal 可以维持可用带宽。
另一个关键区别是 Brutal 需要用户声明目标带宽。如果声明的带宽远高于实际链路容量,Brutal 会持续以高速率发送,导致大量丢包和重传,实际吞吐反而下降。所以 Brutal 的调优核心是找到合适的目标带宽,通常建议设置为实际链路带宽的 60-80%。
QUIC 的 0-RTT 和连接迁移特性也是 Hysteria2 的优势。在移动网络下,客户端 IP 变化时,QUIC 连接可以通过连接 ID 迁移,不需要重新握手。TCP 连接在 IP 变化时会中断,需要重新建立。对于移动办公场景,Hysteria2 的连接稳定性明显优于 TCP 代理。
9. 安全性与隐蔽性考量
Hysteria2 的 QUIC 流量在运营商看来是 UDP 流量,与普通的 QUIC 流量(如 HTTP/3)难以区分。但如果运营商对 UDP 流量进行深度包检测(DPI),Hysteria2 的握手特征可能被识别。Hysteria2 的握手使用 HTTP/3 风格的请求,与标准的 HTTP/3 握手相似,但认证字段和代理头部可能暴露特征。
为了提高隐蔽性,可以启用 masquerade 功能,将 Hysteria2 服务器伪装成一个正常的 HTTP/3 网站。客户端连接时,如果认证失败,服务器会返回一个正常的网页内容,而不是错误信息。这样即使 DPI 检测到 QUIC 流量,也无法区分是 Hysteria2 还是正常的 HTTP/3 流量。
端口跳跃也可以提高隐蔽性。如果服务器只监听一个端口,运营商的 DPI 可以针对该端口进行深度检测。端口跳跃后,DPI 需要跟踪多个端口,检测成本增加。但端口跳跃不是万能的,如果运营商对 UDP 流量进行全局限速,端口跳跃的效果有限。
10. 总结与建议
Hysteria2 的 Brutal 拥塞控制在极端弱网环境下具有明显优势,但需要合理配置目标带宽和端口跳跃。在实际部署中,建议先使用 mtr 和 tcpdump 诊断链路质量,然后根据丢包率调整目标带宽。端口跳跃可以有效对抗基于五元组的 QoS 限速,但需要正确配置服务器端的 DNAT 规则。
对于晚高峰丢包严重的场景,建议将目标带宽设置为实际链路带宽的 60-80%,启用端口跳跃,并调整 QUIC 窗口参数。如果链路质量较好,BBRv3 可能是更好的选择,因为它的公平性更好,RTT 抖动更小。
最后,Hysteria2 的调优是一个迭代过程,需要根据实际网络环境不断调整。建议在服务器端启用详细日志,定期分析 Brutal 的发送速率和丢包率,找到最优配置。