iOS Traceroute 実装技術サマリー

iOS Traceroute 実装技術サマリー

本書は、ルート権限(Root Privileges)を持たないiOS環境において、標準規格に準拠した堅牢なTracerouteを実現するために実装された技術的な課題と解決策をまとめたものです。

課題 (Challenge)

iOSの厳格なサンドボックス制限下でのTraceroute実装には、以下の困難が伴います。

  • Raw Socket不可SOCK_RAW は使用禁止されており、特権不要の SOCK_DGRAM ICMPソケットを使用する必要があります。
  • カーネルの干渉: iOSカーネルがICMPのIdentifier(ID)やChecksumを自動的に処理・書き換えてしまうため、アプリケーション側で設定した値と整合性が取れなくなることがあります。
  • 厳格なファイアウォール: 最近のルーターやNATは、戻りの通信を正しくルーティングするために、送信元ポートやIDの一貫性(フロー)を要求します。
  • 可変長ヘッダー: 戻ってくるエラーパケット(Time Exceeded)には元のIPヘッダーが含まれますが、IPオプションやトンネリングによって長さが変動するため、固定オフセットでの解析が失敗します。
  • 解決策 (Solutions)

    以下の4層のアーキテクチャを実装することで、これらの課題を解決しました。

    1. Persistent Socket(ソケットの永続化によるフロー安定化)

    問題: プローブ(TTL試行)ごとに新しいソケットを作成すると、送信元ポート(Identifier)が毎回変わってしまいます。これはファイアウォールやNATから見ると「ポートスキャン」や「無関係な通信」に見え、パケットが破棄される原因となります。 解決策:

  • Tracerouteの1セッションにつき、単一のソケットを作成します。
  • すべてのTTL(1〜30+)でこの fd を使い回します。
  • 結果: ネットワーク機器からは「一貫した1つの通信フロー」として認識されます。
  • 2. Explicit Binding & ID Synchronization(チェックサム整合性の確保)

    問題: アプリ側でランダムなIDを設定しても、sendto 時にカーネルが自身の割り当てたポート番号でIDを上書きします。この際、チェックサムの再計算が正しく行われない(あるいはアプリ側が古いIDで計算してしまう)ケースがあり、不正なチェックサムを持つパケットとしてルーターに破棄されます。 解決策:

  • bind(fd, ..., port: 0) を呼び出し、カーネルにポート割り当てを明示的に要求します。
  • getsockname(fd) を呼び出し、割り当てられたポート(Identifier)を取得します。
  • このカーネル割り当てIDを使用してICMPパケットを構築し、チェックサムを計算します。
  • 結果: ネットワーク上に流れるパケットのIDとチェックサムが完全に整合します。
  • 3. Sequence Scanning(ロバストなパケット解析)

    問題: ルーターから返される Time Exceeded には、我々が送ったパケットのコピーが含まれています。しかし、ルーターがIPヘッダー長(IHL)を変えたりIPオプションを追加したりすると、我々のシーケンス番号が格納されている位置(オフセット)がずれます。固定位置(例: 48バイト目)のチェックでは失敗します。 解決策:

  • 固定バイト位置を確認するのではなく、受信バッファ全体をスキャンし、2バイトのシーケンス番号(ビッグエンディアン)を検索します。
  • 結果: IPオプションの有無、ヘッダー長の変動、OSの挙動に関わらず、自分のパケットを確実に特定できます。
  • 4. Relaxed Validation & Compatibility(互換性の向上)

  • パケットサイズ: macOSの traceroute コマンドに合わせて 48バイト に増量しました。小さすぎるパケット(例: 32バイト)は、一部のバックボーンルーターで「ラントパケット(ゴミ)」として破棄されるためです。
  • 検証の緩和Time Exceeded 受信時は、シーケンス番号のみを検証します。Identifier(ID)は無視します。一部の厳格なNATが、戻りの経路でIDを誤って書き換えるケースがあるためです。
  • 最終アルゴリズム

  • SOCK_DGRAM UDP/ICMP ソケットを開く。
  • bind(0) & getsockname() -> SessionID を取得。
  • TTL 1 から Max までループ:
    • IP_TTL を設定。
    • ICMP Echo Request を構築 (ID = SessionID, Seq = N)。
    • sendto() 送信。
    • recvfrom() 受信待機。
    • ペイロード内を スキャン して Seq = N を検索。
    • 発見した場合: Hopとして記録。
  • この実装により、標準的なmacOSの traceroute コマンドと区別がつかないレベルの結果が得られるようになりました。

    ← Go home