「昨日まで動いていたAPIが、大きな画像データを投げた途端にタイムアウトするようになった」
「特定のVPN経由の通信だけ、レスポンスが途中で途切れる」
インフラの現場に身を置いていると、こうした「原因不明のパケットロス」に遭遇することがあります。ログを漁ってもアプリケーションエラーは見当たらない。そんな時、熟練のSREが真っ先に疑うのがMTU(Maximum Transmission Unit)と、それに伴うIPフラグメンテーション(断片化)の罠です。
今回は、パブリック/プライベートサブネットを繋ぐ「NATゲートウェイ」が、巨大なパケットをどのように解釈し、裏側でどのような再組み立て(Reassembly)と再分割(Fragmentation)を行っているのか。その深淵な挙動と、実務で役立つデバッグ手法を解き明かしていきます。
—
1. なぜNATゲートウェイでフラグメンテーションが問題になるのか
通常、イーサネットの標準的なMTUは 1500 バイトです。しかし、VPCピアリング、VPN、あるいは複雑なカプセル化(VXLANなど)を経由すると、実効MTUはそれ以下(例:1420 や 1280)に減少します。
ここで問題になるのが、NATゲートウェイはステートフルなデバイスであるという点です。
データの「辻褄」を合わせる苦労
NAT(Network Address Translation)は、プライベートIPをパブリックIPに書き換えます。このとき、単にIPヘッダーを書き換えるだけでなく、戻りのパケットを正しくルーティングするために「どの内側IPが、どの外側ポートを使って通信したか」を管理するセッションテーブルを保持します。
IPフラグメンテーションが発生すると、パケットは以下のように分割されます:
1. 第1フラグメント: IPヘッダー + TCP/UDPヘッダー + データの一部
2. 第2以降のフラグメント: IPヘッダー + 残りのデータ(L4ヘッダーがない!)
NATゲートウェイにとって、第2以降のパケットには「ポート番号」が含まれていません。これではセッションテーブルと照合できず、変換が不可能です。そのため、多くのクラウドネイティブなNATゲートウェイは、一度すべての断片をメモリ上で「再組み立て」してからNAT処理を行い、必要に応じて送り先のMTUに合わせて「再分割」するという高度な処理を行っています。
—
2. パケット再組み立てのシーケンスと挙動
NATゲートウェイ内部で何が起きているのか、その通信フローを脳内トレースしてみましょう。
1. Fragmentの到着: プライベートサブネットのEC2から、MTU 1500 で 2000 バイトのパケットが送出されると、OSレベルで2つに分割されます。
2. NAT GWによるバッファリング: NATゲートウェイは第1フラグメントを受け取ってもすぐには転送せず、後続のパケットを待ちます。
3. 再組み立て (Reassembly): Identification フィールドが一致する断片が揃った時点で、一つの大きなIPデータグラム(2000 バイト)を復元します。
4. NAT変換: 復元されたデータに対して、送信元IPをNAT GWのパブリックIPに書き換えます。
5. 再分割 (Re-fragmentation): 送出先のネットワークインターフェースのMTUを確認します。もしインターネット側への出口が 1500 であれば、再度 1500 と 500 の2つに分割して送り出します。
ここに潜むリスク:セキュリティとパフォーマンス
この「再組み立て」のプロセスは、リソースを消費します。
- DoS攻撃への脆弱性: 意図的に最後のフラグメントを送らないことで、NAT GWのメモリを枯渇させる攻撃(Teardrop攻撃の変種)が存在します。そのため、クラウドベンダーのNAT GWには厳格なタイムアウトとバッファ制限が設けられています。
- レイテンシの増加: パケットが揃うまで待機するため、ネットワークのジッター(揺らぎ)の原因になります。
—
3. 実務で使えるデバッグ・テクニック
「パケットが消えた」と思ったら、まずは以下のコマンドで PMTUD (Path MTU Discovery) が機能しているか確認するのが定石です。
pingによるMTU探索 (Linux/macOS)
Don't Fragment (DF) ビットを立てて、どのサイズまでならフラグメントせずに通るかをテストします。
# -M do : フラグメントを禁止する
# -s : ペイロードサイズ(IP+ICMPヘッダー28バイトを引いた値を指定)
# 1472 + 28 = 1500
ping -c 3 -M do -s 1472 8.8.8.8
# もしこれで "Frag needed and DF set" と出たら、その経路のMTUは1500未満
tcpdumpでの観察
NATゲートウェイの手前(EC2インスタンス上)で、フラグメントが発生しているかを確認します。
# 'ip[6] & 0x20 != 0' は More Fragments ビットが立っているパケットを抽出
# 'ip[6:2] & 0x1fff != 0' は Fragment Offset が 0 でないパケットを抽出
sudo tcpdump -i eth0 'ip[6] & 0x20 != 0 or ip[6:2] & 0x1fff != 0'
—
4. アプリケーション層での対策:MSSクランプ
インフラ側でMTUをいじるのが難しい場合(マネージドなNAT GWを使っている場合など)、最も現実的な解法はTCP MSS(Maximum Segment Size)クランプです。
TCP接続のハンドシェイク時に「パケットの最大サイズはここまでにしてね」と相互に合意させる手法です。
iptablesによる設定例
プライベートサブネット内のルーターやプロキシサーバーで設定する場合:
# TCPのSYNパケットに対して、MSSを強制的に 1300 バイトに書き換える
# これにより、アプリケーションは最初からフラグメントが発生しないサイズで通信を開始する
sudo iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN \
-o eth0 -j TCPMSS --set-mss 1300
Python (requests) での考慮
APIクライアント側で、あえて大きなデータを送る際の挙動を確認するコード例です。
import requests
# 巨大なダミーデータを生成
large_data = "A" * 1024 * 1024 # 1MB
try:
# タイムアウトを厳しめに設定して、フラグメンテーションによる遅延や欠落を検知
response = requests.post(
"https://api.example.com/upload",
data=large_data,
timeout=(3.0, 10.0) # (接続タイムアウト, 読み取りタイムアウト)
)
print(f"Status Code: {response.status_code}")
except requests.exceptions.Timeout:
print("Error: タイムアウト発生。MTU/MSSの不整合によるパケットロスの可能性があります。")
—
5. まとめ:シニアエンジニアの視点
NATゲートウェイにおけるIPフラグメンテーションの処理は、一見すると「クラウドが良しなにやってくれる魔法」のように見えます。しかし、その裏側ではRFC 791に基づいた泥臭い再組み立て処理が走っており、そこには必ずリソースの限界とタイムアウトが存在します。
Web APIの設計者やSREとして意識すべきは、以下の3点です。
1. PMTUDを信じすぎない: ICMPパケット(Destination Unreachable)がセキュリティグループで遮断されていると、Path MTU Discoveryは機能不全に陥ります。
2. MSSクランプは強力な武器: 不安定なネットワーク経路を介する場合、インフラ側でMSSを少し小さめ(1300〜1400程度)に絞るだけで、嘘のように通信が安定することがあります。
3. パケットの「塊」を意識する: 1MBのJSONを投げる時、それは物理層では700個以上のパケットに分割されています。NAT GWはその一つひとつに責任を持っているのです。
ネットワークは、繋がっているのが当たり前ではありません。パケットがNATの門をくぐる際の「苦労」を理解しておくことで、真に堅牢なシステムを構築できるようになります。
現場からは以上です。今日も良いパケットを!
コメント