Monday, November 3, 2025

TCP 3ウェイハンドシェイク 通信の信頼を築く仕組み

私たちが日常的に利用するインターネットの世界では、ブラウザでウェブサイトを開いたり、APIを通じてデータを送受信したりする際に、その裏側で膨大な数の通信が行われています。これらの通信がまるで魔法のように正確かつ確実に目的地に届くのはなぜでしょうか。その答えの核心には、TCP/IPプロトコルスイート、特にTCP(Transmission Control Protocol)が採用している「3ウェイハンドシェイク」という巧妙な仕組みが存在します。多くの開発者がその名前や「SYN, SYN-ACK, ACK」という手順を知識として知ってはいますが、なぜ3回なのか、このやり取りが具体的に何を確立し、アプリケーションの性能やセキュリティにどのような影響を与えるのかという「真実」までを深く理解しているケースは多くありません。本稿では、単なる手順の解説に留まらず、3ウェイハンドシェイクが現代のネットワーク通信の信頼性をいかにして築き上げているのか、その根源的な設計思想からパフォーマンスへの影響、そしてセキュリティ上の意味合いまでを、開発者の視点から深く掘り下げていきます。

第1章 基本の確認:3ウェイハンドシェイクの三段階

まずはじめに、最も基本的なプロセスを再確認しましょう。TCPにおける接続確立のプロセスは、クライアント(接続を開始する側)とサーバー(接続を待ち受ける側)の間で交わされる3つのパケットによって構成されます。この一連のやり取りが「3ウェイハンドシェイク」と呼ばれます。

ステップ1: SYN (Synchronize)

クライアントはサーバーに対して接続を要求するため、最初のパケットを送信します。このパケットには「SYN」フラグが立てられています。これは「Synchronize Sequence Numbers(シーケンス番号を同期したい)」という意思表示です。このとき、クライアントは自身がこれから送信するデータのシーケンス番号の初期値(Initial Sequence Number, ISN)を生成し、パケットに含めてサーバーに通知します。これは、会話を始めるにあたり「私の最初のページ番号はXです」と相手に伝えるようなものです。

クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |

この`seq=x`の `x` は、クライアントがランダムに選んだ32ビットの数値です。このランダム性が、後のセキュリティの議論で重要な役割を果たします。

ステップ2: SYN-ACK (Synchronize-Acknowledge)

SYNパケットを受け取ったサーバーは、クライアントからの接続要求を承諾する意思がある場合、応答パケットを返します。このパケットには「SYN」と「ACK」の両方のフラグが立てられています。

  • SYNフラグ: サーバー自身も、これからクライアントに送信するデータのシーケンス番号の初期値(ISN)を通知する必要があります。これも「私の最初のページ番号はYです」と伝える行為に相当します。
  • ACKフラグ: これはクライアントから受け取ったSYNパケットに対する確認応答です。具体的には、クライアントから送られてきたシーケンス番号 `x` に対して、「あなたのページ番号 `x` を受け取りました。次に私が期待しているのは `x+1` です」という意味で、確認応答番号(Acknowledgement Number)に `x+1` を設定して返します。
クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |
     |   SYN/ACK (seq=y, ack=x+1)               |
     |<-----------------------------------------|
     |                                          |

この時点で、サーバーはクライアントへの接続準備を整え、半開きの接続状態(SYN_RECEIVED状態)になります。

ステップ3: ACK (Acknowledge)

サーバーからSYN-ACKパケットを受け取ったクライアントは、最後の確認応答パケットをサーバーに送信します。このパケットには「ACK」フラグが立てられています。このパケットは、サーバーから送られてきたシーケンス番号 `y` に対して、「あなたのページ番号 `y` を受け取りました。次に私が期待しているのは `y+1` です」と伝えるために、確認応答番号に `y+1` を設定します。

クライアント                                     サーバー
     |                                          |
     |   SYN (seq=x)                            |
     |----------------------------------------->|
     |                                          |
     |   SYN/ACK (seq=y, ack=x+1)               |
     |<-----------------------------------------|
     |                                          |
     |   ACK (seq=x+1, ack=y+1)                 |
     |----------------------------------------->|
     |                                          |
   [接続確立 (ESTABLISHED)]               [接続確立 (ESTABLISHED)]

