Dev Study
← ネットワークの仕組み
レッスン 9 / 13·約 16 分·第2期 ネットワークの仕組み

なぜ手を3回握るのか — 「接続」という幻想の作り方

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

第21週でポート番号を見て、通信の相手が「どのアプリか」まで絞り込めるようになりました。今回はその2つの端点のあいだに「接続」という状態を作る手続きを扱います。ルータより上、アプリケーションより下の段の話です。

まず結論

IPは荷物を1つずつ投げるだけで、「つながっている」という状態をどこにも持ちません。TCPの接続とは、両端のメモリに置かれた合意のことです。その合意を作る手続きが3ウェイハンドシェイクです。

そして「3ウェイ」は3往復ではありません。3つのメッセージ、つまり1往復と片道です。クライアントがデータを送り始められるまでは1 RTT で、3つ目のACKと同時にデータを送れます。

? 読む前に予測

「3ウェイハンドシェイク」でTCP接続が確立します。クライアントが最初のデータを送り始められるようになるまでに、往復(RTT)は何回必要でしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

「接続」はどこにも実在しない

第21週で、ポート番号によって通信の相手が「どのホストのどのアプリか」まで絞り込めることを見ました。しかし宛先が分かることと、そこと「つながっている」ことは別の話です。

ここで一度、下の段が何をしてくれているかを思い出してください。IPは荷物を1つずつ投げるだけです。 届いたかどうかを教えてくれませんし、投げた順に着くとも限りません。線の途中に「接続」という装置があるわけでもありません。

にもかかわらず、私たちはコードで connect() を呼び、「接続が確立した」「接続が切れた」と日常的に語ります。この接続とは何でしょうか。

両端のメモリに置かれた、合意の記録です。 サーバ側のメモリに「この相手とこの番号でやりとり中」という情報があり、クライアント側にも対応する情報がある。この2つが揃っている状態を、私たちは「接続」と呼んでいます。線の上には何もありません。

接続の実体は、両端に置かれた状態のペア
クライアント状態: ESTABLISHED
途中の経路何も覚えていない
サーバ状態: ESTABLISHED
この2つが揃っている状態を「接続」と呼んでいる
接続という実体は線の上には無い
  • ・ルータは通り過ぎるパケットを1つずつ転送するだけで、接続を知らない
  • ・だから「接続の確立」とは、両端の状態を揃える作業にほかならない

すると問題は一気に具体的になります。離れた2つの場所にある状態を、どうやって揃えるのか。 3ウェイハンドシェイクは、その手続きです。

具体実物

RFC 9293 の図をそのまま読む

規格が実際に何を書いているかを見ます。RFC 9293 §3.5 の Figure 6 が、最も単純な3ウェイハンドシェイクです。

RFC 9293 §3.5 Figure 6(状態名とフラグ表記は原文のまま)追跡ログ
TCP Peer A TCP Peer B
 
1. CLOSED LISTEN
 
2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED
 
3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
 
4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
 
5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED

読み方を補っておきます。左端の CLOSED SYN-SENT ESTABLISHED は状態名で、そのセグメントが出ていった/着いた後の状態を表します。中央の <CTL=SYN> が制御ビットです。原文では SYN / SYN,ACK(カンマ区切り)/ ACK と書かれています。

2行目、A が「私は 100 番から数え始めます」と宣言します(SEQ=100)。3行目、B が「101 番を待っています」と確認しつつ(ACK=101)、同時に「私は 300 番から数え始めます」と宣言します(SEQ=300)。4行目、A が「301 番を待っています」と返します。

この2行目から4行目までが、3つのメッセージです。

そして注目してほしいのが4行目と5行目のシーケンス番号です。どちらも SEQ=101 で、同じ番号になっています。RFCはその理由を括弧の中でこう説明しています。

the ACK does not occupy sequence number space (if it did, we would wind up ACKing ACKs!)

ACKだけのセグメントはシーケンス番号を消費しません。もし消費するなら、そのACKにもACKが必要になり、ACKのACKのACKが無限に続いてしまう——というわけです。

✓ ここまでの確認

RFC 9293 の Figure 6 では、4行目の ACK(SEQ=101)と5行目のデータ付きセグメント(SEQ=101)のシーケンス番号が同じです。これはなぜですか。

抽象原理

「3ウェイ」は3往復ではない

ここが本レッスンで最も誤解される点です。そして誤解したまま性能を見積もると、数字が3倍ずれます。

「3ウェイ」が数えているのは、往復の回数ではなくメッセージの数です。 RFC 9293 自身がそう書いています。

this is called the three-way (or three message) handshake (3WHS)

数えてみます。A が SYN を送り、B から SYN,ACK が返る。ここまでで1往復です。3つ目の ACK は A から B へ向かう片道で、A はその返事を待ちません。合計は「1往復 + 片道」であって、3往復ではありません。

