【実務・中級編】 TCPの3ウェイ・ハンドシェイクとSYN/ACKフラグの役割 – ネットワーク基礎とWebセキュリティ実践ガイド

境界の向こう側のリアル:TCP 3ウェイ・ハンドシェイクとSYN/ACKの深層

おい、新米。今日もAPIのレスポンスが遅いだの、負荷分散装置(ロードバランサー)の裏で502が頻発するだのと、Slackの通知音が鳴りっぱなしじゃないか。

「Webアプリケーションが動かない」「APIがつながらない」というトラブルに直面したとき、お前は真っ先にどこを見る?アプリケーションのログか?それともソースコードか?
ちょっと待て。そのコードを書き換える前に、お前の足元――レイヤー4の世界で何が起きているか、想像したことはあるか?

私たちが何気なく叩くcurlや、モダンなWebフロントエンドからのfetch()の裏側では、アプリケーション層がデータを流し始めるよりずっと前に、極めて厳格な「握手の儀式」が行われている。それが TCP 3ウェイ・ハンドシェイク だ。

今回は、このパケットの往復運動の裏側にある「真実」を、現場の泥臭い実例を交えながら徹底的に紐解いていこう。RFCの冷たい仕様書の向こう側で、パケットがどう喘ぎ、どう繋がっているのか、その目でしっかり焼き付けるんだな。

—

1. なぜ「握手」が必要なのか?(信頼性という名の代償)

Web APIの設計やインフラのサイジングをしていると、UDPの軽快さに魅力を感じることがあるだろう。しかし、私たちのビジネスを支える基幹の通信、例えばECの決済や厳密なデータ同期において、信頼性のない通信は文字通り「致命傷」になる。

IP(Internet Protocol)は、いわば「宛先だけ書いて、途中で消えても知らん顔の郵便配達員」だ。パケットが届いたかどうか、順序がバラバラになっていないか、IP自身は一切保証してくからない。
そこで登場するのがTCP(Transmission Control Protocol)だ。

TCPは、データを送り出す前に「今からお前と通信を始めるが、準備はいいか?」という合意形成を行う。これがコネクション確立、すなわち3ウェイ・ハンドシェイクの本質だ。このプロセスを経ることで、通信の双方向性、順序制御、そしてフロー制御の土台が完全に組み上げられる。

—

2. SYN、SYN-ACK、ACKのダンス:パケットの全貌

それでは、クライアントとサーバーの間で交わされる3つのステップを、実務的な視点で分解していこう。ネットワークアナライザ(Wiresharkなど)を覗いたときに、お前が目撃するパケットの正体はこれだ。

[Client]                                           [Server]
   |                                                  |
   | ------ [1] SYN (seq=x) ------------------------> |
   |                                                  |
   | <----- [2] SYN-ACK (seq=y, ack=x+1) ------------ |
   |                                                  |
   | ------ [3] ACK (ack=y+1) ----------------------> |
   |                                                  |
   v                                                  v

ステップ1:SYN(Synchronize)

クライアントがサーバーに対して、「通信を始めたい、そしてこれが私の初期シーケンス番号(ISN)だ」と宣言する最初のパケットだ。

  • フラグ: SYN が 1 にセットされる。
  • 動作: クライアントはランダムに生成したシーケンス番号(例:seq=x)を通知する。このとき、データ本体は含まれていないが、TCPヘッダーのオプション領域(MSS:Maximum Segment Sizeやウィンドウサイズのスケーリングファクターなど)が一緒に詰め込まれる。

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

サーバーがクライアントの「SYN」を受け入れ、自らも通信の準備ができていることを伝えるパケットだ。

  • フラグ: SYN と ACK の両方が 1 にセットされる。
  • 動作: サーバーは、クライアントのシーケンス番号に1を加えた値(ack=x+1)を返すことで、「お前の最初のメッセージを確実に受け取った」と証明する。同時に、サーバー自身の初期シーケンス番号(seq=y)をクライアントに提示する。

ステップ3:ACK(Acknowledgment)

クライアントが、サーバーからの「SYN-ACK」を受け取り、最終的な確認を返すパケットだ。

  • フラグ: ACK が 1 にセットされる。
  • 動作: クライアントは、サーバーのシーケンス番号に1を加えた値(ack=y+1)を返す。この瞬間、クライアント・サーバー間のコネクションが「ESTABLISHED(確立)」状態となり、いよいよアプリケーション層のデータ(HTTPリクエストなど)が流し込まれる。

—

3. 実務で直面するパラメーターと「SYNフラッド攻撃」の恐怖

インフラエンジニアとして現場に立つと、この3ウェイ・ハンドシェイクの裏側にあるパラメーターや脆弱性に頭を悩ませることが多々ある。

バックログキュー(Backlog Queue)の仕組み

サーバーが SYN を受け取ったとき、まだステップ3の ACK が返ってくる前の中途半端な状態を保持するために、OSのカーネル内には「SYNキュー(半オープン接続用)」と「Acceptキュー(確立済み接続用)」という2つのバッファが存在する。

もし、悪意ある攻撃者が偽のIPアドレスから膨大な SYN パケットを送りつけたらどうなるか?
そう、これが有名な SYNフラッド攻撃(SYN Flood) だ。サーバーのSYNキューは瞬く間に満杯になり、正当なユーザーからの新しい接続要求(SYN)を一切処理できなくなる(サービス拒否状態 / DDoS)。

