【実務・中級編】 ICMPベースのtraceroute(Windows tracert)の動作と違い – トラブルシューティング&ネットワーク運用監視実践ガイド

Windowsの tracert はなぜ異端なのか?ICMPベースのルート追跡が隠し持つ「パケットの挙動」と実務の罠

NOC(ネットワークオペレーションセンター)の夜勤明け、冷めかけたコーヒーを片手にモニタを見つめる。そんなシチュエーションでエンジニアが最も頼るツールの一つが、ネットワークの経路を暴く traceroute だ。

「ルータの向こうでパケットがどこで迷子になっているのか?」
「意図したキャリアを通ってトラフィックが流れているのか?」

インフラエンジニアであれ、Web APIのレイテンシやタイムアウトに頭を悩ませるアプリケーションエンジニアであれ、ルーティングの深淵を覗くためにこのコマンドの挙動を理解しておくことは必須教養である。

だが、ここで一つ、長年ネットワークの世界に身を置く人間がニヤリとさせられる「仕様の罠」がある。
それは、Windows標準の tracert コマンドが、UNIX/Linux系OSの traceroute とは全く異なるパケットを叩き込んでいるという事実だ。

今回は、このWindows版 tracert が採用するICMPベースの動作原理に深く切り込み、実務の現場でどうこの挙動を読み解くべきか、泥臭い実例を交えて解説しよう。

—

1. UNIX/LinuxとWindows、それぞれの traceroute が描く「異なるルート」

まず、ネットワークの基本に立ち返ろう。
標準的な(RFC 792 / RFC 1812に基づく)UNIXやLinuxの traceroute は、デフォルトで UDPパケット を使用する。

これに対し、Windowsの tracert は ICMP Echo Request(タイプ8) を直接ぶっ放す。この違いが、パケットの往来において何を意味するのか。まずはその通信の裏側を覗いてみよう。

標準的なUDPベース(Linux traceroute)の挙動

1. 送信元から、宛先ポート番号が「通常使われない巨大な値(例: 33434番以降)」に設定されたUDPパケットが飛ぶ。
2. この時、IPヘッダーの TTL(Time To Live) フィールドを最初は 1 に設定する。
3. 最初のルータ(ホップ1)に到達すると、ルータはTTLをデクリメントし、0 になったためパケットを破棄する。同時に、送信元へ ICMP Time Exceeded(タイプ11、コード0) を送り返す。これで1つ目のルータのIPが判明する。
4. 次にTTLを 2 に増やして同じことを繰り返し、宛先に到達するまでこれを続ける。
5. 最終的に宛先ホストにUDPパケットが届くと、そのポートで待ち受けているプロセスがないため、宛先ホストは送信元へ ICMP Destination Unreachable(Port Unreachable: タイプ3、コード3) を返す。このパケットを受け取ったことで、ツール側は「宛先に到達した」と判断し、プローブを終了する。

Windows特有のICMPベース(tracert)の挙動

これに対し、Windowsの tracert は最初から最後まで ICMP Echo Request(pingでお馴染みのタイプ8) を使い倒す。

1. 送信元から ICMP Echo Request が、TTL = 1 で発射される。
2. 途中のルータでTTLが 0 になり、ルータから ICMP Time Exceeded が返ってくる(ここはLinuxと同じ)。
3. TTLをインクリメントしながら経路上を進み、最終的に 宛先ホスト に到達する。
4. 宛先ホストは、それが自分宛てのICMP Echo Requestであるため、UDPのようにポート非到達エラーを返す必要がなく、正常に ICMP Echo Reply(タイプ0) を返す。

この「最後が Echo Reply になる」という点が、実はファイアウォールやセキュリティアプライアンスが絡む実務において、非常に大きな挙動の違いを生むことになる。

—

2. パケットの往来をシークエンスで紐解く

百聞は一見にしかず。Windowsの tracert がターゲットホスト(例: 8.8.8.4)に向かってパケットを投げる際の、頭の中のイメージ(シーケンス)を整理しておこう。