この最後のACKパケットをサーバーが受信した時点で、双方向の通信経路が完全に確立され、両者はデータを送受信できる状態(ESTABLISHED状態)になります。これでハンドシェイクは完了です。

第2章 なぜ「3回」なのか?2回ではダメな理由

このプロセスを見て、多くの人が抱く素朴な疑問は「なぜ3回も必要なのか?2回のやり取りでは不十分なのか?」というものです。例えば、クライアントがSYNを送り、サーバーがACKを返す2ウェイハンドシェイクではなぜダメなのでしょうか。この問いに答えることは、TCPが解決しようとしたネットワークの根本的な問題を理解することに繋がります。

ネットワークの不確実性という大前提

IPネットワークは、本質的に「ベストエフォート型」です。つまり、パケットが宛先に届くことを保証しません。パケットは途中で消失したり、複製されたり、到着順序が入れ替わったりする可能性があります。TCPの設計目標は、この信頼性のない基盤の上で、信頼性のある双方向のストリーム通信を実現することでした。

2ウェイハンドシェイクの致命的な欠陥

仮に2ウェイハンドシェイク(クライアントがSYNを送り、サーバーがACKを返すだけ)を想像してみましょう。ここに潜む致命的な問題は、「古い接続要求の亡霊」です。

次のようなシナリオを考えてみてください。

  1. クライアントがサーバーに接続要求(SYN_A)を送ります。
  2. しかし、このSYN_Aはネットワークの遅延により、サーバーにすぐには届きません。
  3. 待ちきれなくなったクライアントは諦めて、もう一度新しい接続要求(SYN_B)を送ります。
  4. SYN_Bは正常にサーバーに届き、サーバーはACKを返し、通信が開始され、やがて正常に終了します。
  5. この通信が終わった後、ネットワークのどこかで迷子になっていた最初のSYN_Aが、遅れてサーバーに届いてしまいます。

もし2ウェイハンドシェイクであれば、サーバーはこの遅れてきたSYN_Aを新しい接続要求だと誤解し、ACKを返して接続を確立してしまいます。しかし、クライアント側はとっくの昔にSYN_Aについては忘れており、新しい接続を確立するつもりはありません。結果として、サーバー側だけが一方的に接続を確立し、リソース(メモリ、ポートなど)を消費し続ける「半開き(Half-Open)」の接続が生まれてしまいます。これは深刻なリソースリークに繋がります。

3ウェイハンドシェイクによる解決策:シーケンス番号の相互確認

3ウェイハンドシェイクは、この問題をシーケンス番号の相互確認によって見事に解決します。重要なのは、接続を確立するために双方が相手のシーケンス番号の初期値を知り、かつ、相手が自分のシーケンス番号の初期値を知ったことを確認するプロセスを踏む点です。

  • ステップ1 (SYN): クライアントは「私のISNは `x` です」と宣言します。
  • ステップ2 (SYN-ACK): サーバーは「あなたのISN `x` を了解しました(ack=x+1)。そして、私のISNは `y` です」と返答します。この時点で、クライアントはサーバーが自分の要求を正しく受信したことを確認できます。また、サーバーのISNも知ることができます。
  • ステップ3 (ACK): クライアントは「あなたのISN `y` を了解しました(ack=y+1)」と返答します。このパケットがサーバーに届くことで、サーバーはクライアントが自分のSYN-ACKを正しく受信したことを確認できます。

この3ステップ目があるおかげで、サーバーはクライアントが「生きている」こと、そして双方向の通信路が確立可能であることを確信できます。先の「古い接続要求の亡霊」シナリオでは、遅れて届いたSYN_Aに対してサーバーがSYN-ACKを返しても、クライアントは既に応答を期待していないため、最後のACKを返すことはありません。サーバーは一定時間待ってもACKが来なければ、この接続要求が不正であると判断し、確保したリソースを解放します。これにより、半開きの接続が放置される事態を防ぐことができるのです。

つまり、3ウェイハンドシェイクは、単に接続を開始する合図ではなく、信頼性のないネットワーク上で、双方がこれから始まる通信の初期状態(シーケンス番号)を完全に同期させるための、必要最小限かつ完璧な手順なのです。

