WireGuard 速度慢的真正原因:如何定位
WireGuard 速度慢通常是 MTU 问题。本文用二分法测量路径 MTU,设置 TCP MSS,检查 VPS steal time,并先验证网络路径,避免误怪隧道。
WireGuard 的低速问题实际来自哪里
WireGuard 的低速问题通常来自以下四类原因,而且它们出现的概率并不相同。第一类是 MTU(最大传输单元):隧道生成的数据包超过路径中某条链路能够承载的大小,因此大批量传输会停滞,而小数据包看起来正常。第二类是网络路径本身:即使没有隧道,该路径也已经是性能瓶颈。第三类是小型共享 VPS 上的 CPU 资源:加密处理会与同一主机上的其他租户争用 CPU。第四类是对端自身的网络连接。
请按此顺序检查。MTU 应首先检查,因为列表中只有它是 WireGuard 本身引入的原因,而且它的表现完全不像通常所说的“速度慢”。错误的 MTU 通常表现为:隧道立即建立,能够响应 ping,也能通过 SSH 登录,但首次复制文件时就会冻结。
在进行上述检查前,还应先排除一种现象。如果每个新网站都需要数秒才开始加载,之后传输速度却正常,那么问题出在名称解析,而不是吞吐量。WireGuard 上的 DNS 有其自身的故障模式,修改 MTU 无法解决这些问题。
为什么 WireGuard 的 MTU 是 1420?
发送到隧道中的每个数据包都会先加密,再封装到一个新的数据包中。封装会占用字节数,这些字节会从有效载荷中扣除。
WireGuard 数据头为 32 字节:4 字节类型字段、4 字节接收方索引、8 字节计数器,以及 16 字节 Poly1305 身份验证标签。其外层是 8 字节的 UDP 头。再外层是 IP 头;IPv4 的 IP 头为 20 字节,IPv6 的 IP 头为 40 字节。因此,当 Endpoint 是 IPv4 地址时,总封装开销为 60 字节;当它是 IPv6 地址时,为 80 字节。WireGuard 协议页面记录了这些数字所依据的消息布局。
wg-quick 不会对此进行猜测。它会读取路由到 Endpoint 的接口 MTU,然后减去 80。在普通的 1500 字节以太网路径上,结果为 1420,这就是 ip link show wg0 输出的值。它减去 80 而不是 60,是为了在该端点将来通过 IPv6 访问时仍然安全,因为 IPv6 的外层头大 20 字节。
The data behind this chart
[
{
"label": "Ethernet, IPv4 endpoint",
"path_mtu": 1500,
"encap_overhead": 60,
"usable_wg0_mtu": 1440
},
{
"label": "Ethernet, IPv6 endpoint",
"path_mtu": 1500,
"encap_overhead": 80,
"usable_wg0_mtu": 1420
},
{
"label": "PPPoE DSL, IPv4 endpoint",
"path_mtu": 1492,
"encap_overhead": 60,
"usable_wg0_mtu": 1432
},
{
"label": "Extra tunnel in the path",
"path_mtu": 1400,
"encap_overhead": 80,
"usable_wg0_mtu": 1320
}
]这些 4 行是算术结果,不是测量结果。在使用 IPv4 端点且路径干净、MTU 为 1500 字节的情况下,1440 可以正常容纳,因此默认值 1420 会留下 20 字节未使用空间。这个余量是有意保留的,不是问题所在。
最后一行才是问题所在。如果路径中的某条链路只能承载 1400 字节,而隧道仍设置为 1420,那么每个完整大小的分段都会生成一个 1500 字节的外层数据包,比该链路可接受的大小多 100 字节。能够容纳的数据包大小是 1320。
也不要直接复制 1320。路径 MTU 取决于你的网络路径,唯一确定它的方法是进行测量。
错误 MTU 的表现
这种故障不是逐渐发生的,而是小数据包和大数据包之间出现明显分界。
- 通过隧道传输的
ping在任何正常大小下都能工作。 - SSH 登录可以完成,输入操作也很流畅。
curl -I https://example.com会立即返回响应头。- 对大页面执行
curl https://example.com时,传输在最初几 KB 后挂起。 scp大文件时,传输开始后会在某个百分比处停止。- 运行会输出大量内容的命令时,SSH 会话会立即冻结。
这是因为 TCP 连接只有在需要传输大量数据时,才会建立完整大小的分段。握手和第一个请求在路径上的任何 MTU 下都能正常传输。连接会在遇到第一个完整大小的分段时开始停滞,所以在失去实际作用之前,看起来一直是正常的。
如果外层数据包大于下一条链路的 MTU,它会有两种结果。
数据包被分片。 路由器会将数据包拆分,接收端再重新组装这些分片。传输可以完成,但速度会变慢,因为原本一个数据包现在需要传输两个数据包,而且接收端必须保留状态,直到两个分片都到达。只要丢失一个分片,整个原始数据包就会丢失,因此具有 1% 丢包率的路径实际表现会差得多。许多防火墙也会按策略丢弃 IP 分片,使这种结果变成下一种结果。
数据包被丢弃,并且你可能不会收到任何通知。 不允许分片的路由器会向发送端返回 ICMP(互联网控制消息协议)“需要分片”消息,并携带它可以接受的 MTU。只要该消息能够到达,路径 MTU 发现就会正常工作,发送端会自动减小分段大小。许多网络会过滤 ICMP,因此该消息通常无法到达。没有其他机制会报告这次丢包。这就是黑洞:数据包离开后没有任何响应,两端的日志中都没有错误,传输会一直挂起,直到某个操作超时。
如何找出正确的 MTU?
测量路径,然后进行换算。测试底层网络,而不是隧道。因此,从客户端 ping 服务器的公网地址,并禁止分片。
ping -M do -s 1472 -c 3 203.0.113.10-M do 设置 DF(禁止分片)位,因此沿途路由器都不能拆分数据包。-s 是 ICMP 负载大小。完整的 IPv4 数据包大小等于该负载加上 8 字节 ICMP 头和 20 字节 IP 头,因此 -s 1472 会在网络上传输恰好 1500 字节。
有 3 种结果需要关注。收到正常回复,表示 1500 字节可以通过,问题不在 MTU。出现本地错误,表示本机接口的 MTU 已经小于请求的大小:
ping: local error: message too long, mtu=1500如果收到中间路由器的回复,它会直接告诉你答案,此时可以停止测试:
From 203.0.113.1 icmp_seq=1 Frag needed and DF set (mtu = 1492)在 1472 字节时丢包率为 100%,而更小的大小可以正常回复,这就是黑洞情况。没有路由器告知你原因,因此需要通过折半搜索找到边界。保留一个已知可用的大小和一个已知失败的大小,测试中间值,然后根据结果移动对应边界。每轮都会将剩余范围缩小一半,因此 5 或 6 轮就足够了。
逐轮执行的二分搜索示例
每一行都是在客户端上针对服务器公网地址执行的命令。注释记录了返回结果。
ping -M do -s 1472 -c 3 203.0.113.10 # 100% loss, so 1500 is too big
ping -M do -s 1272 -c 3 203.0.113.10 # replies, so 1300 fits
ping -M do -s 1372 -c 3 203.0.113.10 # replies, so 1400 fits
ping -M do -s 1422 -c 3 203.0.113.10 # 100% loss, so 1450 is too big
ping -M do -s 1397 -c 3 203.0.113.10 # 100% loss, so 1425 is too big
ping -M do -s 1384 -c 3 203.0.113.10 # 100% loss, so 1412 is too big成功通过的最大负载是 1372,因此该路径至少可以承载 1400 字节,但少于 1412 字节。采用安全边界。路径 MTU 为 1400,减去 80 字节的封装开销后,wg0 的 MTU 为 1320。
tracepath 会自行执行相同的搜索,因此开始二分搜索前值得先运行一次:
tracepath -n 203.0.113.10它的最后一行会报告检测结果:
Resume: pmtu 1492 hops 12 back 12将这两个工具视为起点,而不是最终证明。有些主机会限制 ICMP 速率,或完全丢弃 ICMP,因此二分搜索得出的 MTU 可能小于路径实际支持的大小。之前失败的传输才是真正的测试。
先动态应用该值,因为猜错后只需执行一条命令即可恢复:
sudo ip link set mtu 1320 dev wg0重试之前卡住的传输。如果传输完成,请将该值写入客户端的 [Interface] 块,使其永久生效:
[Interface]
PrivateKey = <client private key>
Address = 10.8.0.2/32
MTU = 1320sudo wg-quick down wg0 && sudo wg-quick up wg0
ip link show wg0ip link show wg0 现在应输出 mtu 1320。如果仍然输出旧值,说明 wg-quick 没有读取你编辑的文件。检查你编辑的是 /etc/wireguard/wg0.conf,并确认 MTU 位于 [Interface] 下,而不是位于 [Peer] 下;后者会被忽略。
MTU 是单个接口的属性,不会在对等端之间协商。在客户端设置 MTU 只会减小客户端发送的数据包。服务器仍会按照自身 wg0 的 MTU 构造数据包,因此上传开始正常后,下载仍可能进入黑洞。应在两端设置该值,或在服务器上限制 MSS。
MSS clamping 为什么只修复 TCP
如果服务器为对等端转发流量,那么在任何使用 NAT(网络地址转换)的标准 WireGuard VPS 配置中,只需一条规则即可为所有对等端修复 TCP,避免为每个不受您控制的客户端逐一追查数值。
MSS(最大分段大小)是 TCP 选项。通信双方都会在 SYN 数据包中设置该选项,声明自己愿意接收的分段大小。MSS clamping 会在数据包传输过程中重写该选项,使其匹配实际路径的 MTU。这样,双方会在传输任何数据前,就较小的分段大小达成一致。它之所以有效,是因为重写发生在握手期间,而且不依赖路径可能过滤的 ICMP 消息。
使用 nftables 时,将以下表添加到 /etc/nftables.conf 中现有表的下方:
table inet mangle {
chain forward {
type filter hook forward priority mangle; policy accept;
tcp flags syn tcp option maxseg size set rt mtu
}
}使用 sudo systemctl reload nftables 重新加载。在使用 iptables 的服务器上,对应规则只有一行:
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu确认数据包经过的路径包含此规则。客户端建立新连接时,运行 sudo nft list table inet mangle 或 sudo iptables -t mangle -L FORWARD -n -v,并观察计数器递增。计数器始终为 0,表示数据包没有经过该 hook,因此规则没有生效。
下面说明它的限制。MSS clamping 只覆盖 TCP,不覆盖其他协议;它也只覆盖转发流量。因此,在 WireGuard 服务器本机运行的服务不会经过 forward hook,也不会应用 MSS clamping。该规则还只影响加载规则后建立的连接:现有会话会继续使用双方已经协商的 MSS。
UDP 不受影响,因为 UDP 没有可重写的握手。大多数 UDP 流量仍然可以正常工作。QUIC 是 HTTP/3 使用的传输协议,会自行探测可用的数据包大小,并且会有意从较小的大小开始。无法正常工作的情况是:UDP 发送一个较大的数据报,并要求该数据报完整通过,例如大小超过 1400 字节的 DNSSEC(DNS 安全扩展)响应。这些查询会超时,然后通过 TCP 重试。用户看到的通常是网站加载缓慢,而不是网站完全无法访问。
我的 VPS CPU 是瓶颈吗?
WireGuard 使用 ChaCha20-Poly1305 加密,并通过 Curve25519 交换密钥。数据路径中完全不使用 AES,因此有一个常见误区需要纠正:CPU 中的 AES-NI 指令对 WireGuard 没有作用。主机支持 AES-NI,并不代表它提供了 WireGuard 性能特性。选择 ChaCha20 是因为它在纯软件中运行速度很快,即使 CPU 完全没有加密加速功能也是如此。
但这不代表 WireGuard 不占用资源。在 1 vCPU 的 VPS 上,一个 CPU 核心既要处理加密和网络中断,还要处理应用本身的工作负载。
在传输运行期间进行测量:
sudo apt install -y sysstat
mpstat -P ALL 1查看三列数据。%soft 是 softirq 时间,内核数据包处理会计入此项。%steal 是虚拟机管理程序将 CPU 时间交给其他任务的时间。%idle 是剩余的 CPU 时间。
如果唯一的 CPU 核心上的 %soft 接近 100,说明该主机已达到数据包处理上限。这是真实的性能瓶颈,增加 CPU 核心可以提高上限。top 会在同一时刻显示进程列表顶部的 ksoftirqd/0,这是从另一个角度得出的相同结论。
%steal 超过几个百分点,说明这个限制不由你控制,因为宿主机超售,你的 vCPU 正在等待物理 CPU 核心。这在最便宜的共享套餐中很常见,而且会随一天中的时间变化。来自繁忙邻居的 steal time需要单独排查,调整 MTU 无法解决这个问题。
另一个因素是客户端使用的实现。Linux 内核模块是快速路径,并且会将同一个对等端的加密工作分散到多个 CPU 核心。wireguard-go 是用户空间实现,速度较慢。macOS 和 iOS 客户端使用它,因为这些平台不允许应用加载内核模块。
是路径受限,还是对端链路本身受限?
在调整任何参数前,先在同一客户端上间隔几分钟测量两个数值:不使用隧道时的吞吐量,以及使用隧道时的吞吐量。没有这两个数值,就只能猜测。
在服务器上运行 iperf3 -s。直接测试需要通过公网地址访问 TCP 5201,因此测试期间临时放行该端口,完成后删除规则。不要想当然地认为端口已关闭,应当确认端口已再次关闭。
# on the client, outside the tunnel
iperf3 -c 203.0.113.10
# then through the tunnel
iperf3 -c 10.8.0.1如果两个数值接近,说明 WireGuard 的开销很小,瓶颈在网络路径上。如果隧道数值明显低于直接连接数值,同时 %soft 保持较低,则应重新检查 MTU。分片会降低吞吐量,但不会导致连接中断,因此这里表现为百分比损失,而不是卡死。
测试两个方向,因为家庭网络通常具有上下行不对称性。iperf3 -c 10.8.0.1 -R 会反转数据流,使服务器发送数据。使用 500/20 线路的客户端向隧道发送数据时,上传速度不会超过 20 Mbit/s;服务器端的任何修改都无法改变这一点。
然后使用并行流测试:
iperf3 -c 10.8.0.1 -P 4如果4个流的总吞吐量远高于单个流,说明单个 TCP 连接无法填满这条路径。单个流的吞吐量受接收窗口除以往返时间的限制,因此对于 150 ms 的路径,需要较大的窗口才能传输大量数据。查看系统自身的限制:
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem每组输出中的第三个值是 Linux 自动调优允许达到的最大值。丢包同样会严格限制单个流,因为 TCP 拥塞控制会根据丢包作出反应,而长路径会增加恢复成本。在客户端运行 mtr -rwc 100 203.0.113.10 100 个周期,查看路径中的丢包从哪里开始。丢包从某一跳开始并持续到最终跳,才是真实丢包。某个中间跳出现丢包,但后续跳恢复正常,说明该路由器降低了 ICMP 的处理优先级,这不代表实际丢包。
如需获得可重复的服务器自身性能结果,并将其与网络状况分开,请使用有记录的方法测试 VPS 性能。这样可以在修改后再次运行相同测试,并进行可比对的比较。
WireGuard 无法解决的问题
WireGuard 是隧道。它的速度不可能超过所用路径中最慢的链路,而且加入隧道后,路径速度总会略有下降。
它不会压缩数据。它没有与 OpenVPN 的 comp-lzo 等效的功能,也没有实现该功能的计划,因为加密前压缩会泄露明文信息。大多数大容量数据本来就已经压缩,因此实际使用中不会因此付出额外代价。这是比较 WireGuard 与 OpenVPN 时需要权衡的实际差异之一,也是有意的设计选择。
全隧道会改变每个数据包经过的路由。原本从客户端直接发送到附近 CDN(内容分发网络)的流量,现在会先从客户端发送到 VPS,再转发到 CDN。如果 VPS 位于另一块大陆,每个请求都要经过这段绕行,往返时间也会相应增加。没有任何配置值可以缩短这段距离。请将 VPS 移到更近的位置,或使用分流隧道,让只有需要 VPN 的流量经过这条较长路径。流量的具体去向完全由 AllowedIPs 决定,加密密钥路由说明了这一决策过程。
服务商限制位于隧道之外,很容易被忽略。按月提供带宽额度的套餐通常会在额度用尽后,将端口限速到低得多的速度,此时隧道看起来就像发生了故障。在花费一晚排查 MTU 之前,请先检查服务商控制面板。
PersistentKeepalive 不会影响吞吐量。它用于保持 NAT 映射处于打开状态,使服务器仍能连接到家庭路由器后的客户端。将其降低到 25 秒以下只会增加数据包数量,无法解决任何问题。
按以下顺序进行测量
- 重现问题,并记录这是卡住还是速度均匀变慢。卡住通常指向 MTU;均匀变慢则不是。
- 使用
ping -M do从客户端到服务器的公网地址执行二分探测,并记录路径 MTU。 - 减去 80,在两端的 wg0 上设置该 MTU,然后重新测试之前失败的传输。
- 如果服务器为对端转发流量,请在服务器上添加 MSS clamping。
- 在传输期间运行
mpstat -P ALL 1,读取%soft和%steal。 - 在隧道外和隧道内分别运行
iperf3,测试两个方向,分别使用单流和-P 4。 - 运行
mtr -rwc 100连接服务器,检查是否存在一直持续到最后一跳的丢包。
只有在第 5 步表明 CPU 达到性能上限后,才应更换服务器方案。第 1 至第 4 步无需额外成本,并且可以解决大多数隧道速度慢的问题。
FAQ
为什么我的 WireGuard 隧道 ping 很快,但下载很慢?
这种差异通常说明存在 MTU 问题。小数据包可以适配路径上的每条链路,因此 ping 和 SSH 登录都能正常工作。批量传输会发送完整大小的数据段,封装后的数据段超过某条链路可接受的大小;如果该路由器丢弃数据包,且没有 ICMP 消息返回,就不会有任何组件报告丢包,传输也会卡住。使用 ping -M do 对服务器公网地址执行二分查找,确定路径 MTU,然后减去 80 字节作为封装开销,并在两端将结果设置为 wg0 的 MTU。
WireGuard 应设置多大的 MTU?
没有通用值,这正是部分用户使用默认值 1420 会失败的原因。1420 是 1500 减去 80 字节得出的结果,这 80 字节包括 WireGuard 标头、UDP 标头和外层 IPv6 标头。如果路径支持的大小小于 1500 字节(PPPoE DSL 中很常见,流量经过另一条隧道时也很常见),就需要使用更小的值。先测量路径 MTU,再从中减去 80。
MSS 限制能否替代设置 MTU?
不能。MSS 限制会改写 TCP 握手中的 MSS 选项,使两端发送更小的数据段,因此无需修改接口即可修复 TCP。UDP 没有可改写的握手,因此不受影响。MSS 限制也只适用于服务器转发的流量,因此在 WireGuard 服务器本机运行的服务不会受益。两者应同时使用:在接口上设置正确的 MTU,并通过 MSS 限制处理配置不受您控制的对端。
更快的 VPS 套餐能让 WireGuard 更快吗?
只有在 CPU 是瓶颈时才可以,而且一个命令就能说明这一点。传输运行期间执行 mpstat -P ALL 1。如果唯一的 CPU 核心上的 %soft 接近 100,说明数据包处理已达到上限,增加 CPU 核心数会提高性能。如果 %steal 较高,说明宿主机超额分配资源,此时应更换套餐或主机。如果隧道仍然很慢,但这两个数值都较低,说明 CPU 处于空闲状态,升级套餐不会有任何改善。
为什么我的 Mac 比同一网络上的 Linux 客户端慢?
Linux 客户端使用内核中的 WireGuard 模块,在内核空间处理数据包,并将一个对端的加密处理分布到多个 CPU 核心。macOS 和 iOS 应用使用 wireguard-go,也就是用户空间实现,因为这些平台不允许应用加载内核模块。用户空间实现需要在内核与应用之间复制每个数据包,这会降低吞吐量。这种差距是预期现象,没有任何客户端设置可以消除它。