深夜3時、アラートの音がNOCの静寂を切り裂く。東日本リージョンを繋ぐ主要なバックボーンの一部でパケットロスが跳ね上がり、顧客からの「特定のAPIエンドポイントへ疎通できない」という悲鳴のような問い合わせがチケットシステムを埋め尽くしていく。こういう修羅場で、私たちベテランエンジニアが真っ先に叩くのは、決まって traceroute だ。
しかし、ここでひとつの罠がある。あなたの手元にある端末がWindowsか、あるいはLinuxやmacOSかによって、そのパケットがルーターのセキュリティポリシーにどう裁かれるかが根本的に変わることを意識したことはあるだろうか?
今回は、OSの実装差異によって運命が分かれる「ICMPベースのtraceroute(Windows tracert 等)」のパケットレベルの挙動に焦点を当て、ファイアウォールの向こう側で何が起きているのか、そして私たちが現場でどうこの挙動をハックしトラブルシューティングに活かしているのかを、徹底的に解き明かしていこう。
—
1. パケットの裏側:なぜUDPではなくICMPなのか
一般的なUnix系OSの traceroute は、デフォルトでUDPパケット(宛先ポートは33434からインクリメント)を使い、TTL(Time To Live)を1から順に加算して送信する。ルーターはTTLが0になったパケットを受け取ると、破棄すると同時に送信元へ ICMP Time Exceeded (Type 11, Code 0) を送り返す。この挙動を利用して経路上のルーターIPを暴いていくわけだ。
これに対し、Windowsの標準コマンドである tracert は、歴史的背景とファイアウォールのトポロジを考慮し、ICMP Echo Request(Type 8, Code 0) をベースに用いる。
[Client (Windows)] -- (TTL=1) --> [Router A]
[Client (Windows)] <-- (TTL Expired) -- [Router A] (ICMP Type 11)
[Client (Windows)] -- (TTL=2) --> [Router A] --> [Router B]
[Client (Windows)] <-- (TTL Expired) -- [Router B] (ICMP Type 11)
一見すると「同じパスを辿るのだから結果は一緒だろう」と思われがちだが、ここに現代の厳格なステートフル・ファイアウォールやIDS/IPSが絡むと、話はまったく違ってくる。
ステートフルインスペクションの壁
UDPベースのtracerouteの場合、ランダムな高位ポート(33434番以降)へパケットを撃ち込むため、ファイアウォール側が「これは正当なセッションの開始か?」と首をかしげる。多くのセキュリティアプライアンスは、SYNパケットや既知のハンドシェイクを伴わないUDPの流入を「スキャン行為」とみなし、容赦なくドロップする。
一方、ICMP Echo RequestベースのWindows tracert は、「pingが通るネットワークであれば、tracerouteも通るべきだ」という設計思想に基づいている。多くの企業ネットワークやクラウドのセキュリティグループ(Security Group)では、監視や保守の観点からICMP(Ping)の通過を許可しているケースが多い。そのため、UDP版では途中でブラックホール化するパスであっても、ICMP版であれば最後の宛先まで到達できるという逆転現象が起きるのだ。
—
2. LinuxカーネルとIPスタックの挙動差分
では、Linux環境からWindowsのようなICMPベースのtracerouteを再現したい場合はどうすればいいのか。Linuxの標準 traceroute コマンドには、-I オプションが用意されている。
# LinuxでICMP Echoベースのtracerouteを実行する例
traceroute -I 192.168.100.1
このコマンドを実行したとき、Linuxカーネルのネットワークスタック内部では何が起きているのか。
通常、LinuxでICMP Echo Requestを送信すると、カーネルは内部でICMPソケット(SOCK_RAW)をオープンし、シーケンス番号や識別子(Identifier)を管理する。ここで重要なのが、ソケットの識別子だ。並行して他のプロセスがpingを叩いていたとしても、カーネルは ICMP Identifier フィールドを見て、どのプロセスにどのレスポンスを返すべきかを正確にルーティングしている。
さらに、パケットの往復遅延時間(RTT)をミリ秒単位で正確に計測するため、カーネルのタイマーサブシステムとソケットバッファ(sk_buff)が密に連携する。
パケットキャプチャで見る差異
実際に tcpdump で両者を比較してみよう。
# UDPベース (Unix標準)
IP 192.168.1.10.54321 > 8.8.8.8.33434: UDP, length 28
# ICMPベース (Windows tracert / Linux traceroute -I)
IP 192.168.1.10 > 8.8.8.8: ICMP echo request, id 1, seq 1, length 40
このペイロードの差とプロトコルヘッダーの違いが、中継機器のQoS(Quality of Service)やL4ロードバランサーのハッシュアルゴリズムに大きな影響を与える。多くのL4ロードバランサーは、宛先IPだけでなく、TCP/UDPのポート番号も含めてハッシュ値を計算し、特定のフローを同じサーバー群に割り振る(ECMP: Equal-Cost Multi-Pathのセッション維持)。
ICMPベースの場合、ポート番号の概念がないため、ハッシュの偏りが生じ、UDP版とはまったく異なる物理パスを選択するケースが実運用では多々発生する。障害切り分けの際、「なぜかUDPだと通るパスが、ICMPだと別のルーター経由になってドロップする」という現象に直面したら、このECMPの挙動の差を疑うべきだ。
—
3. 実務で使えるPythonによるカスタムICMP Traceroute実装
市販のツールやOS標準コマンドでは、タイムアウト値の調整や独自のヘッダー付加に限界がある。インフラエンジニアとして、ネットワークの挙動を完全に制御下に見るために、Pythonの生のソケット(Raw Socket)を使ったICMP tracerouteのミニマムな実装を見ておこう。
以下のスクリプトは、ルート権限(root)で実行し、ICMP Echo RequestのTTLを1からインクリメントしながら、途中のルーターからの応答をキャッチする実装だ。
import socket
import struct
import time
import sys
def checksum(source_string):
"""ICMPチェックサムを計算する関数"""
sum = 0
count_to = (len(source_string) // 2) * 2
count = 0
while count < count_to:
val = source_string[count + 1] * 256 + source_string[count]
sum = sum + val
sum = sum & 0xffffffff
count = count + 2
if count_to < len(source_string):
sum = sum + source_string[len(source_string) - 1]
sum = sum & 0xffffffff
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
answer = ~sum
answer = answer & 0xffff
answer = answer >> 8 | (answer << 8 & 0xff00)
return answer
def icmp_traceroute(dest_addr, max_hops=30):
icmp = socket.getprotobyname('icmp')
try:
# 生ソケット(RAW SOCKET)の作成。ICMPパケットを直接組み立てて送信するために必要
send_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
except socket.error as e:
print(f"ソケットの作成に失敗しました。root権限で実行してください: {e}")
sys.exit(1)
# タイムアウトを2秒に設定
recv_socket.settimeout(2.0)
dest_ip = socket.gethostbyname(dest_addr)
print(f"Traceroute to {dest_addr} ({dest_ip}), max hops: {max_hops}")
for ttl in range(1, max_hops + 1):
# ソケットのTTLオプションを設定
send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 0)
send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, struct.pack('I', ttl))
# ICMP Headerの構築 (Type: 8 [Echo Request], Code: 0, Checksum: 0, ID: 12345, Seq: ttl)
# チェックサムは仮に0で計算し、後で埋める
header = struct.pack("bbHHh", 8, 0, 0, 12345, ttl)
data = struct.pack("d", time.time()) # ペイロードにタイムスタンプを埋め込む
my_checksum = checksum(header + data)
# 正しいチェックサムを再構築
header = struct.pack("bbHHh", 8, 0, socket.htons(my_checksum), 12345, ttl)
packet = header + data
start_time = time.time()
try:
send_socket.sendto(packet, (dest_ip, 1))
while True:
# 応答を受信
eval_time = time.time()
_, addr = recv_socket.recvfrom(512)
# 実際のRTTを算出
rtt = (time.time() - start_time) * 1000
current_addr = addr[0]
# 宛先に到達したか、またはTTLが満期になったか
print(f"{ttl:2d}: {current_addr} {rtt:.2f} ms")
break
except socket.timeout:
# タイムアウト時はアスタリスクを表示
print(f"{ttl:2d}: * (Request timed out)")
break
except Exception as e:
print(f"{ttl:2d}: エラーが発生しました: {e}")
break
if current_addr == dest_ip:
print("目的地に到達しました。")
break
send_socket.close()
recv_socket.close()
if __name__ == '__main__':
# テスト対象としてパブリックDNSを指定
icmp_traceroute("8.8.8.8")
このコードを実行すると、OSの抽象化レイヤーを剥ぎ取り、パケットがどのようにカーネルから送り出され、途中のルーターでTTLがデクリメントされていくのかを生々しく体感できるはずだ。実務において、既存のツールが使えない特殊なコンテナ環境や組込み機器の検証において、こうしたカスタムスクリプトが強力な武器になる。
—
4. トラブルシューティングの極意:現場で迷わないためのチェックリスト
大規模データセンターやクラウド環境の境界で障害に直面したとき、ICMPベースとUDPベースの挙動差異を知っているかどうかで、復旧までのリードタイムが何時間も変わる。最後に、現場で私たちが実践している診断の鉄則を共有しよう。
1. 「見えないルーター」に惑わされない
途中のホップがすべて *(アスタリスク)であっても、最終宛先からの応答が返ってくるのであれば、それは単にルーターのセキュリティポリシー(Rate LimitingやICMP破棄)によるものであり、パケット自体は正常にルーティングされている可能性が高い。パニックを起こしてルート変更を急ぐ前に、必ず最終エンドポイントでの疎通を確認すること。
2. クラウド環境のセキュリティグループの仕様を疑う
AWSのセキュリティグループやAzureのNSGでは、インバウンドのICMP(Echo Request)は許可していても、トラフィックのステートフルな戻りやTTL満期に関する挙動がオンプレミスのルーターとは異なる場合がある。クラウド間の接続性検証では、UDP版とICMP版の両方でtracerouteを試し、フィルタリングの差異をあぶり出すのが鉄則だ。
3. MTUとフラグメンテーションの罠
ICMP tracerouteのパケットサイズを変更することで、経路上のPMTUD(Path MTU Discovery)のブラックホールを特定できる。ペイロードサイズを大きくしたICMPパケットを投げ、どのホップで Fragmentation Needed (Type 3, Code 4) が返ってこなくなるかを確認する手法は、大規模なL3VPNやトンネリング技術(VXLANやIPsec)のトラブルシューティングにおいて極めて有効だ。
ネットワークは生き物であり、私たちが送り出す一本のパケットは、数千・数万のデバイスが織りなす巨大な迷宮を潜り抜けて目的地へと向かう。その挙動の裏にあるプロトコルの仕様と、OSごとの実装の差分を深く理解している者だけが、混沌とした障害の海で正確なコンパスを握ることができるのだ。
さあ、次のアラートが鳴る前に、手元のターミナルで traceroute -I を叩いて、あなたのパケットが通る本当の足跡を覗いてみてはどうだろうか。
コメント