第3章 カーネルの視点:TCPの状態遷移

3ウェイハンドシェイクのプロセスは、オペレーティングシステムのカーネル内で管理されているTCP接続の状態遷移と密接に関連しています。アプリケーション開発者が直接この状態を意識することは少ないかもしれませんが、ネットワークプログラミングやトラブルシューティングを行う上では非常に重要な知識です。

以下に、接続確立時における主要な状態を示します。

      +------------------+
      |      CLOSED      |  <-- 初期状態
      +------------------+
              |
 (app: connect())
              |
      V       |
+---------------------+
|      SYN_SENT     |  <-- クライアント: SYN送信後
+---------------------+
              |               /
 (recv: SYN/ACK)       / (recv: SYN)
 (send: ACK)          / (send: SYN/ACK)
              |      /
      V       V     /
+---------------------+
|   ESTABLISHED     |  <-- 接続確立
+---------------------+


      +------------------+
      |      LISTEN      |  <-- サーバー: 待ち受け状態
      +------------------+
              |
 (recv: SYN)
 (send: SYN/ACK)
              |
      V       |
+---------------------+
|    SYN_RECEIVED   |  <-- サーバー: SYN受信後
+---------------------+
              |
 (recv: ACK)
              |
      V       |
+---------------------+
|   ESTABLISHED     |  <-- 接続確立
+---------------------+

クライアント側の状態遷移

  1. CLOSED: 接続が全く存在しない初期状態です。
  2. SYN_SENT: アプリケーションが `connect()` システムコールなどを呼び出し、カーネルがSYNパケットを送信した後の状態です。この状態でサーバーからのSYN-ACKを待ちます。
  3. ESTABLISHED: サーバーからSYN-ACKを受け取り、最後のACKを送信した後の状態です。この状態になって初めて、アプリケーションは `send()` や `write()` を通じてデータを送信できます。

サーバー側の状態遷移

  1. CLOSED: 初期状態です。
  2. LISTEN: サーバーアプリケーションが `listen()` システムコールを呼び出し、特定のポートでクライアントからの接続を待ち受けている状態です。
  3. SYN_RECEIVED: LISTEN状態のポートでSYNパケットを受け取り、SYN-ACKをクライアントに返信した後の状態です。この状態でクライアントからの最後のACKを待ちます。この状態の接続は、しばしば「ハーフオープン接続」とも呼ばれ、後述するSYNフラッド攻撃の標的となります。
  4. ESTABLISHED: クライアントから最後のACKを受け取った状態です。サーバーは `accept()` システムコールを通じてこの確立された接続をアプリケーションに渡し、データの送受信が開始されます。

これらの状態遷移を理解することで、`netstat` や `ss` といったコマンドの出力が何を意味しているのかを正確に把握し、ネットワークの問題(例えば、`SYN_SENT` のまま接続がタイムアウトする、`SYN_RECEIVED` が大量に滞留しているなど)を診断する手助けとなります。

第4章 ハンドシェイクは交渉の場:TCPオプションの役割

3ウェイハンドシェイクのパケットは、単にSYNやACKのフラグを立ててシーケンス番号を交換するだけではありません。TCPヘッダの「オプション」フィールドを使って、これから始まる通信の様々なパラメータを「交渉」する重要な役割も担っています。これにより、通信経路の特性に合わせてTCPの動作を最適化することができます。

MSS (Maximum Segment Size)

MSSは、1つのTCPセグメントで送信できるペイロード(ユーザーデータ)の最大サイズをバイト単位で指定します。ハンドシェイク中に、クライアントとサーバーはそれぞれ自身のMSSを相手に通知します。最終的に採用されるMSSは、両者が提示した値のうち小さい方になります。

なぜMSSを交渉するのでしょうか?それは、IPレベルでの「フラグメンテーション(断片化)」を避けるためです。データがルーターを通過する際、そのルーターが扱うことができるパケットの最大サイズ(MTU, Maximum Transmission Unit)よりも大きいパケットは、複数の小さなパケットに分割されます。このフラグメンテーションは、ネットワークのオーバーヘッドを増加させ、パケットロスの際の影響を大きくするため、可能な限り避けるべきです。MSSを適切に設定することで(通常は `MSS = MTU - (IPヘッダ長 + TCPヘッダ長)`)、TCPセグメントがIPパケットにカプセル化された際にMTUを超えないようにし、フラグメンテーションを防ぎます。