クライアントから見た、データを送り始められるまでの時間
SYN →行き(片道)○ 0.5 RTT 経過
← SYN,ACK帰り(片道)○ 1 RTT 経過 ★ここで往復が1回完了
ACK + DATA →返事を待たない片道○ 同じセグメントにデータを載せられる
↓
データを送り始められるまで = 1 RTT(3 RTT ではない)
  • ・Figure 6 の4行目と5行目のSEQが同じ = ACKは番号を消費しない = データを相乗りできる
  • ・データが相手に届くまでは 1 RTT + 片道、その応答が返るまでは 2 RTT

なぜ相乗りできるかというと、さきほど確認した「ACKはシーケンス番号を消費しない」という性質のおかげです。4行目と5行目が同じ SEQ=101 である以上、この2つは1つのセグメントにまとめられます。

抽象原理

なぜ3回なのか — RFCは2つの説明を与えている

では、なぜ2回でも4回でもなく3回なのでしょうか。ここでRFCの書き方が面白く、2つの異なる説明が別々の節に置かれています。

1つ目は、同期の必要条件です(§3.4.1)。 接続を確立するには、各側が自分のISNを送り、それに対する確認応答を受け取る必要があります。原文は「The synchronization requires each side to send its own initial sequence number and to receive a confirmation of it in acknowledgment from the remote TCP peer」と書いています。素直に並べると4つの手順になります。

RFC 9293 §3.4.1 — 本来は4つの手順追跡ログ
1) A --> B SYN my sequence number is X
2) A <-- B ACK your sequence number is X
3) A <-- B SYN my sequence number is Y
4) A --> B ACK your sequence number is Y

そして続けてこう書かれています。「Because steps 2 and 3 can be combined in a single message this is called the three-way (or three message) handshake (3WHS)」。2番と3番はどちらも B から A への送信なので、1つのパケットにまとめられる。 これが SYN,ACK の正体です。4つの論理的な手順が、3つのメッセージに畳まれている——これが「3」の由来です。

2つ目の説明が、実は本題です。 RFC 9293 §3.5 には、こう書かれています。

The principal reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

つまりRFCが「主な理由」と呼んでいるのは、ISNの交換ではなく「古い重複SYNによる混乱の防止」のほうです。

古い重複SYNとは何でしょうか。ネットワークの中には、経路の迂回などで異様に遅れて届くパケットが存在しえます。ずっと前に終わった接続のSYNが、今ごろになって到着することがあるのです。

もし受け取った側がSYN1つで接続を開いてしまったら、誰も望んでいない接続が勝手に成立します。 §3.4.1 はこの問題をこう説明しています。最初のSYNを受け取った側には「それが古いものかどうかを知る術がない(has no way of knowing whether the segment was an old one or not)」ので、送信側に確認を求めなければならない、と。

3つ目のACKは、まさにこの確認です。B は自分が選んだ SEQ=300 を送り、「301 を確認してくる応答が返ってきたら本物だ」と判断できます。古い重複SYNには送り主の実体がないので、この確認は返ってきません。

具体実物

ISNをランダムにする理由と、その限界

ここまでで「各側がISNを宣言する」と書いてきました。ではその番号はどう選ぶのでしょうか。0から始めれば簡単そうですが、規格はそれを許しません。

RFC 9293 §3.4.1 は生成式を定めています(SHLD-1)。

RFC 9293 §3.4.1 — ISN の生成式設定ファイル
ISN = M + F(localip, localport, remoteip, remoteport, secretkey)
 
M : 約4マイクロ秒ごとに進む32ビットのカウンタ
F() : 接続を識別する4つの値と秘密鍵を入力とする擬似乱数関数
制約 : F() は外部から計算可能であってはならない (MUST-9)

なぜ予測困難にするのでしょうか。§3.4.1 が名指ししている脅威はこうです。「There are security issues that result if an off-path attacker is able to predict or guess ISN values」。

off-path attacker(経路外の攻撃者)とは、その通信を直接は見られない位置にいる相手のことです。パケットを覗けないので、シーケンス番号を当てずっぽうで言い当てるしかありません。ISNが予測可能なら、この当てずっぽうが成功してしまいます。だから F() は外部から計算できてはならない、と定められています。

ここで、保護の範囲を正しく捉えておくことが重要です。

✓ ここまでの確認

RFC 9293 が3ウェイハンドシェイクの「principal reason(主な理由)」として挙げているのはどれですか。

⇄ 2つを見比べる
具体例 A

TCPの3ウェイハンドシェイク — 通り道を作る

交わしているのはシーケンス番号の宣言と、その確認だけです。暗号も相手の身元確認も一切ありません。これが終わって得られるのは「順序どおりに、欠落なく届くバイトの流れ」であって、その中身は誰にでも読めます。3つのメッセージ = 1往復 + 片道です。

交わされる内容(RFC 9293 §3.5)追跡ログ
SYN : 私は 100 番から数え始めます
SYN,ACK : 101 番を待っています / 私は 300 番からです
ACK : 301 番を待っています
 
