ping も TCP 接続も通るのに、なぜ大きなレスポンスだけが返ってこないのか?
VPN 越しに社内 API を叩くと、/health は一瞬で 200 が返るのに、一覧 API だけが固まってタイムアウトする。ping は通り、curl -v を見ると TCP 接続も TLS の最初のやり取りも始まっている。小さいものは届き大きいものだけが消える経路で、何が起きているのだろうか。
- 経路上の各リンクには 1 パケットの上限サイズ(MTU)があり、トンネルやオーバーレイを通ると途中の上限が端の 1500 バイトより小さくなる。
- 送信側は「大きすぎたら ICMP で知らせてくれ」という前提で大きなパケットを送るため、その ICMP がファイアウォールで捨てられると大きなパケットだけが黙って消える(PMTUD ブラックホール)。
- 対策は ICMP Type 3 Code 4 / ICMPv6 Packet Too Big を通す、境界で MSS をクランプする、インターフェースの MTU をトンネルに合わせる、の 3 つである。
MTU と MSS: パケットには「通れる大きさ」がある
MTU(Maximum Transmission Unit)とは、あるリンクが 1 回で運べる IP パケットの最大バイト数である。AWS のドキュメントが述べるとおり、標準的な Ethernet v2 フレームは 1500 MTU を扱い、これがインターネットの大半で通る最大サイズになっている。
TCP はこの上限を前提に、1 セグメントに載せるデータ量を決める。その値が MSS(Maximum Segment Size: TCP セグメント 1 個に載せるデータ部分の最大バイト数)で、IPv4 では MTU から IP ヘッダ 20 バイトと TCP ヘッダ 20 バイトを引いた値になる。MTU 1500 なら MSS は 1460 で、各端は自分のインターフェースから計算した MSS を SYN に載せて相手に伝える。
ここに落とし穴がある。MSS を決めるのは両端のインターフェースであり、途中の経路は計算に入っていない。クライアントもサーバーも MTU 1500 の NIC を持っていれば、間に MTU 1400 のトンネルが挟まっていても、ハンドシェイクは 1460 で合意してしまう。
Path MTU Discovery: 「大きすぎた」を ICMP で教わる仕組み
途中に細い区間がある問題に対して IP が用意している仕組みが PMTUD(Path MTU Discovery: 経路全体で通る最小の MTU を送信側が学習する手順)である。送信側は IP ヘッダの DF ビット(Don't Fragment: 途中で分割するなという指示)を立てて送る。Linux では PMTUD を止める ip_no_pmtu_disc の既定が FALSE なので、TCP は普通この方式で送っている。
DF 付きのパケットが自分の MTU より大きいとき、ルータはパケットを分割せずに捨て、送信元に ICMP の Destination Unreachable: Fragmentation Needed and Don't Fragment was Set(Type 3, Code 4)を返す。このメッセージには「この先は何バイトまで」が書かれていて、送信側はそれを経路の MTU としてキャッシュし、小さく切り直して再送する。IPv6 ではルータが分割を一切しないため、同じ役割を ICMPv6 の Packet Too Big(Type 2)が担う。
クライアント VPN 区間 サーバー
MTU 1500 MTU 1400 MTU 1500
│ │ │
│ SYN (小) │ │
│───────────▶│─────────────▶│ 通る
│ │ 応答 1500B │
│ │◀─────────────│ DF 付き
│ │ 捨てる │
│ │ ICMP 3/4 │
│ │─────────────▶│
│ │ ✕ │
│ │ FW が ICMP │
│ │ を捨てる │
│ 届かない… │ │
▼ タイムアウト ▼
なぜわざわざ DF を立てるのか。途中のルータに分割させる方式も IPv4 にはあるが、断片が 1 つでも落ちるとパケット全体が失われ、分割と再組み立ての負荷もかかる。だから「端が経路に合わせて小さく送る」設計が選ばれた。その代わり、ICMP が送信元に届くことが前提になった。
高さ制限のある高架下を通る配送ルートに近い。トラックは「入れなければ手前の係員が制限の高さを書いたメモを本社に返す」約束で出発する。メモが届けば本社は荷を低く積み直して出し直せる。ところが途中の守衛が「怪しい郵便は全部捨てる」方針でメモを処分していると、本社は理由を知らないまま同じ高さで出し続ける。背の低い軽自動車(SYN や ping)は毎回通るので、ルート自体は生きているように見える。
ICMP を捨てると何が起きるか: PMTUD ブラックホール
ICMP は ping のための補助的なものだと思われがちで、「念のため ICMP は全部遮断」というファイアウォール設定は珍しくない。RFC 2923 もファイアウォールが ICMP を丸ごと抑制するよう誤設定されていることが多いと書いている。こうして Type 3 Code 4 が送信元に戻らなくなった状態が PMTUD ブラックホールである。
症状は冒頭の謎とそのまま重なる。SYN は小さいので通りハンドシェイクは成功し、小さな対話的通信も動く。しかし RFC 2923 が書くとおり、一括転送は最初の大きなパケットで止まり、接続はやがてタイムアウトする。ping も通るので、「ネットワークは生きている、アプリが遅い」と誤診しやすい。
帯域の問題に見えるが、実際には大きさの問題である。同じ API でも件数が少ないときは応答が MTU に収まって返り、件数が増えて 1 パケットを超えた途端に固まる。データ量で再現が変わるため、アプリのバグと区別しにくい。
MTU が縮む場所と、3 つの対策
MTU が端より小さくなるのは、元のパケットに外側のヘッダを足して運ぶ区間である。Calico のドキュメントによれば、IPv4 の VXLAN は 50 バイト、IP-in-IP は 20 バイト、IPv4 の WireGuard は 60 バイトのヘッダを足すので、物理 MTU 1500 の上では Pod 側の MTU をそれぞれ 1450 / 1480 / 1440 にすることが推奨されている。逆向きの段差もあり、AWS の VPC 内は最大 9001 MTU のジャンボフレームを使えるが、インターネットゲートウェイと VPN 接続を通る通信は 1500 に制限される。
1 つ目の対策は ICMP を通すことである。AWS はセキュリティグループで ICMP を許可していても、サブネットのネットワーク ACL が ICMP を拒否していればやはり遮断されると注意している。ping(Echo)を許すかどうかとは別に、Type 3 Code 4 と ICMPv6 Packet Too Big は通す、と分けて考える。
2 つ目は、細い区間の入口にあるルータで SYN に載った MSS を書き換える MSS クランプである。iptables の TCPMSS ターゲットに --clamp-mss-to-pmtu を付けると、MSS を経路 MTU から IPv4 なら 40、IPv6 なら 60 を引いた値まで下げる。両端は最初から小さいセグメントで話すので、ICMP に頼らずに済む。
# VPN ゲートウェイ等の転送経路で SYN の MSS を下げる iptables -t mangle -A FORWARD -p tcp \ --tcp-flags SYN,RST SYN \ -j TCPMSS --clamp-mss-to-pmtu
ただし man ページは、行きと帰りで経路 MTU が違う非対称経路では期待どおり動かないことがあると注意している。3 つ目は、トンネルの内側にあるインターフェースの MTU 自体を小さく設定することだ(ip link set dev wg0 mtu 1420 のように)。MTU を合わせておけば両端の MSS は最初から正しい値になる。Calico のように MTU を自動検出する CNI もあるが、使わないカプセル化を有効のままにしていると検出を誤ることがある。
端末側の保険として、Linux には net.ipv4.tcp_mtu_probing がある。これは ICMP に頼らず TCP 自身が小さいサイズを試して経路 MTU を探る方式(Packetization-Layer PMTUD)を制御し、0 で無効、1 でブラックホールを検知したときだけ有効、2 で常に有効(初期 MSS は tcp_base_mss)になる。1 にすると、ブラックホールを検知した接続は再送タイムアウトを経て小さいサイズに落とすので、固まり続ける代わりに「最初だけ遅い」接続で済むことが多い。
典型的なのは「VPN を繋ぐと特定の API だけ固まる」「Kubernetes で Pod 間の小さなリクエストは通るのに、大きなレスポンスや docker pull が途中で止まる」「Terraform でネットワーク ACL を絞ったら一部の外部通信だけタイムアウトするようになった」といった場面である。データサイズで再現が変わり、ping と TCP 接続は正常なら、MTU を疑う価値がある。Cloudflare も 2015 年に、ECMP でパケットを複数サーバーに振り分けた結果、ICMP が TCP 接続を持たない別サーバーに届き、トンネル経由の一部ユーザーが接続できなくなった事例を公開している。ICMP を通していても、届く先がずれれば同じ症状になる。
自分の Mac から外へ出られるパケットの大きさを、DF ビット付きの ping で確かめる。-s は ICMP のデータ部のサイズで、IP ヘッダ 20 バイトと ICMP ヘッダ 8 バイトを足したものがパケット長になる。
# インターフェースの MTU を見る(Wi-Fi は多くの場合 en0) ifconfig en0 | grep mtu # 1472 + 28 = 1500 バイト、DF 付き ping -D -s 1472 -c 3 1.1.1.1 # 1 バイト増やして 1501 バイト ping -D -s 1473 -c 3 1.1.1.1
MTU 1500 の環境なら、1472 は応答が返り、1473 は手元のインターフェースで送信できずエラーになるはずだ(自分の MTU を超えるパケットはカーネルが送る前に拒否する)。VPN を繋いだ状態や PPPoE 回線では、1472 でも応答が返らなくなる場合がある。その場合は -s を少しずつ減らし、応答が返る最大値に 28 を足せば経路の MTU の目安になる。
- ICMP は ping 用なので全部塞いでも困らない — Type 3 Code 4 と ICMPv6 Packet Too Big は TCP の通信サイズ調整に使われている。塞ぐと大きなパケットだけが消える。
- MSS は経路に合わせて自動で決まる — SYN で交換する MSS は両端のインターフェースの MTU から計算した値で、途中のトンネルは考慮されない。経路への適応は PMTUD か MSS クランプが担う。
- MTU は大きいほど速いので、ジャンボフレームにしておけば得をする — 効果があるのは経路全体が大きな MTU を通す区間だけである。AWS ではインターネットゲートウェイや VPN を通る通信は 1500 に制限され、超えた分は分割されるか、DF 付きなら捨てられる。
- MTU
- リンクが 1 回で運べる IP パケットの最大バイト数。標準的な Ethernet では 1500。
- MSS
- TCP セグメント 1 個に載せるデータ部分の最大バイト数。IPv4 では MTU − 40 で、SYN で相手に伝える。
- DF ビット
- IPv4 ヘッダの「途中で分割するな」フラグ。立っていると、大きすぎるパケットはルータで捨てられる。
- PMTUD(Path MTU Discovery)
- 大きすぎたときに返る ICMP を手がかりに、経路全体で通る最小 MTU を送信側が学習する仕組み。
- PMTUD ブラックホール
- その ICMP が途中で捨てられ、大きなパケットだけが理由を知らされないまま消え続ける状態。
- MSS クランプ
- 経路上のルータが通過する SYN の MSS を細い区間に合わせて書き換え、両端に小さく送らせる手法。
- RFC 2923: TCP Problems with Path MTU Discovery — ブラックホールの症状(ハンドシェイクは通り一括転送で止まる)と、TCP 側での検知・回避の考え方が整理されている。
- Network maximum transmission unit (MTU) for your EC2 instance — VPC 内 9001 と外向き 1500 の境界、PMTUD とセキュリティグループ・ネットワーク ACL の関係が一枚で分かる。
- Calico: Configure MTU to maximize network performance — VXLAN・IP-in-IP・WireGuard それぞれのヘッダ長と推奨 MTU の対応表。
- Cloudflare Blog: Path MTU discovery in practice — ECMP で ICMP が別サーバーに届いてしまう、ICMP を許可しているのに壊れるケースの実例。