「疎通確認? とりあえず ping 打ってみて」——。
インフラエンジニアの日常で何度繰り返されたか分からないこのフレーズ。しかし、あなたが管理しているプライベートサブネット内のインスタンスから、NATゲートウェイを経由してインターネットへ放たれたICMPパケットが、どうやって「帰り道」を見つけて戻ってくるのか、その詳細を正確に説明できるでしょうか。
TCPやUDPなら「ポート番号」があるから話は簡単です。しかし、ICMPにはポートという概念が存在しません。
今日は、教科書的な説明を一段掘り下げて、パケットがNATゲートウェイの「中身」をどう駆け抜け、エラー通知(TTL超過など)がどのようにして発信元に届けられるのか、その泥臭いメカニズムを解剖していきましょう。
—
1. ポートがない世界でどう識別するか:ICMP Query IDの魔術
NAT(ネットワークアドレス変換)の基本は、プライベートIPとソースポートの組み合わせを、パブリックIPと新しいソースポートの組み合わせに書き換えることです。しかし、ICMPはネットワーク層(レイヤー3)のプロトコルであり、トランスポート層の「ポート番号」を持ちません。
ここで登場するのが、RFC 3022でも言及されている 「Query Identifier(クエリ識別子)」 です。
ICMPエコーリクエストの書き換えフロー
ping(ICMP Echo Request)を実行したとき、パケットヘッダー内には16ビットの Identifier フィールドが含まれています。NATゲートウェイは、これをTCP/UDPのポート番号と同じように扱います。
1. プライベートインスタンス: 10.0.1.10 が Identifier: 500 でパケットを送信。
2. NATゲートウェイ: 自身のパブリックIP 203.0.113.1 に変換。このとき、内部の「NATテーブル」に 「10.0.1.10:500 ⇔ 203.0.113.1:12345」 のようなマッピングを記録します。
3. 宛先サーバ: 203.0.113.1 からの Identifier: 12345 を受け取り、同じIDで Echo Reply を返します。
4. NATゲートウェイ: 受信したパケットの Identifier: 12345 を見て、テーブルから 10.0.1.10:500 を引き当て、変換してプライベート側に流します。
これが、ポート番号がないICMPで双方向通信が成立する裏側にある仕掛けです。
—
2. 難所:ICMPエラーメッセージ(TTL超過)の返送メカニズム
実務でより重要なのは、ping よりも traceroute の挙動です。traceroute は、TTL(Time To Live)を1ずつ増やしながら送信し、途中のルーターが返す ICMP Time Exceeded (Type 11) を拾い集めます。
ここで疑問が生じます。「途中のルーターが返すエラーパケットには、元のQuery IDが含まれていないのではないか?」 という点です。
実は、ICMPエラーメッセージのペイロード(データ部分)には、「エラーを引き起こした元のIPヘッダーと、それに続く8バイト以上のデータ」 が含まれるというルールがあります。
ネットワークを駆け巡る「入れ子構造」の追跡
1. NATゲートウェイは、戻ってきた Time Exceeded パケットの中身を「深掘り」します。
2. エラーパケットのペイロード内にある「元の送信パケットのヘッダー」を見つけ出し、そこに書き込まれている Identifier を読み取ります。
3. その Identifier をキーにNATテーブルを逆引きし、プライベートサブネット内のどのインスタンスが送ったものかを特定します。
この「パケットの中のパケット」を解析する振る舞いこそが、NATゲートウェイが単なるルーターではなく、ステートフルなインテリジェンスを持っている証拠なのです。
—
3. 実践:Python(Scapy)でパケットの挙動を可視化する
実際にNAT越えのICMP挙動をデバッグする際、標準の ping コマンドだけでは限界があります。Pythonの Scapy ライブラリを使って、Identifierを手動で制御したパケットを生成し、NATゲートウェイがどう書き換えるかを観察してみましょう。
# scapyをインストール済みであること: pip install scapy
from scapy.all import IP, ICMP, sr1
# 1. カスタムIdentifierを持つICMPパケットの生成
# プライベートインスタンス上のIdentifierを「0x1234」に固定
target_ip = "8.8.8.8"
packet = IP(dst=target_ip) / ICMP(id=0x1234, seq=1)
print(f"--- Sending ICMP Echo Request to {target_ip} with ID: 0x1234 ---")
# 2. パケット送信と応答の待機
reply = sr1(packet, timeout=2)
if reply:
# 応答パケットのIdentifierを確認
# NATゲートウェイを通過して戻ってきた際、IDが書き換わっているか、
# あるいは透過的に戻っているかを確認できる(AWS NAT GWは基本保持するが、
# 競合時は書き換わる可能性がある)
print(f"Reply from {reply.src}")
print(f"Received ICMP ID: {hex(reply[ICMP].id)}")
print(f"Received ICMP Sequence: {reply[ICMP].seq}")
else:
print("No response received. Check Security Groups or Routing Tables.")
このコードをプライベートサブネットで実行し、同時にパブリック側のインターフェースや宛先サーバで tcpdump を動かせば、NATゲートウェイが id フィールドをどう操作したかが一目瞭然になります。
—
4. 現場でハマる「NATゲートウェイ×ICMP」の落とし穴
SREとして数々の「繋がらない」現場を見てきた経験から、特に注意すべきポイントを2つ挙げます。
① セキュリティグループの「ステートフル」の罠
AWSなどのセキュリティグループは「ステートフル」です。アウトバウンドでICMPを許可していれば、戻りのパケットは自動的に許可されます。しかし、ネットワークACL(NACL)は「ステートレス」 です。
もしNACLでインバウンドのICMPを絞っていると、NATゲートウェイまでは戻ってきているのに、そこからインスタンスに届かないという事態に陥ります。
② Tracerouteのデフォルトプロトコル
Linuxの traceroute は、デフォルトで UDP パケットを使用します。一方、Windowsの tracert は ICMP を使用します。
- Linux:
traceroute -I <宛先>(ICMPモードを使用) - 理由: NATゲートウェイの背後では、UDP高ポートがブロックされているケースが多く、デフォルトの
tracerouteが完走しないことが多々あります。
デバッグ時は、必ず curl や telnet だけでなく、プロトコルを明示した traceroute を使い分けるのがプロの鉄則です。
—
5. まとめ:パケットの「帰り道」を想像する
NATゲートウェイは、私たちの代わりに複雑なアドレス変換とマッピング管理を黙々とこなしてくれる、クラウドインフラの要です。
- ICMP Echo:
Identifierをポート番号の代わりにしてマッピング。 - ICMP Error: ペイロードに埋め込まれた「元のヘッダー」を覗き見て逆引き。
この仕組みを理解していれば、traceroute が特定のホップから先に進まないとき、それがNATゲートウェイの制限なのか、途中のルーターのポリシーなのか、あるいは戻りパケットを遮断しているNACLの設定ミスなのかを、パケットの挙動から論理的に推論できるようになります。
「魔法」を「技術」として理解すること。それが、信頼性の高いネットワークを構築するための第一歩です。
—
執筆者:クラウドSRE アーキテクト
数万ノードのKubernetesクラスター運用から、オンプレミス・クラウド間のBGPピアリングまで、ネットワークの深淵を愛するエンジニア。最近の悩みは、マネージドサービスのNATゲートウェイ料金をいかに削減するか。
コメント