6月10日、p2clawが「The iPad was on Tailscale」と題した記事を公開した。
症状:iPadだけで発生する謎の障害
p2clawアプリケーションがiPadで白紙画面になる問題が発生した。同じWi-Fi、同じブラウザエンジンを使っているにも関わらず、Mac、Linuxマシン、スマートフォンでは正常に動作していた。さらに困ったことに、何度もリフレッシュするとたまに読み込めるという不安定な再現性を示していた。
アプリは初期のHTMLを読み込んで読み込み状態を表示するところまでは動作するが、その後にハングアップしてしまう。ブラウザは**WebRTCのデータチャネル経由で最初のGET /リクエスト**を送信した後、永遠にレスポンスを待ち続けるという状況だった。
WebRTCは、ブラウザ間でリアルタイムコミュニケーションを可能にする技術で、音声・映像通話だけでなく、P2Pデータ転送にも利用される。近年、従来のHTTPサーバーを介さない直接通信によるWebアプリケーションの開発が注目されており、このような障害の原因特定は重要な課題となっている。
計測による原因特定
まず両端の接続をログに記録し、時計時刻で同期させて分析を行った。その結果、ボックスエージェントは3つのチャンクを送信していることが判明した:
- 220バイトのヘッダー
- 7,874バイトのボディ
- 199バイトのテール
iPadでは220バイトのヘッダーのみが受信され、その後の大きなボディチャンクが一切到達していなかった。WebRTCデータチャネルは順序保証配信(SCTP over DTLS)を行うため、1つのチャンクが欠損すると後続のメッセージもブロックされる仕組みになっている。
真犯人の発見:Tailscaleと分断処理
2時間のWebKit原因説を経て、著者は重要な事実に気づいた。MacとiPadの違いは、iPadでTailscaleが有効になっていることだった。
Tailscaleは、WireGuardベースのメッシュVPNサービスで、デバイス間のゼロ設定ネットワーキングを提供する。近年、リモートワークの普及に伴い、個人開発者や企業で急速に採用が進んでいる技術である。
TailscaleはVPNであり、トラフィックを追加レイヤーでラップするため、各パケットで利用可能な容量が減少する。メッセージが800バイト以下になるよう制限すると、Tailscaleの有無に関係なくiPadが即座に読み込まれた。
しかし、これは問題の一部に過ぎなかった。
根本原因:webrtc-rsの固定定数
問題の核心は、RustのWebRTCスタックwebrtc-rsのハードコードされた定数にあった:
// sctp/src/association/mod.rs
pub(crate) const INITIAL_MTU: u32 = 1228;
MTU(Maximum Transmission Unit)は、1つのパケットで送信可能な最大データサイズを指す。この1,228バイトのパケットは暗号化レイヤーを加えると1,265バイトとなり、UDPとIPヘッダーを含めるとIPv6では1,313バイトのパケットになる。しかし、Tailscaleのトンネルは最大1,280バイトまでしか処理できない。
TailscaleのIPv6フラグメント処理バグ
さらに深刻な問題が発見された。以下のpingテストで明確になった:
ping -s 100 <iPad over IPv4> 3/3 received
ping -s 1400 <iPad over IPv4> 3/3 received # フラグメント正常
ping -s 100 <iPad over IPv6> 3/3 received
ping -s 1400 <iPad over IPv6> 0/3, 100% loss # フラグメント消失
IPv4フラグメントは再組み立てされるが、IPv6フラグメントは完全に消失していた。
Tailscaleの診断カウンターを確認すると、以下のような結果が得られた:
tailscaled_inbound_dropped_packets_total{reason="acl"} 6
3回のping、各2フラグメント、計6回のドロップ。Tailscaleのソースコードを調査した結果、IPv6パーサーがフラグメントを解析しない設計になっていることが判明した。IPv6フラグメントヘッダーを持つパケットは「未知のプロトコル」として分類され、許可ルールにマッチしないため、デフォルトの拒否ポリシーによってドロップされていた。
コメントには以下のように記述されていた:
Note that this means we don't support fragmentation in IPv6. This is fine, because IPv6 strongly mandates that you should not fragment.
これはRFC 8200の誤解である。IPv6はルーターによる途中でのフラグメント化を禁止しているが、送信者によるフラグメント化は完全に許可されており、受信側は断片を再組み立てする必要がある。
すべての謎が解決
- なぜiPadだけか? MacがTailscale未使用だったため
- なぜLinuxラップトップでは発生しないのか? WebRTCハンドシェイクでIPv4ペアが選択されていたため
- なぜリフレッシュで時々成功するのか? 再接続時にIPv6以外のルートが選ばれることがあるため
- なぜ1,200バイトでも失敗するのか? webrtc-rsが最初のパケットを1,228バイトまでパディングするため
教訓とネットワーク設計への示唆
この問題は、パケットが大きすぎる場合に静かに消失し、誰にも理由が伝えられないというインターネットの古典的な問題の現代版である。WebRTC、ゲーム、P2P通信など独自のパケット送信を行うソフトウェアを構築する際は、ユーザーの相当数が期待より小さなパスを使用していると想定し、パケットサイズを保守的に抑えるか、送信前にPath MTU Discoveryを実装すべきである。
特に、VPN利用が一般化した現在のネットワーク環境では、従来の1,500バイトEthernet MTUを前提とした設計では問題が発生する可能性が高い。WebRTCアプリケーション開発者は、RFC 3550で推奨される1,200バイト以下のパケットサイズを意識する必要がある。
詳細はThe iPad was on Tailscaleを参照していただきたい。