Linux環境でこの挙動を制御・緩和するためには、/etc/sysctl.conf で次のようなカーネルパラメータをチューニングするのが実務の定石だ。

# /etc/sysctl.conf の設定例:SYNフラッド対策とコネクション耐性の強化

# SYNキューが溢れた際に、クッキーを用いてセッションを維持する「SYNクッキー」を有効化
net.ipv4.tcp_syncookies = 1

# SYNパケットを受信した際のリトライ回数を減らし、リソースの解放を早める
net.ipv4.tcp_synack_retries = 2

# SYNキュー(半オープン接続)の最大長を拡張する(デフォルト値は環境により異なる)
net.ipv4.tcp_max_syn_backlog = 4096

# TIME_WAIT状態のソケットを素早く再利用・リサイクルできるようにする
net.ipv4.tcp_tw_reuse = 1

こうした設定を適切に行うことで、トラフィックの急増耐性を劇的に高めることができる。現場のSREなら、このあたりのチューニング値の根拠を即答できなければ失格だ。

—

4. コードと通信の架け橋:実際にパケットの挙動を観測する

では、実際に私たちが書くコードやコマンドが、このTCPハンドシェイクとどう結びついているのかを見ていこう。

1. curl による詳細なコネクション情報の観測

APIの疎通確認やデバッグで頻繁に使う curl 命令だが、 -v(verbose)オプションや -w(write-out)オプションを使うことで、TCPハンドシェイクを含めた詳細なメトリクスを暴くことができる。

# 接続確立にかかった時間(time_connect)や全体の時間を計測する実用コマンド
curl -so /dev/null -w "DNS解決: %{time_namelookup}s\nTCP接続(ハンドシェイク): %{time_connect}s\n総時間: %{time_total}s\n" https://api.example.com/v1/health

このコマンドを実行した際、time_connect の値が異常に大きい場合は、ネットワークの物理的な遅延(レイテンシ)だけでなく、ファイアウォールやセキュリティアプライアンスが SYN パケットをドロップしていないか(パケットロスやパケット破棄)を疑うべきだ。

2. Python (requests) によるタイムアウト制御の実装

アプリケーション層から外部APIを叩く際、TCPハンドシェイクの段階でハングアップするケースは少なくない。例えば、宛先サーバーが死んでいる、あるいは途中のルーターでルーティングループが発生している場合だ。

Pythonの requests ライブラリを使用する際は、必ずコネクション確立(connect)とデータ読み込み(read)のタイムアウトを明示的に分けるべきだ。

import requests
from requests.exceptions import Timeout, RequestException

# 接続先APIのエンドポイント
api_url = "https://api.example.com/v1/data"

try:
    # connect=(TCPハンドシェイク完了までの許容秒数), read=(データ受信までの許容秒数)
    # ここではSYN送信からACK返却までの3ウェイ・ハンドシェイクに「3.0秒」の制限をかける
    response = requests.get(api_url, timeout=(3.0, 10.0))
    
    # ステータスコードのチェック
    response.raise_for_status()
    
    print("API通信成功:", response.json())

except Timeout:
    print("【警告】TCPの3ウェイ・ハンドシェイク、またはデータ読み込みがタイムアウトしました。ネットワーク経路やファイアウォールを確認してください。")
except RequestException as e:
    print(f"その他のリクエストエラーが発生しました: {e}")

この timeout=(3.0, 10.0) という書き方は実務で非常に強力だ。connect のタイムアウトを短く設定しておくことで、障害発生時にアプリケーション全体がスレッドプールを枯渇させ、共倒れになる最悪のシナリオを防ぐことができる。

—

5. シニアからの現場の教訓:パケットは嘘をつかない

トラブルシューティングの現場において、開発者とインフラエンジニアの議論が平行線を辿ることがある。
「アプリのコードに問題はありません」「いや、ネットワーク側は正常です」――こういう水掛け論に終始したとき、どちらを信じるべきか?

答えは簡単だ。「パケットをキャプチャしろ」。

人間は嘘をつくし、思い込みで動く。だが、Wiresharkや tcpdump が吐き出すダンプデータは絶対に嘘をつかない。
もし SYN を投げているのに SYN-ACK が返ってこないのであれば、それはルーターのACL(アクセスコールリスト)か、クラウドのセキュリティグループ(Security Group / NACL)が容赦なくパケットをブラックホール送りにしている証拠だ。逆に、SYN-ACK は返っているのにクライアント側が ACK を返さずに再送(Retransmission)の嵐になっているなら、OSのファイアウォール設定やローカルのソケット枯渇を疑うべきだ。

TCPの3ウェイ・ハンドシェイクという基本中の基本のメカニズムを深く理解しているか否かで、障害発生時の復旧スピードは数倍、いや数十倍変わってくる。

さあ、今日の講義はここまでだ。
次に「つながらない」というアラートが上がったときには、慌ててコードをいじる前に、頭の中でこのSYN、SYN-ACK、ACKのダンスを思い浮かべてみろ。おのずと、次に打つべき手がクリアに見えてくるはずだ。

コメント

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