TCP接続確立の仕組み:3ウェイハンドシェールの詳細とパケット喪失時の挙動

TCP接続確立の標準手順

TCPセッションの開始は、クライアントとサーバー間で状態とシーケンス番号を同期する一連のプロセスで構成されます。このプロセスは通常、以下の3段階で進行します。

第1段階:接続要求(SYN)
サーバーは接続待ち状態(LISTEN)にあり、クライアントがSYNフラグをセットしたセグメントを送信します。このパケットにはクライアントが生成した初期シーケンス番号(ISN)や、通信に使用可能な最大セグメントサイズ(MSS)が含まれます。送信完了後、クライアントはSYN_SENT状態に移行します。

第2段階:同期応答(SYN-ACK)
サーバーはSYNセグメントを受信すると、接続維持用のバッファを確保し、自身のISNを生成します。次に、SYNフラグとACKフラグの両方をセットしたパケットをクライアントに返送し、状態をSYN_RCVDに変更します。このパケット内の確認応答番号は、クライアントのISNに1を加えた値となります。

第3段階:最終確認(ACK)
クライアントはSYN-ACKを受信すると、ACKフラグをセットしたパケットで応答します。これによりクライアントはESTABLISHED状態へ移行し、サーバーも同パケットを受信後にESTABLISHED状態となります。これで双方向のシーケンス番号同期が完了し、アプリケーションデータの送受信が可能になります。

TCPセグメント構造と制御フラグ

上記のフラグや番号の仕組みを正確に理解するには、TCPセグメントの構造を把握する必要があります。TCPヘッダーの標準長は20バイトですが、オプション字段を含めると最大60バイトまで拡張可能です。主要フィールドは以下の通りです。

TCPセグメント構造図
  • ソース/デスティネーションポート:各16ビット。送受信アプリケーションを特定します。
  • シーケンス番号/確認応答番号:各32ビット。送受信データのバイト単位の位置を示します。
  • ヘッダー長(HLEN):4ビット。32ビットワード単位でヘッダー長を指定します。
  • 制御フラグ:各1ビット。URG(緊急)、ACK(確認)、PSH(プッシュ)、SYN(同期)、FIN(終了)、RST(リセット)などがあります。SYNは接続確立時、ACKは受信確認、FINは切断時に使用されます。
  • ウィンドウサイズ:16ビット。受信バッファの空き容量を通知し、輻輳制御とフロー制御に寄与します。
  • チェックサム:16ビット。ヘッダーおよびデータ部の誤り検出に必須です。
  • 緊急ポインター:16ビット。URGフラグが立っている場合、緊急データの終了位置を指します。

2回ではなく3回の理由

単純に双方向の番号同期だけであれば2回で完了しそうに思えますが、3回目のACKには重要な防御的な意味があります。ネットワーク経路では遅延した古いパケットが突然届くことがあります。2回目(SYN-ACK)のみで接続を確立すると、クライアントが既に切断した古いセッションのSYN-ACKが後から届いた際、サーバーが誤って新規接続としてリソースを消費してしまいます。3回目のACKを送信させることで、クライアントが現在も積極的に接続を維持していることをサーバーに明示でき、リソースの無駄遣いや状態の不一致を防げます。

各段階でのパケット喪失時の挙動

ハンドシェール中にパケットが消失した場合、TCPはタイムアウトベースの再送機構を働かせます。喪失箇所に応じて、誰が何を再送するかが異なります。

初回パケット喪失(SYN消失)

クライアントがSYNを送信後、サーバーから応答が返ってこない場合、クライアントはリトライタイマーを起動します。Linuxカーネルの初期再送タイムアウト値は、RFC 6298に準拠して定義されています。実際のパラメータ管理は、スクリプトやシステムコマンドを通じて確認できます。

import subprocess
import os

def fetch_tcp_retransmission_config():
    try:
        # Linux sysctl 設定の動的取得
        syn_limit = subprocess.check_output(["sysctl", "-n", "net.ipv4.tcp_syn_retries"]).decode().strip()
        synack_limit = subprocess.check_output(["sysctl", "-n", "net.ipv4.tcp_synack_retries"]).decode().strip()
        
        # 再送間隔の初期値(RTO)をミリ秒単位で取得
        initial_rto = int(subprocess.check_output(["sysctl", "-n", "net.ipv4.tcp_rto_min"]).decode().strip())
        
        return {
            "client_syn_retries": int(syn_limit),
            "server_synack_retries": int(synack_limit),
            "base_timeout_ms": initial_rto
        }
    except Exception as err:
        print(f"カーネルパラメータの読み取りエラー: {err}")
        return None

tcp_params = fetch_tcp_retransmission_config()
if tcp_params:
    print(f"クライアントSYN再送上限: {tcp_params['client_syn_retries']}回")
    print(f"サーバーSYN-ACK再送上限: {tcp_params['server_synack_retries']}回")
    print(f"初期タイムアウト基準: {tcp_params['base_timeout_ms']} ms")

実際には、/proc/sys/net/ipv4/tcp_syn_retries で再送上限(デフォルト6)が管理されます。タイムアウト間隔は指数関数的に増加するアルゴリズム(RTO計算)に従うため、2回目以降の待機時間は初期値より長くなります。上限回数を超えると接続試行は中止され、エラーがアプリケーションに報告されます。

中段パケット喪失(SYN-ACK消失)

サーバーがSYN-ACKを返したがクライアントに届かなかった場合、両者が異なる動作を示します。クライアントはSYN応答がないと判断して初回のSYNを再送します。一方のサーバーは、クライアントからの最終ACKを待機しているため、自身が発行したSYN-ACKの再送を試みます。この際、tcp_synack_retries(デフォルト5)が上限となり、これを越えるとサーバー側の接続試行は破棄されます。

最終パケット喪失(ACK消失)

クライアントが最終ACKを送信したがサーバーに到達しなかったケースです。この場合、サーバーはクライアントが自身のISNを受け入れたことを確認できず、SYN-ACKの再送を続けます。クライアント側は既に接続確立状態とみなしてデータ送信を開始するため、サーバーからの再送SYN-ACKを受信すると、再度ACKで応答して状態を同期します。サーバーの再送上限は中段パケット喪失時と同様に5回が標準です。

タグ: tcp-protocol network-handshake packet-loss-recovery linux-networking retransmission-algorithm

5月19日 19:19 投稿