例えば、一般的なイーサネットのMTUは1500バイトです。この場合、IPヘッダ(20バイト)とTCPヘッダ(20バイト)を引いた1460バイトがMSSの典型的な値となります。

ウィンドウ・スケール・オプション (Window Scale Option)

TCPには、受信側が一度に受信できるデータ量を送信側に伝える「ウィンドウサイズ」という仕組みがあります。これにより、受信側のバッファが溢れるのを防ぐフロー制御を実現しています。しかし、オリジナルのTCPヘッダでは、このウィンドウサイズを表現するフィールドは16ビットしかありませんでした。これは最大で 65,535 バイト (64KB) しか表現できないことを意味します。

現代の高速なネットワーク(ギガビットイーサネットなど)や、遅延が大きいネットワーク(衛星通信など)では、64KBのウィンドウサイズはあまりにも小さすぎます。通信路上に常にデータを流し続けるためには、より大きなウィンドウサイズが必要です。この積はBDP(Bandwidth-Delay Product)と呼ばれ、最適なスループットを出すために必要なバッファサイズを示します。

そこで導入されたのが「ウィンドウ・スケール・オプション」です。ハンドシェイク中にこのオプションを交換することで、双方は16ビットのウィンドウサイズフィールドの値を、指定されたスケールファクタ(2のべき乗)でスケールアップすることに合意します。例えば、スケールファクタが7(2の7乗 = 128)であれば、ヘッダ上のウィンドウサイズが1000でも、実際のウィンドウサイズは 1000 * 128 = 128,000 バイトとして扱われます。これにより、最大で約1ギガバイトまでのウィンドウサイズを表現できるようになり、高速・長距離通信の性能を劇的に向上させることが可能になりました。

SACK Permitted Option (Selective Acknowledgment)

従来のTCPでは、パケットロスが発生すると、失われたパケット以降に受信した全てのパケットを再送する必要がありました(Go-Back-N方式)。これは非常に非効率です。SACKは、受信側がどのセグメントを受け取ったかを不連続なブロックとして送信側に伝えることを可能にする仕組みです。これにより、送信側は失われたセグメントだけを選択的に再送すればよくなり、再送の効率が大幅に向上します。

3ウェイハンドシェイク中に「SACK Permitted」オプションを交換することで、双方はこのSACK機能を使用することに合意します。この合意がなければ、通信中にSACKを用いることはできません。

このように、3ウェイハンドシェイクは、接続の「有無」だけでなく、接続の「質」を決定するための重要な交渉の場でもあるのです。

第5章 パフォーマンスへの影響と最適化

3ウェイハンドシェイクはTCPの信頼性の根幹をなす仕組みですが、その一方でパフォーマンス上のトレードオフも存在します。特に、レイテンシ(遅延)に与える影響は無視できません。

ハンドシェイクがもたらす遅延

ハンドシェイクのプロセスをよく見ると、クライアントが最初のSYNを送信してから、サーバーが最後のACKを受信して接続が完全に確立するまでには、クライアントとサーバー間をパケットが1.5往復する必要があります。しかし、アプリケーションが実際にデータを送信し始めることができるのは、クライアントがSYN-ACKを受信した後です。つまり、アプリケーションデータが流れ始めるまでには、最低でも1 RTT (Round-Trip Time、往復遅延時間) が必要になるのです。

RTTが数ミリ秒程度のLAN環境ではこの遅延は問題になりませんが、モバイルネットワークや国際通信のようにRTTが数百ミリ秒になる環境では、この初期遅延がユーザー体感を大きく損なう原因となります。特に、今日のウェブサイトのように多数の小さなリソース(画像、CSS、JavaScriptファイルなど)を読み込む場合、リソースごとに新しいTCP接続を確立していると、このハンドシェイク遅延が何度も積み重なり、ページの表示が著しく遅くなります。

最適化技術

