ハクソク

世界を動かす技術を、日本語で。

最も洗練されたTCPホールパンチングアルゴリズム

概要

  • TCP hole punchingはNAT越しにPC同士を接続する技術
  • 多くの前提条件と複雑なインフラが必要
  • 本手法はインフラ不要でアルゴリズムの動作確認が可能
  • タイムスタンプから全メタデータを導出し同期
  • ポート選択・ソケット設定・接続判定までを簡潔に解説

TCP hole punchingの前提と課題

  • TCP hole punchingはNATルータ配下の2台のPCを直接接続する技術
  • 成功には以下の条件が必須
    • 互いのWAN IPの事前把握
    • 正しい外部ポート番号の共有
    • 完全な同時接続の実現
  • 実運用では、STUNによるWAN IP取得NATタイプ判定・ポート予測NTPによる時刻同期必要なメタデータ交換チャネルが必要
  • これらは複雑なインフラと実装を要求し、エラーも多発

インフラ不要でのアルゴリズム検証法

  • 単一パラメータから全メタデータを決定的に導出する方式を採用
  • UNIXタイムスタンプを基準パラメータとして選択
  • 両端が通信せずとも合意できる「バケット」値を算出
    • max_clock_error(最大時刻誤差)を考慮
    • min_run_window(実行許容時間幅)を設定
    • 数式例:
      • now = timestamp()
      • max_clock_error = 20s
      • min_run_window = 10s
      • window = (max_clock_error * 2) + 2
      • bucket = int((now - max_clock_error) // window)
  • このバケットを用い時刻ズレにも強い同期を実現

ポートリストの決定方法

  • バケット値をPRNG(擬似乱数生成器)のシードとして利用
  • 乱数で得た値から共通のポートリストを生成
    • local port = external port となることを期待(equal delta mapping)
    • 一部ルータでのみ有効な簡易性重視の仕様
  • 具体的手順
    • large_prime = 2654435761 などの素数でバケットを変換
    • stable_boundary = (bucket * large_prime) % 0xFFFFFFFF で範囲を固定
    • 16個程度のポートを生成、バインド不可なものは除外
    • OSの他プロセスとポート衝突の可能性あり

ソケット設定とネットワーク処理

  • TCP hole punchingには特定のソケットオプションが必須
    • SO_REUSEADDR および SO_REUSEPORT の有効化
  • 通常のTCPとは異なりソケットのクローズ禁止
    • クローズでRSTパケットが送信されプロトコル破綻
    • OS側のTIME_WAIT等の状態遷移も再利用性を損なう
  • ノンブロッキングソケット推奨
    • **非同期(async)**はタイミング制御が難しく不適
    • selectによるポーリングで状態管理
  • SYNパケットを0.01秒間隔で連打し、適度な頻度を維持

接続確立後の勝者選定

  • 複数ポートで複数接続が成立するため同一接続の選定が必要
  • WAN IPが大きい方をリーダー、もう一方をフォロワーに決定
    • リーダーが任意の1接続で1文字送信し他はクローズ
    • フォロワーはselectでイベント検知し1バイト受信した接続を採用
  • 単一文字で判定する理由
    • TCPはストリーム型なので複数文字だとバッファリングが複雑化

全体の流れと実装例

  • 全プロトコルは決定的であり、宛先IPのみで動作可能
  • NTPによる時刻同期は推奨だが必須ではない
  • 別プロセスで実行タイミングを調整すれば、メタデータ交換不要
  • equal delta mappingを採用する一般的な家庭用ルータで動作
  • 実装例: tcp_punch.py 127.0.0.1 でローカル動作確認が可能

この手法により、複雑な外部インフラなしでTCP hole punchingアルゴリズムのテストが容易に実施可能。時刻同期と決定的なパラメータ生成により、最小限の準備でNAT越し接続の動作確認を行える。

Hackerたちの意見

「リスナーはどこ?」って聞いてるなら、リスナーはいらないよ。
RFCでは同時接続が許可されるべきだと言っているかもしれないけど、それがファイアウォールにブロックされないって意味じゃないよ。多くの設定が受信SYN、!ACKパケットをブロックしていて、両方の側がそれをやったら、接続は絶対に確立されない。
> 多くの家庭用ルーターは、外部マッピングでソースポートを保持しようとします。これは「イコールデルタマッピング」と呼ばれる特性で、すべてのルーターで機能するわけではありませんが、私たちのアルゴリズムではシンプルさのためにカバレッジを犠牲にしています。この点が、私が友達とp2pのWireGuard設定を接続する際に困惑した理由です。友達はpfSenseルーターを使っていて、何を試してもpfSenseは常にランダムなソースポートを選びます。でも、このブログが説明しているシンプルなケースでは、両端が同じソースポートを使えば、この方法で2つのファイアウォールを簡単に突破できます。: [1] https://blog.rymcg.tech/blog/linux/wireguard_p2p/
あなたの友達がpfSenseでポートフォワーディングを設定するのは、あなたのシナリオには役立たないの?
私の経験では、Cisco ASAはデフォルトでソースポートの持続性を持ってるよ(できない場合はランダムに戻るけど)。Fortigateはできるけど、バージョンによっていろいろな方法があるね。ただし、マップポートのフォールバック方法は機能しない。Juniper SRXはできないけど、1:1のマッピングを保証すれば別だね。
- お互いのIPアドレスを知っている(またはそれを知らせる方法がある) - 同じメッセージ内でポートを決められない - NATポートのランダム化に悩まされていない これは絶対に起こらないとは言わないけど、これが最小限の複雑さの解決策になるVennダイアグラムは、あまり大きくない気がするんだよね。
多くの人は「自分のIPは何?」をググって友達に送ることはできると思うけど、ポートが何かは必ずしも知らないよね。NATのランダム化については、わからないな。設定によるんじゃないかな。
これは素晴らしいアルゴリズムだね!AIが決定論的なコンピュータの性質を侵食しているこの時代に、決定論的な論理を使った現実の問題に対するエレガントな解決策を読むことができて、本当に感謝してる。
まだ決定論的なコンピュータの時代に生きてるよ。曖昧になっているのはソフトウェアの方だね。(話がそれたけど、AIなんて存在しないよ)
TCPホールパンチングは、一般的なCPEやCG-NATで実際に機能するの?成功したのを見たことがないし、使い道がないからなのか、UDPホールパンチングに比べて成功率や複雑さが悪いからなのか、ずっと疑問に思ってた。そうは言っても、標準化された方法があればいいのに。特定のホスト/ポートペアからの接続が数秒間必要だってことを、すべてのファイアウォールに明示的(または少なくとも暗黙的だけどあいまいでない)に示す何かがあればいいな。基本的には軽量なインバンドポートマッピングプロトコルだね。TCPホールパンチングを促進するための公式な推奨があったかもしれないけど、今となっては手遅れだろうね。ファイアウォールの挙動は何十年もかけて異なる方向に進化してきたから。
> 本当に標準化された方法があればいいのに。特定のホスト/ポートペアからの接続が次の数秒間望ましいことをすべてのファイアウォールに示す明示的(または少なくとも暗黙的だけど曖昧でない)インジケーターが必要だよ。 NATのユニキャストUDPに関する行動要件、https://datatracker.ietf.org/doc/html/rfc4787 NATのTCPに関する行動要件、https://datatracker.ietf.org/doc/html/rfc5382
標準的な方法はIPv6って呼ばれてるよ。それを実装するのは、あのRFCよりも簡単だと思う。
タイムスタンプバケットのアイデアは面白いね。ソースポートを保持するルーター以外でこれが信頼できると思う?私の理解では、TCPパンチングはNATの挙動にかなり依存すると思ってたんだけど。
UDPを使ったWindowsのWintunベースのP2P VPNトンネルを作ったよ。https://github.com/mascarenhasmelson/Windows-P2P-UDP
バケット選択アルゴリズムは機能しないんじゃないかな?二つのホストがバケットの端の反対側にいることもあるからね。例えば、一方のホストがt=61を見て、もう一方がt=62を見た場合、20秒未満の差なのに異なるバケットに入ることになるよ。エラー許容範囲内で隣接するバケットをチェックしないとダメで、バケットのサイズをそれに基づいて広げるのは良くないよ。
主張されているエレガンスは、NATデバイスがアウトバウンド接続のソースポートを保持するという非常に大胆な仮定に基づいているね。典型的な展開ケースの半分でも、そんなことはほとんどないよ。
あなたのコメント、いいね。でも、著者はこれをアルゴリズムの注意点として認めてるみたいだね。 >「多くの家庭用ルーターは、外部マッピングでソースポートを保持しようとします。これは“イコールデルタマッピング”と呼ばれる特性です。すべてのルーターで機能するわけではありませんが、私たちのアルゴリズムでは、シンプルさのためにカバレッジを犠牲にしています。」 じゃあ、具体的にどのくらいカバレッジが犠牲になってるの?全然わからない。もしその割合が高いなら、あなたが言ってるようにあんまり役に立たないよね。
この件について気づいたことが一つあるんだけど、もし欠陥が家庭用のネットワーク機器に存在するなら、その欠陥を利用して企業ネットワーク環境でこの機能を無効にできるってことだね。アルゴリズムのユーザー(または、それを使っているアプリのユーザー)がその制限に気づいていなかったら、めっちゃイライラするだろうな!