Improving the reliability of the QUIC Handshake のメモ

Huitema氏が投稿していたImproving the reliability of the QUIC Handshakeという記事のメモです。

QUIC Interop Runner でのテストの中でも、しばしば失敗が検出されたものを紹介しています。

そのケースに対して、問題の起こり方、RFCに対してどのように反映されたのかが書かれています。

約30%のパケットロスがある環境で、50回の連続した接続を成功させるテストがあります。

おそらくinteropのREADME.md に書かれている以下のケースのことです。

Handshake Loss (multiconnect): Tests resilience of the handshake to high loss. The client is expected to establish multiple connections, sequential or in parallel, and use each connection to download a single file.

接続の成功までなので、Initialパケット、Handshakeパケットが落ちた場合について解説しています。

クライアントのInitialパケットやHandshakeパケットが落ちた場合、クライアントは再度送信をしています。

サーバーのInitialパケットが落ちた場合

サーバーのイニシャルパケットが落ちた場合、クライアントは自身が送信したパケットが落ちたのか、サーバーのイニシャルパケットが落ちたのかはわかりません。 理論上はサーバーからのHandshakeパケットを受信することはできますが、サーバーからのInitialパケットが落ちているため、クライアントは復号することができません。

なので、クライアントはInitialパケットを繰り返し送信します。

サーバーはそのパケットを受けた時にいくつかオプションがありますが、Initialパケットを再送するのが良い解決方法になります。

サーバーがInitialパケットへのACKを返してクライアントの送信を止めようとしても、接続の成功にはつながりません。 また、Handshakeパケットを送り続けても、クライアントは復号できるようにはなりません。

サーバーのHandshakeパケットが落ちた場合

サーバーのHandshakeパケットが落ちた時、特にクライアントのInitialパケットがACKされている場合に難しいことになります。

つまり、クライアントはACKされたパケットを再送しないので、「Initialパケットを送信することでサーバーのHandshakeパケットを誘発すること」ができない状態になります。

クライアントはイニシャルパケットをサーバーから受け取っているので、Handshakeパケットの送受信が可能になっています。 自然な選択肢は、サーバーから受け取ったInitialパケットへのACKを送ることですが、そのパケットがロスしたら交換全体が止まってしまいます。

それには以下のような理由があります。

一つ目はクライアントが1回しかInitialパケットへのACKを送る必要がない点です。 QUICの仕様によると、クライアントが送信するACKのみのパケットはサーバーからのACKを誘発しないため、そのルールに従うとクライアントは繰り返し送る必要はありません。したがって、このクライアントが送信するサーバーから受信したInitialパケットへのACKが落ちた場合クライアントはその再送は行いません。その結果、サーバー側でアドレスバリデーションが行われないため"anti-amplification" が解除されません。

その結果、サーバーがHandshakeパケットを再送し続けると、"anti-amplification" を使い果たしてしまうとそれ以上再送できない状態になります。

そうすると、クライアントもサーバーも何もパケットを送れないデッドロックのような状態に至ってしまいます。

このような状態が、相互接続テストで何度も起きていました。

クライアントは何らかのパケットを送信することでこの何も送信されない状態を回避する必要があります。

具体的には3つのオプションが考えられます。

  • Initialパケットを繰り返す
  • Handshakeパケットを繰り返す
  • InitialパケットとHandshakeパケットの両方を繰り返す

著者の好みの方法は、Handshakeパケットを繰り返し送信する方法です。Handshakeパケットを送信することで、クライアントはサーバーにInitialパケットを受信していることを伝えることができます。

サーバーからHandshakeパケットを受け取っていないので、このHandshakeパケットにはACKフレームを含めることができません。しかし、PINGフレームは含むことができます。

もしサーバーが賢ければ、ACKのみを含むHandshakeパケットを受け取った時点で再送できます。

そうじゃない場合は、追加のラウンドトリップが必要になります。

サーバーはクライアントからHandshakeパケットを受け取ることで、3-wayハンドシェイクが終わりanti-amplificationモードが解除されます。その結果、サーバーはクライアントが送ってきたHandshakeパケットへのACKをクライアントに送信します。

クライアントはそれを受けたら即座に、受信したHandshakeパケットへのACKを含むHandshakeパケットをサーバーに送ります。ここで、ACKフレームを含むことができるので、最初にサーバーが送ってきたHandshakeパケットを受け取れていないことをサーバーに伝えることができます。

このACKフレームを受け取ると、サーバーは最初のHandshakeパケットを送りなおします。これによってQUICのハンドシェイクを完了することができます。

f:id:neko--suki:20220411231135p:plain
Figure 4 – Breaking the deadlock を引用

ということで、この内容が実はRFC9000の8.1に書かれています。

“Loss of an Initial or Handshake packet from the server can cause a deadlock if the client does not send additional Initial or Handshake packets. A deadlock could occur when the server reaches its anti-amplification limit and the client has received acknowledgments for all the data it has sent. In this case, when the client has no reason to send additional packets, the server will be unable to send more data because it has not validated the client’s address. To prevent this deadlock, clients MUST send a packet on a Probe Timeout (PTO); see Section 6.2 of [QUIC-RECOVERY]. Specifically, the client MUST send an Initial packet in a UDP datagram that contains at least 1200 bytes if it does not have Handshake keys, and otherwise send a Handshake packet.”

このような短い段落は、ちらりと見るだけで見過ごされることが多いです。これを少し説明することで、相互接続テストの失敗が減ることを期待しましょう、と記事を締めくくっています。

短い記事ですが、詳細の理解のためにはRFCを確認しながら考える必要があり、勉強になりました。