[Windows Client]                                 [Router (Hop 1)]               [Destination (8.8.8.4)]
      |                                                 |                               |
      |--- (1) ICMP Echo Request [TTL=1] -------------->|                               |
      |                                                 | (TTLが0になったため破棄)      |
      |<-- (2) ICMP Time Exceeded [TTL Expired] --------|                               |
      |                                                                                 |
      |--- (3) ICMP Echo Request [TTL=2] -------------->|--- (4) Forward [TTL=1] ------>|
      |                                                 |                               | (TTLが0になったため破棄)
      |<-- (5) ICMP Time Exceeded ----------------------|<-- (6) ICMP Time Exceeded ----| (※ルータによる)
      |                                                                                 |
      |--- (7) ICMP Echo Request [TTL=N] ---------------------------------------------->|
      |<-- (8) ICMP Echo Reply (正常応答) ----------------------------------------------| (完了!)

ここでインフラエンジニアとして注意しなければならないのは、「途中のルータが返す ICMP Time Exceeded」と「宛先が返す ICMPパケット」は、ネットワーク機器のコントロールプレーン(CPU処理)で生成されるという点だ。
そのため、ルータの負荷が高い場合や、ICMPのレートリミット(Rate Limiting)が厳しくかけられている場合、途中のホップが * (タイムアウト)だらけになる現象が発生する。障害切り分けの際、「ルータが落ちているのか、単にICMPをシブっているだけなのか」を見極める眼力が求められる所以である。

—

3. 実務で役立つ!ネットワーク診断スクリプト(Pythonによる挙動再現)

「Windowsの挙動が特殊なら、自分でパケットを組み立てて挙動を確認してみたい」
そんなアグレッシブなエンジニアのために、rawソケットを使ってICMPベースのtracerouteを自作するPythonスクリプトの断片を共有しよう。

実務において、社内ネットワークの監査ツールや、特定のクラウド環境へのパス検証用エージェントを組む際の良いベースになるはずだ。

import socket
import struct
import time
import sys

def icmp_traceroute(destination_host, max_hops=30):
    # 宛先IPアドレスの解決
    try:
        dest_addr = socket.gethostbyname(destination_host)
    except socket.gaierror as e:
        print(f"ホスト名の解決に失敗しました: {e}")
        sys.exit(1)

    print(f"Tracing route to {destination_host} [{dest_addr}] with maximum {max_hops} hops:")

    # ICMPチェックサム計算用関数
    def calculate_checksum(source_string):
        countTo = (len(source_string) // 2) * 2
        sum = 0
        count = 0
        
        while count < countTo:
            val = source_string[count + 1] * 256 + source_string[count]
            sum = sum + val
            count = count + 2

        if len(source_string) < len(string):
            sum = sum + source_string[len(string) - 1]

        sum = (sum >> 16) + (sum & 0xffff)
        sum = sum + (sum >> 16)
        answer = ~sum & 0xffff
        answer = answer >> 8 | (answer << 8 & 0xff00)
        return answer

    for ttl in range(1, max_hops + 1):
        # Rawソケットの作成 (ICMPプロトコルを指定)
        # ※注意: 実行には管理者権限(root / Administrator)が必要です
        try:
            icmp = socket.getprotobyname("icmp")
            recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
            send_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
        except PermissionError:
            print("エラー: RAWソケットを作成するには管理者権限が必要です。")
            sys.exit(1)

        # ソケットにTTLを設定 (これがtracerouteのキモ)
        send_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, struct.pack('I', ttl))
        recv_socket.settimeout(2.0)

        # ICMP Echo Requestパケットの組み立て (Type=8, Code=0, Checksum=0, ID=1234, Seq=ttl)
        # Windowsのtracertに近づけるため、ペイロードに適当なデータを詰める
        packet_id = 12345
        checksum = 0
        header = struct.pack("bbHHh", 8, 0, checksum, packet_id, ttl)
        data = b"WindowsTracertEmulationPayload"
        
        # チェックサムの再計算
        my_checksum = socket.htons(~(sum(bytearray(header + data)) & 0xffff) & 0xffff)
        header = struct.pack("bbHHh", 8, 0, my_checksum, packet_id, ttl)
        packet = header + data

        curr_addr = None
        t = time.time()
        
        try:
            # パケット送信
            send_socket.sendto(packet, (dest_addr, 1))
            
            # 応答受信ループ
            while True:
                start_select = time.time()
                _, alt_addr = recv_socket.recvfrom(512)
                curr_addr = alt_addr[0]
                
                # 受信データの解析 (ICMPヘッダー等の処理は省略・簡易化)
                elapsed = (time.time() - t) * 1000
                break
        except socket.timeout:
            curr_addr = None
            elapsed = None

        # 結果の出力
        if curr_addr:
            print(f"{ttl:2d}    {elapsed:7.2f} ms    {curr_addr}")
            if curr_addr == dest_addr:
                print("Trace complete.")
                break
        else:
            print(f"{ttl:2d}     * * *     Request timed out.")

        send_socket.close()
        recv_socket.close()

