QUICのAckとロスリカバリについて

QUICのAckとロスリカバリについて自分なりにまとめる。
(トランスポートは詳しくないのでご容赦ください)

あわせて、関連記事もどうぞ

とくに、ストリームやフレームに関しては下記記事を参照のこと
asnokaze.hatenablog.com

はじめに

QUICでは、UDP上で信頼性のあるデータ通信を提供するトランスポートプロトコルです。
(暗号化およびHTTP/3については、関連記事参照)

f:id:ASnoKaze:20190703231338p:plain

今回はdraft-20の主に下記仕様に基づいています

QUICでは内部的に仮想的な通信単位であるストリームを持ち、フレームと呼ばれるメッセージをやりとりします。TCPではパケットロスが発生すると、OSレイヤでパケットロスが回復されるまで後続のパケットを処理できません。しかし、QUICでは同一ストリーム内でのみデータの順番が保証されているため、パケットロスが発生してもデータのロスがなかったストリームは問題なく処理を進めることができます。

このように、UDP上にストリームという通信単位を持つというのは強力ですが、Ackやロスリカバリも多くの工夫がされています。

例えば、QUICではロスしたパケット自体そのものを再送することはなく、必要なデータのみを新しいパケットに格納して送信します。ロスしたパケットの中身のうち不必要なものは再送しませんし、時間が経過し状況が異なっているため新しいACKフレームを送ったり、はたまた優先度の高いアプリケーションデータを先に送るということもできます。(フレームタイプごとに再送処理が異なる)

また、QUICではパケット番号は常に1つずつ増えていきます。先述の通り、パケットロスした場合もそのままパケットを再送するのではなく、新しいパケットとして送信されます。

TCPでは、同一のシーケンス番号で再送を行うため、パケットを受け取った側は再送されたパケットなのかそうでないのかがわかりません。これは、RTTを測定する上での曖昧性となります。RTTをより正確に測定できればより適切なタイムアウト値を設定できるようになります。

その他にも、QUICのACKフレームではロスしたパケットをたくさん示すことができるようになっていたりします。詳しく見ていきましょう

パケットとアプリケーションデータ

TCPではシーケンス番号を使用していました。

QUICではシーケンス番号の持っていた役割をパケット番号とオフセットに分離しています。すべてのQUICパケットにパケット番号がついており明確に1つずつ増えていきます。パケット番号は明確に送信者がパケットを送信した順番を示します。

それとは別にアプリケーションデータを格納するSTREAMフレームにはデータのオフセット, 長さがあり、データ上の順番を特定することができます。

そのため、ロスしたアプリケーションデータを別のパケット番号で送信することができます。
f:id:ASnoKaze:20190704001213p:plain

Ack自体はパケット番号ごとに行うので、パケロスと判断されたパケットに含まれていたデータを送り直してやれば大丈夫です。また「パケットNが届いてないというAck」に対してAckを返してやれば、確かにロスしたことは伝わったから再送がされるだろうということになります。

ACKフレーム

QUICのAckはACKフレームを送信することで行われます。ACKフレームは受信したパケット番号レンジと、受信できていないパケット番号レンジを複数表現することができます。

ACKフレームは下記のような構造をしています。
f:id:ASnoKaze:20190704010950p:plain

  • Largest Acknowledged: ACKフレームを作成するまでに受信した最大のパケット番号
  • ACK Delay: Largest Acknowledgedで示されるパケットを受信してから、このパケットを送信するまでのディレイ(ms)
  • ACK Range Count:このACKフレーム内のGapフィールドとACKレンジの個数
  • First ACK Range: Largest Acknowledgedに続いて受信したパケットの数を示すACK Range(後述)
  • ACK Ranges: 0回以上のGapとACK Range(後述)

ACK Rangesの構造は下記の通りになります。下記のように複数のGapとACK Rangeのフィールドからなります。
f:id:ASnoKaze:20190704012648p:plain

  • Gap: 前のACKレンジに続いて、受信できていないパケットの数
  • ACK Range: 前のGapに続いて、受信したパケットの数

このように一つのACKフレームで、受信できたレンジと、受信できてないレンジを表現することができます。

f:id:ASnoKaze:20190704014232p:plain
例えば、パケット番号1 ~ 110, 120 ~ 130までのパケットを受信したことを相手に伝えるACKフレームは

  • Largest Acknowledged: 130
  • ACK Range Count: 1
  • First ACK Range: 10 (パケット番号120~129)
  • Gap: 8 (パケット番号111~119 から1引いたものを表記する)
  • ACK Range: 109 (パケット番号1~110 から1引いたものを表記する)