このハンドシェイクによる遅延を緩和するため、様々な技術が考案されてきました。

HTTP Keep-Alive と HTTP/2

最も基本的な最適化は、一度確立したTCP接続を使い回すことです。HTTP/1.1で導入されたKeep-Alive(持続的接続)は、一つのTCP接続上で複数のHTTPリクエスト/レスポンスをやり取りすることを可能にしました。これにより、ハンドシェイクのコストを最初の一回だけに抑えることができます。さらに進んだHTTP/2では、一つのTCP接続上で複数のリクエスト/レスポンスを並行して多重化できるため、接続を使い回す効率がさらに向上しました。

TCP Fast Open (TFO)

TFOは、ハンドシェイクの遅延そのものを削減しようとする、より野心的な技術です。一度通信したことのあるサーバーに対しては、2回目以降の接続時に、ハンドシェイクの最初のSYNパケットにアプリケーションデータの一部を含めて送信してしまおう、というアイデアです。

  1. 初回接続: 通常の3ウェイハンドシェイクを行います。このとき、サーバーはクライアントに対して「TFOクッキー」と呼ばれる暗号化されたトークンを発行します。
  2. 2回目以降の接続: クライアントは、最初のSYNパケットにこのTFOクッキーとアプリケーションデータ(例えばHTTP GETリクエスト)を一緒に含めて送信します。
  3. サーバー側: サーバーは受け取ったクッキーを検証し、正当であれば、SYN-ACKを返すのと同時に、パケットに含まれていたデータをアプリケーションに渡します。

これにより、クライアントはSYN-ACKを待たずにデータ送信を開始でき、RTTを最大で1回分削減できます。ただし、TFOはリプレイ攻撃などのセキュリティリスクも指摘されており、サーバーとクライアント双方での対応と慎重な設定が必要です。

第6章 セキュリティの側面:SYNフラッド攻撃

3ウェイハンドシェイクの仕組みは、その状態遷移の特性から、古典的かつ強力なDoS(Denial of Service)攻撃である「SYNフラッド攻撃」の標的となってきました。

SYNフラッド攻撃のメカニズム

この攻撃は、サーバーがSYNを受け取ってから最後のACKを受け取るまでの間、`SYN_RECEIVED` という半開きの状態で待機するという性質を悪用します。

  1. 攻撃者は、送信元IPアドレスを偽装した大量のSYNパケットを標的サーバーに送りつけます。
  2. サーバーは、それぞれのSYNパケットに対してSYN-ACKを返信し、`SYN_RECEIVED` 状態の接続情報をメモリ(SYNバックログキュー)に保持して、クライアントからの最後のACKを待ちます。
  3. しかし、SYN-ACKの宛先であるIPアドレスは偽装されている(存在しないか、攻撃とは無関係な第三者のもの)ため、サーバーに最後のACKが返ってくることはありません。
  4. サーバーは、タイムアウトするまで半開きの接続を保持し続けます。攻撃者がSYNパケットを送り続けることで、サーバーのSYNバックログキューはすぐに満杯になってしまいます。
  5. キューが満杯になると、サーバーはそれ以上新たなSYNパケットを受け付けられなくなり、結果として、正規のユーザーからの接続要求をすべて拒否してしまう状態に陥ります。これがサービス不能(Denial of Service)です。

この攻撃が厄介なのは、非常に少ない帯域幅で実行可能であり、かつ送信元IPが偽装されているため攻撃者の特定が困難である点です。

対策技術

SYNフラッド攻撃に対抗するため、いくつかの防御技術が開発されています。

SYNクッキー (SYN Cookies)

SYNクッキーは、SYNフラッド攻撃に対する非常に巧妙な防御策です。この技術を有効にすると、サーバーはSYNパケットを受け取った際に、`SYN_RECEIVED` 状態の情報をメモリに保持しません。その代わり、接続に関する情報(クライアントのIPアドレス、ポート、サーバー自身の秘密鍵など)をハッシュ計算し、その結果をシーケンス番号としてSYN-ACKに含めて返信します。

