【実務・中級編】 HTTPS(TLS)ハンドシェイクとTCP接続の順序 – ネットワーク基礎とWebセキュリティ実践ガイド

はじめに:ブラウザの裏側で何が起きているか、その「本当のレイテンシ」を見極めろ

Web APIの設計や、高スループットが求められるインフラのチューニングに携わっていると、必ず直面するのが「レイテンシ(遅延)の壁」です。

「なぜか初回リクエストだけがやたらと遅い」
「地理的に離れたリージョンへのAPI呼び出しで、どうしても数百ミリ秒のオーバーヘッドが削れない」

こうした現場のトラブルシューティングにおいて、アプリ側のコードばかりをプロファイリングしても泥沼にハマるだけです。パケットキャプチャを開き、TCPの3-way handshake(3ウェイハンドシェイク)から、その直後に始まるTLS(Transport Layer Security)ハンドシェイク、そしてアプリケーション層のHTTPリクエストに至るまでの「正確なシーケンス」を解剖できなければ、真のパフォーマンスチューニングなど語れません。

今回は、TCPとTLSが織りなす接続プロセスの全貌を、パケットの動きそのものを可視化しながら、実務で役立つTipsとともに徹底的に紐解いていきます。

—

1. TCP 3-way HandshakeとTLSハンドシェイクの全体像

私たちが普段何気なく叩いている https://api.example.com/v1/data というURL。この背後では、アプリケーション層のデータ(HTTPリクエスト)が流れる前に、OSのカーネルとネットワークスタックが激しい「儀式」を行っています。

全体のシーケンスは、大きく分けて2つのフェーズで構成されています。

1. TCP Connection Establishment (3-way Handshake): 信頼性のあるトランスポート層の確立
2. TLS Handshake: 暗号化されたセキュアなセッションの確立(TLS 1.3を基準)

まずは、この2つがどのように連続して実行されるのか、全体のシーケンス図を見てみましょう。

Client (Browser / App)                    Server (API Gateway)
  |                                         |
  | -------- [1] SYN ------------->         |  <-- TCP 3-way Handshake開始
  | <------- [2] SYN-ACK ----------         |
  | -------- [3] ACK (TCP確立) --->         |  <-- ここでTCPコネクション確立
  |                                         |
  | -------- [4] Client Hello ---->         |  <-- TLSハンドシェイク開始 (TLS 1.3)
  |          (Key Share, Ciphers)           |
  |                                         |
  | <------- [5] Server Hello -----         |
  |          (Key Share, Certificate)       |  <-- サーバー側で暗号鍵生成完了
  |          [Change Cipher Spec]           |
  |          [Finished]                     |
  |                                         |
  | <------- [6] Finished ---------         |  <-- クライアント側で検証完了・暗号化通信可能
  |                                         |
  | -------- [7] Encrypted HTTP Req ->      |  <-- アプリケーションデータ送信開始

現場のエンジニアとして特に意識してほしいのは、「TCPが完全に繋がるまで、一文字たりともTLSのデータは送れない」という物理的(かつ論理的)な制約です。

—

2. 各ハンドシェイクフェーズの詳細とパケットの挙動

フェーズ1:TCP 3-way Handshake(確実な道づくり)

信頼性のある通信路を確保するため、お互いの生存確認とシーケンス番号の同期を行います。

  • SYN: クライアントが「通信を始めたいです。初期シーケンス番号はこれです」と通知。
  • SYN-ACK: サーバーが「了解です。こちら側の初期シーケンス番号はこれ、あなたのSYNも受け取りました」と返答。
  • ACK: クライアントが「サーバーからのSYN-ACKを受け取りました」と返答し、これにてTCPコネクションが確立(ESTABLISHED状態)します。

この3つのパケットが往復するだけで、物理的な距離に応じた1往復半(1.5 RTT: Round Trip Time)の遅延が確実に発生します。

フェーズ2:TLSハンドシェイク(鍵の共有と身元証明)

TCPが確立した瞬間、TLSのネゴシエーションが始まります。現代のデファクトスタンダードである TLS 1.3 では、このプロセスが劇的に洗練され、高速化されています。

従来(TLS 1.2以前)は鍵交換アルゴリズムのネゴシエーションに余分な往復が必要でしたが、TLS 1.3ではクライアント側の ClientHello に予測可能な暗号スイートや鍵共有(Key Share)の候補を最初から含めることで、ハンドシェイクのラウンドトリップを最小限に抑えています。

  • ClientHello:

クライアントがサポートするTLSバージョン、暗号スイート(暗号化アルゴリズムの組み合わせ)、そして自身の公開鍵パラメータ(key_share)を送信します。「私はこの暗号化手法が使えます。鍵の種はこれです」と最初に手を挙げるわけです。

  • ServerHello / 署名・証明書 / Finished:

サーバーは ClientHello を受け取ると、自身の証明書(Certificate)、サーバー側の公開鍵パラメータ、そして選択した暗号スイートを返します。サーバーはこの時点で共通鍵の導出が可能なため、自身の Finished メッセージまでを一気に送信します。

  • Finished (Client):

クライアントはサーバーからのパラメータを受け取り、共通鍵を生成してハンドシェイクの完了を告げます。

これによって、TLS 1.3では1 RTTでセキュアなハンドシェイクが完了します。TCPの1.5 RTTと合わせると、実際にアプリケーションデータが流れるまでに最低でも 2.5 RTT のレイテンシが必ず発生するという計算になります。

—

3. 実務で直面するレイテンシ要因とパフォーマンス・チューニング

インフラ設計やAPI開発において、「なぜかレイテンシが落ちない」というボトルネックの多くは、このハンドシェイクの回数とネットワークの物理的距離に起因します。現場で使える具体的なチューニング手法を見ていきましょう。