ACKフレームをいつ送信するかについて説明していきます。

  • ACKすべきパケット(ACKのみPADDNGのみ以外のパケット)を受信した際に、max_ack_delay(デフォルトで25ms)以内に送信すべきです。
  • 受信したパケット番号の最大値+1以外のパケットを受信した場合はすぐにACKフレームを送信すべきです。
  • ただし、ACKフレームを送信する前に複数のパケットを処理してもよいです(個々別にACKしないにする)

ロス検出

QUICでもAck情報とタイムアウトを使って、TCPのFast Retransmit [RFC 568]、Early Retransmit [RFC 582]、FACK [FACK]、SACK損失回復[RFC 6675]、およびRACK [RACK]に則りパケットロス検出を行います。

基本的には

  • 3パケット以上の順番の入れ替わり (kPacketThreshold == 3 は仕様で推奨されている)
  • 1パケット以上の順番の入れ替わりがあり、9/8 * max(SRTT, latest_RTT) 以上経過している

また、QUICでは、Ackが送られてこない場合のRTO(Retransmission Time Out)と、データ末尾の再送を促すTLP(Tail Loss Probe)のタイマーを一つにまとめたPTO(Probe Timeout)を持つ。PTOの計算式は下記のとおりである。

pto = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay

もしPTOタイマーが切れた場合、もし送信するデータが有ればそれを送信するか、ロスしたと思われるSTREAMのデータをプローブとして送信します。これにより、ACKフレームを送信させることができます。

連続したPTOタイマーは指数関数的増えていき、送信頻度は少なくなってきます。

今回書かなかったこと

結構奥が深い

  • crypto context毎にパケット番号空間が違うので、異なるcrypto contextに対してACKすることはできなくなっています
  • ハンドシェイク時に使用するCRYPTOはより積極的に再送されます
  • RTTの測定アルゴリズム (仕様を参照)
  • QUICはECN (Explicit Congestion Notification)をサポートしてます
  • 輻輳制御について。仕様上は、輻輳制御はTCP NewReno です。Cubic やその他のアルゴリズムを使用しても良いです。

処理中のPOSTリクエストを別のサーバで引き継ぐPartial POST Replayについて

なんらかの理由でWebサーバを停止する場合に、処理中のPOSTリクエストをそのまま別のサーバで引き継げるようにする「HTTP Partial POST Replay」という仕様がFacebookAlan Frindell氏から提出されています (HTTP Workshopの資料はこちら)。

スポットインスタンスを利用していたり、サーバの設定を変えて再起動したい場合、新しいリクエストは受け付けないようにし、すでに来ているリクエストのみ処理をするのは一般的です。それでも大きなファイルをアップロードしているPOSTリクエストは処理が終わるまで時間がかかってしまう場合がありあります。

やむをえずPOSTリクエストの処理を中断してしまうと、ユーザは再度大きなファイルをアップロードしなおす必要があり、とてもストレスがかかります。

HTTP Partial POST Replay」では、ユーザの接続を切ることなく別のサーバでPOSTリクエストを引き継ぐことができます。

Partial POST Replay

ひとことで「HTTP Partial POST Replay」の説明をすると、POSTリクエストを処理しているWebサーバがリバースプロキシまで処理中のリクエストを差し戻し、別のサーバで改めて続きを処理します。そのため、構成としてはPartial POST Replayに対応したリバースプロキシがいる前提となります。

クライアントとProxy間の接続は切断することなく、処理を引き継ぐことができます。

流れ

f:id:ASnoKaze:20190630185717p:plain
処理の引継ぎを行うWebサーバはPOSTリクエストに対して「3xx Partial POST Replay」(ステータスコードは未定)を返します。このHTTPレスポンスには、Webサーバが引き継ごうとしているHTTPリクエスト情報が格納されています。リクエストヘッダと疑似ヘッダは、echo-というプレフィクスをつけてレスポンスヘッダに格納されます。POSTしていたデータはレスポンスボディに格納されています。

このレスポンスを受け取ることで、Proxyは元のリクエストと現時点でPOSTされているデータを復元することができます。

f:id:ASnoKaze:20190630191627p:plain
Proxyは復元できたPOSTリクエストを別のWebサーバに割り振りなおします。このときクライアントとのコネクションは切断することなく、送信されてくるデータはProxyがバッファリングし続けています。

また、再度同じWebサーバに割り振られるのを防ぐために、partial-post-replayリクエストヘッダをつけることで、Partial POST Replayであることを明示します。

