WireGuardの速度低下はMTUが原因?実際の切り分け方
WireGuardが遅い原因はMTUとは限りません。経路MTUを二分探索で調べ、TCP MSSを調整し、steal timeとトンネル前の速度を測定して原因を切り分けます。
WireGuard の速度低下が実際に発生する原因
WireGuard の速度低下は、4 つの原因のいずれかに起因します。ただし、4 つが同じ確率で発生するわけではありません。1 つ目は MTU (maximum transmission unit) です。トンネルが経路上のいずれかのリンクで送信できない大きさのパケットを生成するため、大容量転送が停止し、小さな通信は正常に見えます。2 つ目は経路そのものです。トンネルを構築する前から、その経路が速度の上限になっていた可能性があります。3 つ目は、小規模な共有 VPS の CPU です。同じホスト上にいる他のゲストと暗号化処理が CPU を奪い合います。4 つ目は、peer 自身の接続です。
この順番で確認します。MTU を最初に確認するのは、一覧の中で WireGuard 自体が追加する唯一の原因だからです。また、その症状は速度低下には見えません。MTU が正しくない場合、通常はトンネルがすぐに接続し、ping に応答し、SSH ログインも受け付けます。しかし、最初にファイルをコピーした時点で停止します。
この確認を始める前に、1 つの症状を切り分けておきます。新しいサイトを開くたびに読み込み開始まで数秒かかり、その後は最大速度で転送される場合、原因はスループットではなく名前解決です。WireGuard 経由の DNS には固有の障害要因があり、MTU を変更しても解決しません。
WireGuard の MTU はなぜ 1420 なのか
トンネルに送信するすべてのパケットは暗号化され、新しいパケットの中にカプセル化されます。カプセル化には数バイト必要で、その分だけペイロードが減ります。
WireGuard のデータヘッダーは 32 バイトです。内訳は、4 バイトのタイプフィールド、4 バイトの受信者インデックス、8 バイトのカウンター、16 バイトの Poly1305 認証タグです。その外側に 8 バイトの UDP ヘッダーがあります。さらにその外側に外側 IP ヘッダーがあり、IPv4 では 20 バイト、IPv6 では 40 バイトです。したがって、カプセル化の合計は Endpoint が IPv4 アドレスの場合は 60 バイト、IPv6 の場合は 80 バイトです。これらの数値の根拠となるメッセージ形式は、WireGuard protocol page に記載されています。
wg-quick はこの値を推測していません。Endpoint への経路に使用するインターフェースの MTU を読み取り、そこから 80 を引いています。通常の 1500 バイトの Ethernet 経路では 1420 になり、これが ip link show wg0 の出力値です。60 ではなく 80 を引くのは、そのエンドポイントに 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 エンドポイントを使用する正常な 1500 バイト経路では、1440 が収まります。そのため、デフォルトの 1420 では 20 バイトが未使用になります。この余裕は意図的なものであり、問題ではありません。
問題になるのは最後の行です。経路上の一部のリンクが 1400 バイトしか処理できない場合、1420 に設定されたトンネルは、最大サイズの各セグメントで 1500 バイトの外側パケットを生成します。これは、そのリンクが受け入れられるサイズを 100 バイト超えています。収まる値は 1320 です。
1320 もそのまま使用しないでください。パス MTU は実際の経路に依存するため、知るには測定するしかありません。
MTU が誤っている場合の症状
この障害は徐々に進行しません。小さいパケットと大きいパケットで、明確に結果が分かれます。
pingは、通常のサイズであればトンネル経由でも動作します。- SSH ログインは完了し、入力にも遅延を感じません。
curl -I https://example.comは、すぐにヘッダーを返します。- 大きなページに対する
curl https://example.comは、最初の数キロバイトを受信した後に停止します。 - 大きなファイルの
scpは開始しますが、ある割合まで進むと停止します。 - 大量の出力を表示するコマンドを実行した瞬間に、SSH セッションがフリーズします。
これは、TCP 接続が大量のデータを転送する段階になって初めて、最大サイズのセグメントを作成するためです。ハンドシェイクと最初のリクエストは、経路上のどの MTU よりも小さいサイズに収まります。停止が始まるのは、最初の最大サイズのセグメントを送信するときです。そのため、接続は使用できなくなる直前まで正常に見えます。
次のリンクの MTU より大きい外側パケットは、2 つのいずれかの結果になります。
フラグメント化される。 ルーターがパケットを分割し、宛先側が各断片を再構成します。転送は成功しますが、1 つのパケットに対して 2 つのパケットを処理することになり、受信側は両方が到着するまで状態を保持するため、速度は低下します。1 つの断片を失うと、元のパケット全体を失います。そのため、パケット損失率が 1% の経路は、実際にはそれより大幅に悪い経路として動作します。多くのファイアウォールはポリシーによって IP フラグメントも破棄するため、この結果は次の結果に変わります。
破棄され、通知される場合と通知されない場合がある。 フラグメント化を行わないルーターは、ICMP (internet control message protocol) の "fragmentation needed" メッセージを送信元へ返します。このメッセージには、受け入れ可能な MTU が含まれます。メッセージが到着すれば、path MTU discovery が機能し、送信元は自動的にセグメントサイズを小さくします。多くのネットワークは ICMP をフィルタリングするため、メッセージが届かないことがよくあります。損失を通知する別の仕組みはありません。これがブラックホールです。パケットは送信されますが何も返らず、どちらの端のログにもエラーが現れず、何かがタイムアウトするまで転送が停止します。
適切な MTU を見つけるにはどうすればよいですか?
経路を測定してから、カプセル化分を差し引きます。トンネルではなく下位ネットワークをテストします。そのため、クライアントからサーバーの公開アドレスへ、フラグメントを禁止して ping を実行します。
ping -M do -s 1472 -c 3 203.0.113.10-M do は DF(フラグメント禁止)ビットを設定するため、経路上のルーターはパケットを分割できません。-s は ICMP ペイロードのサイズです。完全な IPv4 パケットは、このペイロードに ICMP ヘッダー 8 bytes と IP ヘッダー 20 bytes を加えたものです。そのため、-s 1472 を指定すると、wire 上でちょうど 1500 bytes になります。
重要なのは 3 種類の結果です。正常な応答が返れば、1500 bytes は通過できるため、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% packet loss になり、より小さいサイズでは正常な応答が返る場合は、ブラックホール状態です。どのルーターからも情報が返らないため、サイズを半分ずつ絞り込んで境界を探します。確実に通るサイズと、確実に失敗するサイズを 1 つずつ保持し、中間値をテストします。結果に応じて該当する境界を移動します。各回で残りの範囲が半分になるため、5 回または 6 回で十分です。
二分探索の例(1 回ずつ実行)
各行は、クライアントでサーバーの公開アドレスに対して実行するコマンドです。コメントには返された結果を記録しています。
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 bytes を通過でき、1412 bytes 未満であることが分かります。安全側の値を採用します。経路 MTU 1400 からカプセル化分 80 bytes を差し引くと、wg0 の MTU は 1320 になります。
tracepath は同じ探索を自動的に実行するため、二分探索を始める前に 1 回実行する価値があります。
tracepath -n 203.0.113.10最後の行に検出結果が表示されます。
Resume: pmtu 1492 hops 12 back 12どちらのツールも、確定値ではなく出発点として扱ってください。一部のホストは ICMP をレート制限するか、完全に破棄します。そのため、二分探索で実際の経路より小さい MTU が報告される場合があります。失敗していた転送が、実際のテストになります。
まず値を一時的に適用します。誤った値でも、1 コマンドで元に戻せるためです。
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 は個々のインターフェースの属性であり、ピア間でネゴシエーションされるものではありません。クライアントに設定した場合、クライアントが送信するパケットだけが小さくなります。サーバーは自身の wg0 MTU に基づいてパケットを生成するため、アップロードが機能し始めても、ダウンロードがブラックホールになる場合があります。両端に値を設定するか、サーバーで MSS をクランプしてください。
MSS クランプで TCP だけが修正される理由
NAT(network address translation)を使用する 標準的な WireGuard VPS 構成で、サーバーがピアのトラフィックを転送している場合、1 つのルールですべてのピアの TCP を修正できます。管理していない各クライアントで値を個別に調整する必要もありません。
MSS(maximum segment size)は、各側が SYN パケットに設定する TCP オプションです。受信できるセグメントのサイズを示します。クランプは、通過中にこのオプションを書き換え、実際の経路 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 を使用する環境では、同等の設定は 1 行です。
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 のままなら、パケットはそのフックを通過していません。そのため、ルールは機能していません。
ここで、適用範囲を正確に確認します。クランプが対象にするのは TCP だけです。また、対象は転送トラフィックだけです。WireGuard サーバー自体で動作するサービスは forward フックを通過しないため、クランプされません。ルールの読み込み後に開かれた接続だけにも適用されます。既存のセッションでは、すでに合意した MSS が維持されます。
UDP は変更されません。UDP には書き換え可能なハンドシェイクがないためです。それでも、ほとんどの UDP トラフィックは正常に動作します。HTTP/3 のトランスポートである QUIC は、使用可能なパケットサイズを独自に検出し、意図的に小さいサイズから開始します。一方、1 つの大きなデータグラムを送信し、そのまま到達することを前提とする UDP は動作しないことがあります。たとえば、1400 bytes を超える DNSSEC(DNS security extensions)の応答です。このようなクエリはタイムアウトし、TCP で再試行されます。その結果、ユーザーにはサイトが壊れた状態ではなく、遅いサイトとして現れます。
VPS の CPU が制限要因ですか?
WireGuard は ChaCha20-Poly1305 で暗号化し、Curve25519 で鍵を交換します。データパスに AES は一切なく、ここで誤解されやすい点が 1 つあります。CPU の AES-NI 命令は WireGuard では何の効果もありません。AES-NI 対応をうたうホストでも、WireGuard の性能機能が提供されるわけではありません。ChaCha20 は、暗号アクセラレーションをまったく備えていない CPU を含め、ソフトウェアだけで高速に動作するため採用されています。
ただし、WireGuard の処理負荷がゼロになるわけではありません。1 vCPU の VPS では、アプリケーションの処理に加えて、1 つのコアが暗号化とネットワーク割り込みの両方を処理します。
転送を実行しながら測定します。
sudo apt install -y sysstat
mpstat -P ALL 13 つの列を確認します。%soft は softirq 時間で、カーネルによるパケット処理に使われた時間です。%steal はハイパーバイザーが別の処理に割り当てた時間です。%idle は残りの時間です。
1 つしかないコアで %soft が 100 近くになる場合、そのサーバーはパケット処理の上限に達しています。これは実際の制限であり、コアを増やすと上限を引き上げられます。同じ時点でプロセス一覧の先頭に ksoftirqd/0 が表示されることを top で確認できます。これは別の角度から見た同じ結果です。
%steal が数パーセントを超える場合、その制限は利用者側では解決できません。ホストが過剰に割り当てられており、vCPU が物理コアの空きを待っているためです。これは最安の共有プランでよく発生し、時間帯によって変動します。近隣の利用者による steal time については個別に調査が必要であり、MTU の値を変更しても解決しません。
もう 1 つの要因は、クライアントがどの実装を使用しているかです。Linux カーネルモジュールは高速な処理経路であり、1 つの peer の暗号化処理を複数のコアに分散します。wireguard-go はユーザー空間実装で、処理速度は低下します。macOS と iOS のクライアントがこれを使用するのは、これらのプラットフォームではアプリケーションがカーネルモジュールをロードできないためです。
経路の問題か、それともピア側のリンク自体の問題か
チューニングを始める前に、同じクライアントから数分間隔で 2 つの数値を取得します。トンネルなしのスループットと、トンネル経由のスループットです。この組み合わせがなければ、推測で判断することになります。
サーバーで 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.12 つの数値が近ければ、WireGuard による性能低下はごく小さく、制限要因は経路です。トンネル経由の数値が直接接続より大幅に低く、%soft が低いままであれば、MTU の確認に戻ります。断片化は接続を停止させずにスループットを低下させるため、ここではハングではなく、割合で示される損失として現れます。
通常、家庭向け接続は非対称なので、両方向をテストします。iperf3 -c 10.8.0.1 -R はフローを反転し、サーバーから送信します。500/20 回線のクライアントがトンネルへ送信できる量は、20 Mbit のアップロード速度を超えません。サーバー側を変更しても、この制限は変わりません。
次に、並列ストリームでテストします。
iperf3 -c 10.8.0.1 -P 44 本のストリームを合計した速度が 1 本の場合を大きく上回るなら、単一の TCP 接続が経路を十分に使い切れていません。1 本のストリームのスループットは、受信ウィンドウをラウンドトリップ時間で割った値に制限されます。そのため、150 ms の経路で大量のデータを転送するには、大きなウィンドウが必要です。自身の上限を確認します。
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem各出力の 3 番目の値は、Linux が自動調整で設定できる最大値です。パケット損失も単一ストリームを大きく制限します。TCP の輻輳制御はパケット損失に反応し、経路が長いほど回復にコストがかかるためです。クライアントから mtr -rwc 100 203.0.113.10 を 100 サイクル実行し、経路上のどこで損失が発生するかを確認します。あるホップで始まった損失が最終ホップまで続くなら、実際の損失です。途中の 1 ホップで発生した損失が、その後のホップで消えるなら、そのルーターが ICMP の優先度を下げているだけであり、意味のある損失ではありません。
ネットワークとは分けてサーバー自体を再現性のある形で評価するには、手順を記録した方法で VPS のベンチマークを実行すると、変更後に同じテストを再実行し、同じ条件で比較できます。
WireGuardで解決できないこと
WireGuardはトンネルです。経路上で最も遅いリンクより速くなることはなく、追加すると必ず経路がわずかに遅くなります。
圧縮は行いません。OpenVPNのcomp-lzoに相当する機能はなく、実装予定もありません。暗号化前に圧縮すると、平文に関する情報が漏れるためです。大半の大容量データはすでに圧縮されているため、実際の運用でこの制限が問題になることはほとんどありません。これは、WireGuardとOpenVPNを比較する際に検討すべき実質的な違いの1つであり、意図的な設計上の選択です。
フルトンネルでは、すべてのパケットが通る経路が変わります。以前は利用者から近くのCDN(コンテンツ配信ネットワーク)へ直接送っていたトラフィックが、利用者からVPSへ送られ、その後CDNへ転送されます。VPSが別の大陸にある場合、すべてのリクエストでこの迂回が発生し、それに応じてラウンドトリップ時間も長くなります。設定値で短縮することはできません。VPSを近くに移すか、VPNが必要なトラフィックだけが遠回りするようにスプリットトンネルを使用してください。トラフィックの振り分けはAllowedIPsだけで決まり、cryptokey routingがその決定方法を説明します。
プロバイダーの制限はトンネルの外側にあり、忘れやすい点です。月間帯域幅の上限があるプランでは、上限に達するとポートの速度が大幅に制限されることが多く、その結果、トンネルが壊れたように見えます。MTUの調整に時間をかける前に、プロバイダーの管理パネルを確認してください。
PersistentKeepaliveはスループットに影響しません。自宅のルーター配下にあるクライアントへサーバーから到達できるよう、NATマッピングを維持するために使用します。25秒未満に設定してもパケットが増えるだけで、問題は解決しません。
この順序で測定します
- 問題を再現し、ハングなのか、一様な速度低下なのかを記録します。ハングなら MTU が原因である可能性があります。一様な速度低下は MTU を示しません。
- クライアントからサーバーのパブリックアドレスまで
ping -M doで二分探索し、パス MTU を記録します。 - 80 を差し引き、その MTU を双方の wg0 に設定します。その後、失敗していた転送を再テストします。
- サーバーが peer のトラフィックを転送する場合は、サーバーに MSS クランプを追加します。
- 転送中に
mpstat -P ALL 1を実行し、%softと%stealを確認します。 - トンネルの外側と内側で、両方向について、単一ストリームの場合と
-P 4を使う場合にiperf3を実行します。 - サーバーに対して
mtr -rwc 100を実行し、最後のホップまで継続するパケット損失を探します。
CPU が上限に達していると step 5 で確認できた場合に限り、サーバーのプランを変更します。Steps 1 から 4 は費用がかからず、トンネルが遅いという報告の大半を解決できます。
FAQ
WireGuard トンネルでは ping が速いのに、ダウンロードが遅いのはなぜですか?
この症状は MTU の問題を示しています。小さいパケットは経路上のすべてのリンクに収まるため、ping と SSH ログインは正常に動作します。大量転送では最大サイズのセグメントが送信され、それをカプセル化したパケットが一部のリンクの許容サイズを超えます。そのパケットをルーターが破棄し、ICMP メッセージも返さない場合、損失が通知されず、転送が停止します。ping -M do による二分探索でサーバーの公開アドレスまでのパス MTU を調べ、カプセル化分として 80 バイトを引き、その値を両端の wg0 の MTU に設定してください。
WireGuard の MTU には何を設定すべきですか?
共通の値はありません。そのため、デフォルトの 1420 では問題が発生する場合があります。1420 は、1500 から WireGuard ヘッダー、UDP ヘッダー、外側の IPv6 ヘッダーの合計 80 バイトを引いた値です。経路上で 1500 バイト未満しか通過できない場合は、より小さい値が必要です。PPPoE DSL や、別のトンネルを経由する経路では、この状況が一般的です。まずパス MTU を測定し、そこから 80 を引いてください。
MSS クランピングを設定すれば、MTU の設定は不要ですか?
いいえ。MSS クランピングは TCP ハンドシェイクの MSS オプションを書き換え、両端がより小さいセグメントを送信するようにします。これにより、インターフェースを変更せずに TCP の問題を解決できます。UDP には書き換え対象のハンドシェイクがないため、影響を受けません。また、クランピングが適用されるのはサーバーが転送するトラフィックだけです。そのため、WireGuard サーバー自体で動作するサービスには効果がありません。インターフェースには正しい MTU を設定し、構成を管理できないピアにはクランピングを適用するという、両方の方法を使用してください。
より高速な VPS プランに変更すると WireGuard は速くなりますか?
CPU が制限要因である場合に限り、速くなります。転送中に mpstat -P ALL 1 を実行してください。唯一のコアで %soft が 100 近くに達している場合、パケット処理が上限に達しており、コア数を増やすと性能が向上します。%steal が高い場合は、ホスト上でリソースが過剰に共有されています。その場合は、別のプランまたは別のホストを選択してください。トンネルが遅いまま両方の数値が低い場合、CPU はアイドル状態です。より大きなプランに変更しても改善しません。
同じネットワーク上にあるのに、Mac が Linux クライアントより遅いのはなぜですか?
Linux クライアントはカーネル内の WireGuard モジュールを使用します。パケットをカーネル空間で処理し、1 つのピアの暗号化処理を複数の CPU コアに分散します。macOS と iOS のアプリは wireguard-go というユーザー空間実装を使用します。これらのプラットフォームでは、アプリがカーネルモジュールを読み込めないためです。ユーザー空間実装では、各パケットをカーネルとアプリケーションの間でコピーする必要があり、このコピーによってスループットが低下します。この差は想定されるものであり、クライアント設定で解消することはできません。