深夜3時。データセンターの冷たい空調音だけが響くNOCルームで、私はコーヒーのカップを片手にモニターを睨みつけていた。グローバル展開するWeb APIのレイテンシが突如として跳ね上がり、特定のトランジット区間でパケットがブラックホールに吸い込まれているかのように消えている。
「おい、またあのルートか……。ちょっと traceroute 打ってみてくれ」
隣に座る若手エンジニアが慌ててキーボードを叩くが、画面には * * * の無慈悲なタイムアウトが並ぶだけだ。「先輩、ICMPの ping は通るのに、なぜか traceroute が途中で止まります。ファイアウォールでブロックされてるんでしょうか?」
よくある誤解だ。彼はまだ、パケットの裏側で何が起きているのかの「本質」を掴みきれていない。
ネットワークエンジニアやWeb APIのインフラを支えるバックエンドエンジニアにとって、traceroute は単なる死活監視ツールではない。それはパケットがどのようなルーターの海を渡り、どのポリシールールに阻まれ、どこで息絶えたのかを暴くための「手術用のメス」なのだ。
今回は、UNIX系OS(LinuxやmacOS)で標準的に使われる 「UDPベースのtraceroute」 に焦点を当て、その泥臭くもエレガントな仕組みと、エフェメラルポートの知られざる役割について、実務の現場目線で徹底的に解説しよう。
—
1. なぜ「UDP」なのか? — ICMP型との決定的な違い
世の中には大きく分けて2種類の traceroute が存在する。Windowsの tracert が使う「ICMP Echo Request(タイプ8)ベース」のものと、LinuxやmacOSがデフォルトで使う「UDPベース」のものだ。
インフラ運用の現場で、なぜ私たちがUNIX系のUDP方式を好んで使うのか。それは、「現代のインターネットの現実」 に最適化されているからに他ならない。
多くの商用ルーターやファイアウォールは、セキュリティ上の理由(あるいはCPU保護)から、ICMPパケットの処理を厳しく制限したり、レートリミットをかけたりしている。そのため、ICMPベースの traceroute を実行すると、本来は生きているはずの中継ルーターからの「Time Exceeded(TTL切れ通知)」がドロップされ、全区間が * だらけになることが珍しくない。
一方、UDPベースの traceroute は、「誰も受信していないはずの適当な高位ポート」 めがけてUDPパケットを撃ち込む。これにより、ルーターではなく「宛先のホストそのもの」から意図的なエラーを引き出すという、実に洗練されたアプローチをとるのだ。
—
2. パケットの旅路:TTLとエフェメラルポートの裏側
では、UDP版 traceroute がネットワーク上をどのように駆け巡るのか、その通信フロー(シーケンス)を解剖しよう。
Linuxで traceroute target.com を実行したとき、舞台裏では次のようなドラマが展開されている。
[自ホスト (NOC端末)] [中継ルーター (Hop 1)] [宛先サーバー]
│ │ │
│── (1) UDP / TTL=1 / Port=33434 ────────▶│ │
│ │ │
│◀─ (2) ICMP Time Exceeded (TTL=0) ───────│ │
│ │ │
│── (3) UDP / TTL=2 / Port=33435 ─────────────────────────────────────▶│
│ │ │
│ │ (中継ルーターを通過) │
│ │ │
│◀─ (4) ICMP Port Unreachable (Type 3) ────────────────────────────────┤
│ │
TTL(Time to Live)の巧妙なインクリメント
パケットの寿命を決める TTL フィールドを、最初は 1 に設定して送信する。
1. TTL=1 のパケットは、すぐ隣の最初のルーター(ゲートウェイ)に届いた瞬間に寿命を迎え、ルーターは自ら破棄しつつ、送信元へ ICMP Time Exceeded(TTL exceeded in transit)を返す。これで1ホップ目のIPアドレスが判明する。
2. 次のパケットは TTL=2 にして送信する。今度は1台目のルーターを通過し、2台目のルーターで破棄されてそのIPが返る。
3. これを宛先に届くまで繰り返す。
なぜ宛先ポートは「33434」から始まるのか?
ここが本日のハイライトだ。UNIX系の traceroute は、デフォルトで宛先ポートに 33434 を指定し、パケットを送信するごとにポート番号をインクリメントしていく(33434, 33435, 33436……)。
このポート番号(33434〜33534の範囲)は、IANA(Internet Assigned Numbers Authority)によって公式に traceroute 用として予約されているわけではない。古のエンジニアたちが、「通常のアプリーケーション(HTTPの80や443、SSHの22など)がまず使わない、かつ上位のOSやファイアウォールでブロックされにくいエフェメラル(一時的な)ポートの領域」として、慣習的に選んだものなのだ。
そして、パケットが最終宛先(ターゲットホスト)に到達すると、何が起きるか?
宛先ホスト側では、「そんなポート番号(例: 33434)で待ち受けているプロセスなんてないよ!」と怒り、送信元に対して ICMP Port Unreachable(Type 3, Code 3) を返す。
traceroute はこの Port Unreachable を検知した瞬間に、「あ、宛先まで到達したな」と判断し、プローブ(測定)を終了する仕組みになっている。非常に美しく、理にかなった設計だ。
—
3. 実務で役立つ!パラメータチューニングと実践コマンド
障害切り分けの現場では、デフォルトのパラメータでは正確な測定ができない場面に多々遭遇する。例えば、AWSやGCPなどのクラウド環境や、厳格なセキュリティポリシーを持つ企業のネットワーク境界だ。
ここでは、実務で即座に使える traceroute(Linux環境)のテクニックとパラメータを紹介しよう。
① ICMPモードへの切り替え(ファイアウォール対策)
UDPパケットが完全に遮断されているファイアウォール配下では、ICMP Echoを使った方式に強制切り替えするのが定石だ。
# -I オプションで ICMP Echo (ping形式) を用いたtracerouteを実行する
traceroute -I api.example.com
*実務Tips:* これにより、中継機器のICMPレートリミットに引っかかるリスクはあるものの、通常のWebトラフィックに近い経路調査が可能になることがある。
② プローブパケットのポート番号やプロトコルを変える
APIサーバーの特定のロードバランサー(LB)やAPI Gatewayの手前でパケットがドロップしている疑いがある場合、宛先ポートを 443(HTTPS)などに固定して、LBのルーティングやステートフルなファイアウォールの振る舞いを模倣することがある。
# TCP SYNパケットを使って、ポート443への到達性を経路と同時に確認する(tcptracerouteを使用)
sudo tcptraceroute -p 443 api.example.com
*実務Tips:* 現代のインフラでは、UDPの 33434 あたりがセキュリティアプライアンス(Palo AltoやFortiGateなど)で容赦なくパケットロスさせられることが多い。そのため、本番環境のデバッグでは tcptraceroute を使ったTCPベースの調査が事実上のスタンダードになりつつある。
—
4. コードからネットワークを覗く:PythonでのUDPパケットシミュレーション
「パケットがどう飛んでいるのか、自分の手でロジックを組んで確かめてみたい」
そんな探究心旺盛な若手エンジニアのために、Pythonの低水準ソケット(socket)を使った極めてシンプルなUDP tracerouteの概念コードを用意した。生のソケットを操作することで、OSが裏側で何をしているのかが肌感覚で理解できるはずだ。
import socket
import sys
def simple_udp_traceroute(dest_ip, dest_port=33434, max_hops=30):
print(f"Traceroute to {dest_ip} via UDP (Port: {dest_port}), max hops: {max_hops}")
for ttl in range(1, max_hops + 1):
# 1. 送信用ソケットを作成 (IPv4, UDP)
send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
# 2. ソケットのTTLオプションを設定(ここが肝!)
send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl)
# 3. 受信用ソケットを作成 (ICMPを受信するため RAWソケットを使用 - 要root権限)
# ※実運用ではsocket.SOCK_RAWを扱うためroot権限が必要です
try:
recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
recv_socket.settimeout(2.0) # タイムアウトは2秒
except PermissionError:
print("Error: ICMPを受信するためのRAWソケット作成にはroot権限が必要です(sudoで実行してください)。")
sys.exit(1)
try:
# 4. 宛先へダミーのUDPパケットを送信
send_socket.sendto(b"NOC_DEBUG_PROBE", (dest_ip, dest_port))
# 5. 中継ルーターからのICMPレスポンスを待つ
data, addr = recv_socket.recvfrom(512)
print(f"Hop {ttl:2d}: Router IP = {addr[0]}")
# もし宛先からの Port Unreachable であれば終了
# (実際にはICMPペイロードの中身をパースして判定しますが、ここでは簡略化しています)
if addr[0] == dest_ip:
print("Destination reached!")
break
except socket.timeout:
print(f"Hop {ttl:2d}: * * * (Timeout)")
finally:
send_socket.close()
recv_socket.close()
# ポート番号をインクリメント(UNIX標準の挙動に合わせる)
dest_port += 1
if __name__ == "__main__":
# テストとしてパブリックDNSなどを指定して実行(要sudo)
# python3 udptraceroute.py
target = "8.8.8.8"
simple_udp_traceroute(target)
このコードを実行すると、OSのIPレイヤーが TTL をどのように操作し、中継ルーターがどう反応しているのかが手に取るようにわかるはずだ。APIの通信エラーに直面したとき、ブラウザの画面の向こう側で繰り広げられているパケットたちのドラマを頭の中で描けるかどうかが、シニアとジュニアの分かれ道となる。
—
5. おわりに:障害の嵐を越えて
ネットワークの世界に魔法はない。すべての挙動には必ず物理的、あるいは論理的な理由(RFCの仕様と実装の歴史)がある。
「なぜこのポートなのか」
「なぜこのエラーコードが返ってきたのか」
そうした細部へのこだわりこそが、複雑怪奇なマイクロサービスやクラウドのネットワーク障害に直面したとき、最短で真の原因にたどり着くための羅針盤となる。
さあ、コーヒーを飲み干したら、次のインシデントフォレストへ出かけるとしよう。パケットの旅路に、今日も異常なし、と祈りながら。
コメント