① TLS 1.3の強制と古いプロトコルの排除

いまだに TLS 1.2 をフォールバックとして残しているシステムを見かけますが、TLS 1.2のフルハンドシェイクは 2 RTT かかります。現代のインフラストラクチャにおいては、クライアントとサーバーの両方で TLS 1.3 を強制、あるいは優先させる設定が必須です。

NginxでTLS 1.3のみを許可し、不要なオーバヘッドを削ぎ落とす設定例は以下の通りです。

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # 古い脆弱なプロトコルを完全に排除し、TLS 1.3のみを強要する
    ssl_protocols TLSv1.3;

    # パフォーマンスに優れたモダンな暗号スイートを指定
    ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:DHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # セッション再開(Session Resumption)を有効化し、2回目以降のハンドシェイクをゼロRttに近づける
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1h;

    # 乱数生成の最適化
    ssl_buffer_size 4k; # パケットのフラグメント化を防ぎ、初速のレスポンスを改善する

    location / {
        proxy_pass http://backend_upstream;
    }
}

② 0-RTT Resumption(早期データ送信)の検討

TLS 1.3には、一度接続したことのあるクライアントに対して、2回目以降の接続時にハンドシェイクを待たずにアプリケーションデータを送り始める 0-RTT Resumption という強力な機能があります。

ただし、リプレイアタック(悪意ある攻撃者が過去のパケットを再送する攻撃)に対して脆弱性を持つため、冪等性(Idempotency)が保証されていない POST リクエストなどでの利用には細心の注意が必要です。GETメソッド中心の静的なAPIやCDNエッジでの終端処理においては、極めて有効な選択肢となります。

—

4. デバッグと実検証:コマンドラインからハンドシェイクを暴く

理屈はわかりましたが、実際に目の前のサーバーがどのようなハンドシェイクを行っているのか、自分の手でパケットを覗いてみましょう。実務の現場で即座に使えるデバッグコマンドを紹介します。

curlでハンドシェイクの詳細とTLSバージョンを暴く

APIのレスポンスタイムだけでなく、どのプロトコルで接続が確立されたのかを正確に計測するには、curl のフォーマット機能が重宝します。

# TLSのバージョン、ハンドシェイクにかかった時間、証明書の検証結果を詳細に確認する
curl -Iv https://api.ipify.org \
  --write-out "\n--- 統計情報 ---\nTCP Connection Time: %{time_connect}s\nTLS Handshake Time: %{time_appconnect}s\nTotal Time: %{time_total}s\n" \
  -o /dev/null

このコマンドを実行すると、次のような出力が得られます。

*   Trying 1.2.3.4:443...
* Connected to api.ipify.org (1.2.3.4) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client Hello (1):
* TLSv1.3 (IN), TLS handshake, Server Hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
...
--- 統計情報 ---
TCP Connection Time: 0.045s
TLS Handshake Time: 0.082s
Total Time: 0.120s

time_connect がTCP確立までの時間、time_appconnect がTLSハンドシェイク完了までの時間です。この数値を見るだけで、インフラの物理的な遅延(ネットワーク起因)なのか、サーバー側の処理遅延なのかが一発で切り分けられます。

Python (requests / urllib3) での接続挙動の確認

アプリケーションコード側からTLSの挙動を厳密に制御したい場合、例えばカスタムのCA証明書や特定のTLSバージョンを強制したい場合は、次のように記述します。

import ssl
import requests
from urllib3.poolmanager import PoolManager
from urllib3.util import ssl_

# TLS 1.3を強制し、セキュアな通信を行うためのカスタムAdapterクラス
class TLS13Adapter(requests.adapters.HTTPAdapter):
    def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
        # 明示的にTLSコンテキストを作成し、最小プロトコルバージョンをTLS 1.3に固定
        context = ssl_.create_urllib3_context(
            minimum_version=ssl.TLSVersion.TLSv1_3
        )
        kwargs['ssl_context'] = context
        return super().init_poolmanager(connections, maxsize, block, **kwargs)

# セッションにカスタムアダプターをバインド
session = requests.Session()
session.mount("https://", TLS13Adapter())

try:
    # APIリクエストの実行
    response = session.get("https://api.ipify.org?format=json", timeout=5.0)
    print(f"ステータスコード: {response.status_code}")
    print(f"レスポンスボディ: {response.json()}")
except requests.exceptions.SSLError as e:
    print(f"TLSハンドシェイクエラーが発生しました: {e}")
except requests.exceptions.RequestException as e:
    print(f"通信エラーが発生しました: {e}")

このコードでは、Pythonの標準ライブラリである ssl モジュールをラップし、urllib3 のレイヤーで強制的に TLS 1.3 を使わせることで、レガシーな暗号化通信の混入をアプリ層からブロックしています。セキュリティ要件が厳しい金融系やヘルスケア系のAPIクライアントを実装する際には、こうした明示的なバージョン制御がエンジニアの武器となります。

—

おわりに

HTTPS通信の裏側で行われているTCP 3-way handshakeとTLSハンドシェイクは、一見すると「ブラックボックスな暗号化の儀式」に見えるかもしれません。しかし、パケットレベルの挙動を分解し、それぞれのステップがどのような理由で存在し、どれだけのレイテンシを消費しているのかを把握していれば、インフラの設計ミスやパフォーマンス低下の兆候を誰よりも早く察知できるようになります。

「なぜ遅いのか」と悩んだときは、まずパケットキャプチャを開き、あるいは curl の時間計測機能を使い、TCPとTLSの境界線に目を向けてみてください。そこには、ネットワークの真実が必ず隠されています。

コメント

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