パケットが「黒塗り」される恐怖:PMTUDとICMPが握るネットワークの生命線
インフラエンジニアのキャリアにおいて、避けては通れない「ブラックホール現象」がある。特定の通信だけがなぜかタイムアウトする、あるいはコネクション確立後のデータ転送でパケットがピタリと止まる。Wiresharkを立ち上げ、SYNパケットが飛び交うのを見つめながら、「なぜACKが返ってこないのか」と溜息をついた経験は一度や二度ではないはずだ。
その元凶の多くは、MTU(Maximum Transmission Unit)の不一致と、それに伴う「パケットの沈黙」にある。今日は、クラウド環境のネットワークの深淵、特にIGW(Internet Gateway)とNATゲートウェイを巡るPMTUD(Path MTU Discovery)の挙動について、現場の血肉となった知見を共有したい。
—
なぜ「断片化」は現代のネットワークで嫌われるのか
かつてIP層での断片化(Fragmentation)は、ルーターの基本的な機能として当然視されていた。しかし、今日のハイパフォーマンスなクラウドネットワークにおいて、フラグメンテーションは「悪」である。
1. CPU負荷の増大: ルーターやロードバランサーがフラグメントの再構築や分割を行うのは、ステートフルな負荷を強いる行為だ。
2. セキュリティリスク: 断片化されたパケットを悪用したIDS/IPS回避や、リソース枯渇攻撃の踏み台にされやすい。
3. ドロップ率の向上: 多くのモダンなファイアウォールやセキュリティグループは、断片化されたパケットを最初から破棄するように設計されている。
だからこそ、私たちは「断片化させるな(DFビットを立てろ)」というポリシーを採用し、その結果としてPMTUDを正しく機能させる必要がある。
—
PMTUDの死角:ICMP Type 3 Code 4の帰還
PMTUDのメカニズムはシンプルだ。送信元ホストはIPヘッダーに DF(Don’t Fragment)ビットを立ててパケットを送出する。経路上のリンクMTUを超えるパケットに遭遇すると、ルーターはパケットを破棄し、送信元へ「ICMP Type 3 Code 4(Destination Unreachable, Fragmentation Needed and DF set)」を送り返す。
ここで問題になるのが、プライベートサブネット内のインスタンスだ。
NATゲートウェイ越しにインターネットと通信する場合、帰りのICMPメッセージはNATゲートウェイを経由してインスタンスに戻らねばならない。もし、NATゲートウェイの背後のネットワークACLやセキュリティグループが、このICMPメッセージを「不要なトラフィック」として遮断していれば、PMTUDは機能不全に陥る。これが「コネクションは確立するが、大きなペイロードを流すと固まる」という悪夢の正体だ。
—
現場で打つべき対策:ブラックホールを防ぐ設定
1. セキュリティグループとACLの盲点
まず、インスタンスのセキュリティグループ(インバウンド)において、ICMPタイプ3の全コードを許可することを強く推奨する。
# AWS CLI例: ICMP 3:4 (Fragmentation Needed) を許可するためのルール追加
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxx \
--protocol icmp \
--port -1 \
--icmp-type-code type=3,code=4 \
--cidr 0.0.0.0/0 # 実際にはNATゲートウェイのIP範囲に絞るのがベスト
2. TCP MSS クランプの強制
PMTUDがうまく機能しないケース(例えば、特定のパスでICMPが常にフィルタリングされている場合)に備え、OSレベルで MSS(Maximum Segment Size)を調整する。これにより、TCPハンドシェイクの段階でパケットサイズを強制的に絞り込む。
Linuxカーネルの iptables を使用した強硬手段は以下の通りだ。
# TCP SYNパケットのMSSを1360バイトに制限し、オーバーヘッドを考慮する
# 1500(MTU) - 20(IPヘッダー) - 20(TCPヘッダー) = 1460だが、
# VPNやトンネリングを考慮し、1360程度まで下げるのが経験則。
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --set-mss 1360
—
パフォーマンスの極致:TLSハンドシェイクとバッファチューニング
MTUの最適化は、単なる接続性の問題ではない。TLS 1.3のような現代のプロトコルでは、ハンドシェイクのパケットサイズが重要になる。
パケットがMTUを超えて分割されると、RTTが1往復増えるのと同等のレイテンシロスが発生する。これを防ぐためには、TCP_NODELAY を有効にし、かつバッファサイズを適切にチューニングする必要がある。
import socket
# ソケットレベルでバッファを最適化する例
def optimize_socket(sock):
# Nagleアルゴリズムを無効化(小パケットを即時送信)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# 送受信バッファを拡大(高スループット環境用)
# 大規模なTLSデータを扱う際にパケットロスによる再送負荷を低減する
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 262144)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 262144)
—
最後に:ネットワークを「見る」エンジニアであれ
「ネットワークは繋がっていて当たり前」という時代だが、クラウドの抽象化されたレイヤーの裏側では、相変わらずIPプロトコルの厳格なルールが支配している。
もしあなたが大規模なマイクロサービスを運用しているなら、NATゲートウェイのメトリクスを監視するだけでは足りない。パケットがどこでドロップしているのかを、ICMPのフローという視点から追いかける能力こそが、トラブルシューティングの最終兵器となる。
教科書を閉じて、tcpdumpを握りしめろ。パケットが教えてくれる真実は、常にドキュメントの行間にあるのだから。
コメント