得られるもの: 順序どおり・欠落なく届く通り道
守られないもの: 中身の秘密、相手が誰かの保証
具体例 B

TLSのハンドシェイク — 相手と鍵を確かめる

TLSが交わすのは、暗号パラメータの折衝・サーバ証明書・署名・鍵素材です。RFC 9846 は TLS のハンドシェイクを「authenticates the communicating parties, negotiates cryptographic algorithms and parameters, and establishes shared keying material」と説明しています(廃止された RFC 8446 では algorithms の箇所が modes でした)。順序と到達はTCPに任せきりです。

交わされる内容(RFC 9846 §2 Figure 1 を簡略化)追跡ログ
ClientHello : 使える暗号方式と鍵素材の候補
ServerHello : 使う暗号方式の決定と鍵素材
Certificate : サーバの証明書(あなたは誰か)
CertificateVerify / Finished : 署名と完了確認
 
得られるもの: 相手の認証と、共有された暗号鍵
前提にしているもの: 順序どおり・欠落なく届く通り道

どちらも「ハンドシェイク」と呼ばれ、どちらも通信の冒頭で数往復のメッセージを交わします。この2つは何のために手を握っているのでしょうか。そして、なぜ両方とも必要なのでしょうか。

それぞれのハンドシェイクが終わった時点で「何が保証されたことになるか」を並べてみてください。片方が前提にしているものを、もう片方が作っています。

この章のまとめ

  • 接続は線の上に実在しない。 両端のメモリに置かれた状態のペアを、私たちが「接続」と呼んでいる。3WHSはその状態を揃える手続き。
  • 「3ウェイ」は3往復ではない。 RFC 9293 自身が「three-way (or three message) handshake」と書いている。1往復 + 片道であり、データを送り始められるまでは1 RTT。データが届くまでは1 RTT + 片道、応答が返るまでは2 RTT。
  • 3つ目のACKにデータを相乗りできるのは、ACKがシーケンス番号を消費しないから(Figure 6 の4行目と5行目のSEQが同一)。
  • なぜ3回かについて、RFCは2つの説明を持っている。 §3.4.1 の「本来4手順だが、中間の2つが SYN,ACK として1パケットに畳まれる」(同節は続けて、古いSYNかどうかを受け手が見分けられないことも挙げる)と、§3.5 の「古い重複SYNによる混乱の防止」。RFCが "principal reason" と呼ぶのは後者で、ISN交換だけを答えると規格が最も重視する点が落ちる。
  • ISNのランダム化の現行規定は RFC 9293 §3.4.1。RFC 6528 は RFC 9293 に廃止されているので、現行規格として引くとそれ自体が誤りになる。
  • ISNのランダム化が守るのは off-path attacker による当て推量まで。経路上でパケットを見られる相手には効かない。 TCP自身に暗号機能は無い。
  • TCPの3WHSとTLSのハンドシェイクは目的が違う。 前者は信頼できる順序どおりの通り道を作り、後者はその上で認証と鍵共有を行う。RFC 9846 は下位トランスポートへの唯一の要求を「a reliable, in-order data stream」と書いている。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

「3ウェイハンドシェイクには3回の往復(3 RTT)が必要なので、接続確立は重い」という説明の評価として最も正確なものはどれですか。

用語集(6語)
3ウェイハンドシェイク (3WHS)
TCPの接続を確立する手続き。SYN / SYN,ACK / ACK の3つのメッセージを交わす。RFC 9293 §3.4.1 と §3.5 が扱う。§3.4.1 の原文に「three-way (or three message) handshake」とあるとおり、3は往復ではなくメッセージの数を指す。
ISN (初期シーケンス番号)
接続の開始時に各側が選ぶ32ビットの番号。以降のデータはこの番号からの相対位置で数えられる。RFC 9293 §3.4.1 が生成方法を規定しており、外部から計算できてはならない(MUST-9)。
SYN / ACK
TCPヘッダの制御ビット。SYN は synchronize の略で「シーケンス番号を同期したい」という意思表示、ACK は受け取った番号の確認応答。RFC 9293 の図では SYN,ACK のようにカンマで併記される。
SYN-SENT / SYN-RECEIVED / ESTABLISHED
接続確立の途中および完了を表すTCPの状態名(RFC 9293 の表記はハイフン付き)。この「状態」こそが接続の実体で、両端のメモリの中だけに存在する。
RTT (往復時間)
パケットを送ってから応答が返るまでの時間。3WHS でデータを送り始められるまでは1 RTT、データが相手に届くまでは1 RTT + 片道、その応答が返るまでは2 RTT。
TLSハンドシェイク
TLS が通信相手の認証・暗号パラメータの折衝・鍵の確立を行う手続き(RFC 9846 §4)。TCPの3WHSとは目的が異なり、TCPが作った信頼できるストリームの上で、その後に行われる。