Cross-Origin-Embedder-Policyヘッダについて

ChromeがCross-Origin-Embedder-Policy (COEP)の実装に着手しそうなので、ざっと仕様を眺める(議論段階であり、正式な仕様ではありません)
mikewest.github.io

ちょっと話が難しいので間違ってたらすみません。

なお以前は、仕様名は「Cross-Origin-Resource-Policy-Policy(CORP-P)」などとも呼ばれていた。仕様自体の議論や、名称の変遷については下記Issueを参照のこと
Opting into a CORS-only mode (Cross-Origin) · Issue #4175 · whatwg/html · GitHub

Cross-Origin-Embedder-Policy

Spectre攻撃以後、サイドチャネル攻撃を用いてメモリの内容を推測できる場合、Same-Originポリシーは十分ではないことがわかっています。つまり、ブラウザにクロスオリジンのリソースを読み込ませることに成功すれば、内容を推測できる可能性が出てきたわけです。

Cross-Origin-Embedder-Policyヘッダでは、埋め込むリソースがCross-Origin-Resource-Policyを明示するように強制します。また、そのためにCross-Origin-Resource-Policy: same-site (仕様上だとcross-originという表記もある)を指定できるように拡張します。

CORP自体は以前書いたとおりです
asnokaze.hatenablog.com


このドキュメントでは変更に合わせ、HTMLやFetchの仕様に対してモンキーパッチを当てています。

流れ

ブラウザは、example.comからhtmlファイルを読み込みます。このとき、レスポンスにCross-Origin-Embedder-Policy:require-corpヘッダがついています。これによって、サブリソースは Cross-Origin-Resource-Policyが必須になります。

サブリソースのレスポンスにCross-Origin-Resource-Policy:cross-siteとクロスオリジンでの読み込みを明示すると、ブラウザはそのリソースを読み込めるようになります。

f:id:ASnoKaze:20190623013333p:plain

安全なコンテンツを要求するPrefer:safeヘッダ (RFC8674)

『追記』RFC 8674になりました
https://www.rfc-editor.org/rfc/rfc8674.html


Webサービスによっては、子供に見せたくない有害なコンテンツを非表示にできるサービスもあります。

それの多くはcookieを使い、設定を保存するケースが多く個別に設定を有効にしていく必要があります。また、広告といった設定がし辛い部分もあります。

こういった要求をブラウザの設定でできるように、「The "safe" HTTP Preference」という提案がMark Nottingham氏から出されています。

この提案仕様は、「rfc7240 Prefer Header for HTTP」で定義されている、preferヘッダPreference-Appliedヘッダを使用します。

Prefer: safe

この仕様では、以下のように「Prefer: safe」をヘッダに指定することで安全なコンテンツを要求していることを示します。

   GET /foo.html HTTP/1.1
   Host: www.example.org
   User-Agent: ExampleBrowser/1.0
   Prefer: safe

もちろん、サーバが対応しているか、その要求を尊重するかは実装次第です。サーバはPreference-Appliedヘッダを使うことで、その要求を適応したことを示せます。

   HTTP/1.1 200 OK
   Transfer-Encoding: chunked
   Content-Type: text/html
   Preference-Applied: safe
   Server: ExampleServer/2.0
   Vary: Prefer

標準化

もちろん、安全なコンテンツを明確に仕様化することは難しいです。この提案仕様でもその曖昧性のためIESGからの承認が得られず標準とはなっていないと述べられています。

しかし、すでに実装があり有用性の観点からドキュメントとして提出されています。

サービスやリソースの廃止時間を示すSunset HTTP ヘッダ (RFC8594)

Webサービス、古いバージョンのWeb API、期限付きのデータなど、そのURLで提供しているリソースが将来的に廃止するということはよくあることです。

RFC 8594 「The Sunset HTTP Header Field」では、そのようなリソースの廃止時間をHTTPレスポンスヘッダで示せるようになります。

ただしこれはヒントに過ぎず、示した時間までの提供を保証するものではありません。

2016年から、10年間のみ保存されるデータなどには、下記のようなレスポンスヘッダをつけることで、2026年にリソースが取得できなくなることを示すことができます。
(フォーマットは、RFC7231 の通り 「Date/Time Formats」)

Sunset: Wed, 11 Nov 2026 11:11:11 GMT

廃止に関する追加の情報は、Linkヘッダで示すことができます。relation typeとして sunsetを指定します。

Link: <http://example.net/sunset>;rel="sunset";type="text/html"

このポリシーの中では、廃止の範囲(リクエストを受けたURLだけなのか、それとももっと広い範囲なのか)、廃止後の移行先などを書けます。