if __name__ == "__main__":
    # テストとしてGoogleパブリックDNSを指定
    icmp_traceroute("8.8.8.4")

このコードを眺めると、OSが裏側でどれほど泥臭くIPヘッダーの TTL をいじり、ソケットを制御しているかがよく分かるはずだ。

—

4. 現場の教訓:Windows tracert 特有の「ハマり所」

最後に、この仕様の違いが実務の現場でどのようなトラブルを生むか、実際の体験談ベースで注意喚起しておこう。

トラブル事例:Linuxからだと通るのに、Windowsからだと途中で途切れる?

あるWeb APIサーバー群の前段に置かれたロードバランサーやステートフルファイアウォール(NGFW)の背後でトラブルが起きたときのことだ。
Linuxの traceroute(UDP)ではきれいにルートが見えるのに、障害報告をしてきたクライアント(Windows環境)の tracert だと、途中からすべて * * * になってしまう現象に遭遇した。

原因の考察:
セキュリティポリシーの厳格な企業ネットワークやクラウドのセキュリティグループにおいて、「外から入ってくる、あるいは通過するICMP Echo(ping)を厳しくドロップ・レートリミットしている」ケースは非常に多い。
LinuxのUDP tracerouteであれば、パケット自体は「高番号UDPポート」宛てであるため、ファイアウォールがUDPとして許可していれば通過し、最終的に宛先でPort Unreachableが返ることで経路が可視化される。しかし、Windowsの tracert は全行程でICMP Echoを使うため、途中のセキュアな機器がICMP Echoを嫌って弾いてしまうと、途中でルート追跡がブロックされてしまうのだ。

「Windowsの tracert が途中で星(*)になるからといって、必ずしもルーティング障害やルータの故障とは限らない。セキュリティ機器のICMPポリシーに阻まれているだけの可能性が高い」
これは、ネットワークエンジニアが夜間の障害対応で迷子にならないための、非常に大切な羅針盤である。

—

まとめ

パケットの挙動を深く理解することは、単なる試験勉強ではない。
画面に表示される味気ないテキストの裏側で、OSのカーネルがどのようなソケットを叩き、ルータがどのようにTTLを処理しているのかを頭の中でビジュアライズできるようになれば、どんな難解なネットワークトラブルに直面しても、恐れることはなくなる。

Windowsの tracert という、一見すると標準から外れた「異端児」の仕様の裏側には、OSの歴史とセキュリティのせめぎ合いがしっかりと刻み込まれているのだ。

次の現場でルートがおかしいと感じたら、まずは「今、自分が叩いているのはUDPなのか、それともICMPなのか」を思い出してほしい。パケットの声が、必ずや真実のルートを教えてくれるはずだ。

コメント

タイトルとURLをコピーしました