正規のクライアントであれば、このSYN-ACKに対して正しくACK(シーケンス番号+1)を返してきます。サーバーは、受け取ったACKパケットの確認応答番号から元の情報を逆算・検証することで、このACKが正当なものかを判断できます。検証に成功して初めて、サーバーは接続情報をメモリに確保し、接続を `ESTABLISHED` 状態に移行させます。

これにより、サーバーはACKが来るまで一切のリソースを消費しないため、SYNバックログキューが溢れるという攻撃の根本を無効化できます。

バックログキューの拡大とタイムアウトの短縮

より直接的な対策として、OSのカーネルパラメータを調整し、SYNバックログキューのサイズを大きくしたり、`SYN_RECEIVED` 状態のタイムアウト時間を短くしたりする方法もあります。しかし、これらは攻撃の規模によっては効果が限定的であり、根本的な解決策とはなりにくいです。

第7章 思想の対比:TCPとUDP

3ウェイハンドシェイクの複雑さ、そしてそれがもたらす信頼性とオーバーヘッドを理解するためには、もう一つの主要なトランスポート層プロトコルであるUDP(User Datagram Protocol)と比較するのが最も効果的です。

UDPは、TCPとは対照的に、極めてシンプルなプロトコルです。UDPには接続という概念がなく、ハンドシェイクも行いません。データを送りたいときは、宛先IPアドレスとポート番号を指定して、パケットを「送りっぱなし」にするだけです。そのため、「コネクションレス型」プロトコルと呼ばれます。

この違いがもたらす特性を以下の表にまとめます。

特性 TCP (Transmission Control Protocol) UDP (User Datagram Protocol)
接続形態 コネクション指向(3ウェイハンドシェイクで接続確立) コネクションレス
信頼性 高い(到達確認、順序保証、再送制御あり) 低い(到達も順序も保証しない)
フロー制御 あり(スライディングウィンドウ) なし
輻輳制御 あり(ネットワークの混雑状況に応じて送信量を調整) なし
オーバーヘッド 大きい(ヘッダが20バイト以上、ハンドシェイクの遅延) 小さい(ヘッダが8バイト固定)
主な用途 Web (HTTP/S), メール (SMTP), ファイル転送 (FTP)など、信頼性が重要な通信 DNS, VoIP, オンラインゲーム, ライブストリーミングなど、リアルタイム性が重要な通信

TCPの3ウェイハンドシェイクは、この表で挙げたTCPの持つ高い信頼性、フロー制御、輻輳制御といった高度な機能を実現するための「入場券」のようなものです。この最初の約束事があるからこそ、その後のデータ転送が安定して行えるのです。一方、UDPはそうした約束事を一切省くことで、低遅延と低オーバーヘッドを実現しています。どちらが優れているというわけではなく、アプリケーションの要求に応じて適切なプロトコルを選択することが重要です。この選択の根拠を理解するためにも、TCPがなぜハンドシェイクという手続きを踏むのかを知ることは不可欠です。

結論:単なる手続き以上の意味を持つ約束

TCPの3ウェイハンドシェイクは、単に通信を開始するための形式的な手続きではありません。それは、信頼性のないIPネットワークの上で、確実な双方向通信という城を築き上げるための、最初の、そして最も重要な礎石です。

この3回のやり取りを通じて、クライアントとサーバーは、過去の通信の亡霊に惑わされることなく、お互いの存在を確認し、これからの対話のルール(シーケンス番号)を同期させます。さらに、通信経路に最適化されたパラメータ(MSS、ウィンドウスケールなど)を交渉し、パフォーマンスを最大限に引き出す準備を整えます。この一連のプロセスは、遅延という代償を伴いますが、それと引き換えに得られる「信頼性」は、現代のインターネットアプリケーションのほとんどがその上で成り立っている、かけがえのない価値です。

開発者として、このハンドシェイクの裏側にある設計思想、パフォーマンスへの影響、そしてセキュリティ上の含意を深く理解することは、より堅牢で、高性能で、安全なアプリケーションを構築するための強力な武器となります。次にネットワークの問題に直面したとき、あるいはアプリケーションのパフォーマンスをチューニングしようとするとき、この「通信の約束」がどのように機能しているかを思い出すことで、問題の本質により深く迫ることができるでしょう。


0 개의 댓글:

Post a Comment