Cookie の SameSite=Lax をデフォルトにする提案仕様

20191226 追記
SameSite属性のついたCookie自体を拒否する古いクライアントにご注意ください
https://sites.google.com/a/chromium.org/dev/updates/same-site/incompatible-clients

20190823 追記
suidenOTI さまよりご指摘いただきました

不具合があったためChrome80でのリリースに延期になったようです。
https://www.chromestatus.com/feature/5088147346030592
https://www.chromestatus.com/feature/5633521622188032

20190523 追記
Firefoxでも同様の動きがあります
https://groups.google.com/forum/#!msg/mozilla.dev.platform/nx2uP0CzA9k/BNVPWDHsAQAJ


開催中のGoogle I/O で、SameSite属性のないCookieをSameSite=Laxとして扱うようにしていくという話があったようです
blog.chromium.org

SameSite=Laxになると、img, iframeやxhrなど送信される他サイトへのHTTPリクエストにおいてthird party cookieがつかなくなります。これによって、cookieの露出を控える事ができます。(SameSite = None とすることで引き続きトラッキングは可能)

Google調査によると SameSite属性のついたCookieはまだ0.1%以下であり、まだまだ普及できておらず、今回デフォルトでSameSite=Laxとする動きになったのだと思います。

この挙動は、すでにChrome Canaryでchrome://flagsより設定することができます。
f:id:ASnoKaze:20190509004402p:plain

提案仕様

また、上記のアナウンスに続いて、IETF側でもGoogleのMike West氏より「Incrementally Better Cookies」という提案仕様が出ております。

同氏がメーリングリスト投げた「Incremental improvements to cookies.」でも書かれている通り、Cookieを段階的に改善しようとしており、この仕様では

  • 1. デフォルトで、Cookieを「SameSite = Lax」として扱う
  • 2. 開発者が明示的に `SameSite = None`を設定することにより現状の振る舞いのようにできるが、そうするときは` Secure`属性が必要

にするという提案である。

同氏の関連活動

Mike West氏は、Cookieに関わる多くの改善を提案している

SameSite属性の仕様自体は「Same-Site Cookies」で書かれているが、下記の記事の通り、RFC6265bisに統合される流れである
asnokaze.hatenablog.com

また、新しい仕組みも検討中であったが、フィードバックをうけCookieを段階的に改善しようという流れのようだ
asnokaze.hatenablog.com

Cross-Origin-Opener-Policyについて

Cross-Origin-Opener-Policy (COOP)は現在、ChromeFirefoxで実装が進められている機能です。

仕様としては、whatwgで長らく議論がされており、おそらく仕様に入るでしょう

面白そうなので、簡単に読んで見る。今の所下記ドキュメントが定義のようだが、適宜議論を参照のこと

間違ってたらご指摘ください

Cross-Origin-Opener-Policy とは

ユーザがサイトAを閲覧しているとき

サイトA から サイトBをウィンドウとして開いた場合 (noopnerはつけてない)、Bはwindow.openerを介してAにアクセスすることができます(仮にAとBのオリジンが違っていても、制限はありますがAのプロパティにアクセスできます。)
f:id:ASnoKaze:20190508015638p:plain

このようなアクセスは、サイトのアイソレーション上好ましくありません。このようなことを防ぐために、Cross-Origin-Opener-Policyヘッダを利用します。もし、指定されたPolicyに合わない場合は、上記のような繋がりは解除されます(ウィンドウを閉じて開き直したのと同じ状態)

Cross-Origin-Opener-Policyは下記のような値を取ります。

Cross-Origin-Opener-Policy = same-origin
Cross-Origin-Opener-Policy = same-site
Cross-Origin-Opener-Policy = same-origin unsafe-allow-outgoing

A及び、Bそれぞれへのアクセスした際はポリシーに合わず、openerがnullを返すようになります

  • Aもしくは、BのどちらかのみにレスポンスヘッダでCross-Origin-Opener-Policyが設定される
  • AとBのCross-Origin-Opener-Policyのsameness (same-origin or same-site)が異なる
  • 値がsame-originだが、AとBのオリジンが異なる
  • 値がsame-siteだが、AとBのホスト名が異なる

こうすることで、noopenerを指定できる開く側だけでなく、開かれる側からもopenerのつながりを解除することができるようになります。(unsafe-allow-outgoingを指定すると開く側のときだけ許可する)

おまけ

議論の変遷のなかで、openerのポリシーへとヘッダ名とともに変遷しており、議論を追うのが大変だった...