クエリパラメータ付きURLのキャッシュを改善する No-Vary-Search ヘッダ

キャッシュのキーとしてURLが使われます。このときURLのクエリパラメータも含めて考慮されます。

特定のクエリパラメータは、分析や別様とでアクセスログに残すために使われたりしますが、提供するリソースが変わらない場合があります。そのケースであれば、クエリパラメータが付いてたとしても、キャッシュがあればキャッシュを使ってもらいたいものです (ここでキャッシュは、HTTPキャッシュ及び、prefetchやprerenderを指します)。

そのため、クエリパラメータが付いている場合の扱いを改善する No-Vary-Search レスポンスヘッダ が、Chromeの方中心に議論されています。

github.com

No-Vary-Search レスポンスヘッダの例として次のものが上げられている

クエリパラメータのキーの順序を考慮しない

No-Vary-Search: key-order

特定のクエリパラメータをキャッシュのキーとして考慮しない (特に分析用のものなど)

No-Vary-Search: "utm_source", "utm_medium", "utm_campaign"

特定のクエリパラメータ意外をキャッシュのキーとして考慮しない

No-Vary-Search: *; except=("productId")

HTTPのProxyを改善する「Upgrade: connect-tcp」 の提案仕様

Modernizing HTTP Forward Proxy Functionality」という仕様がIETFに提出されています。

これは、CONNECTメソッドを使った今までのForward Proxyの機能を、新しい(モダン)なしくみに置き換える提案です。HTTP/2やHTTP/3上でHTTP以外のデータをやり取りする場合は、拡張CONNECTの仕組みを使うのが一般的です。例えば、HTTPを介してUDPパケットをProxyするCONNECT-UDPなどもこの拡張CONNECTを使っています。

この拡張CONNECTのアイディアをもとに、今までのCONNECTにバックポートするというのが今回の「Modernizing HTTP Forward Proxy Functionality」になります (参考: MLでの著者のメール)

拡張CONNECT

拡張CONNECTの仕組みは以前説明したとおりです
asnokaze.hatenablog.com

今までのCONNECTメソッド問題点

今までのCONNECTメソッドの使い方は次のとおりです

クライアントはProxyに対してCONNECTメソッドを行い、TCP通信のデータをTargetにForwardしてもらいます

CONNECT server.example.com:80 HTTP/1.1
Host: server.example.com

この方法では

  • Proxyサーバは単一のエンドポイントしか提供できない。VirtualHostや、Pathを使ってサービスを分ける事ができない。
  • クライアントはIPアドレスでTargetを指定する際に、IPv4IPv6どちらで指定すれば良いかわからない

Modernizing HTTP Forward Proxy Functionality

本提案では、拡張CONNECT同様、ProxyのエンドポイントとしてPathを指定できるようになっています。
また、targetとしてIPv4IPv6を併記し、Proxyが選択できるようになっています。

HTTP/1.1

HTTP/1.1ではProxyに対して次のリクエストを送って、targetにデータをforwardしてもらいます

GET /proxy?target_host=192.0.2.1,2001:db8::1&tcp_port=443 HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: connect-tcp
  • GETのクエリパラメータとしてtarget_host, tcp_port を指定する
  • ConnectionヘッダにUpgrade を指定する
  • Upgradeヘッダに connect-tcp を指定する
HTTP/2, HTTP/3の場合

HTTP/1.1では拡張CONNECTと同様にリクエストします

CONNECT HTTP/2.0
:authority = request-proxy.example
:scheme = https
:path = /proxy?target_host=192.0.2.1,2001:db8::1&port=443
:protocol = connect-tcp
  • CONNECTで、クエリパラメータとしてtarget_host, tcp_port を指定する
  • protocol疑似ヘッダで connect-tcp を指定する

Webサイトを離れたときにデータを送る Page Unload Beacon (Pending Beacon API)

Webサイトを離れたときにサーバにデータを送れるようにする「Page Unload Beacon」という仕組みが、W3CのWICGで議論されています。

既存のページのライフサイクル(unloadイベントやbeforeunload)で、サーバにデータを送ろうとしても処理されないことがあります。そのため、ページのunload時にビーコンを送るように登録できるようにするのが「Page Unload Beacon」です。

最新のChrome Canaryでとりあえず動くっぽいので、触ってみる (まだ動作するだけで、一部仕様と異なります)

Page Unload Beacon

デベロッパーツールから次の通り実行して、Beaconを登録しておきます。今回はGETリクエストとPOSTリクエストのビーコンをそれぞれ登録。

getbeacon = new PendingGetBeacon("http://example.com/?get");
postbeacon = new PendingPostBeacon("http://example.com/?post");
postbeacon.setData("post data")

その後、Webページを閉じます。

WiresharkでHTTPリクエストを引っ掛けると、次の通りリクエストが飛んでる事ができます

おまけ

  • 通常のBeacon APIと同じようにCookieは付くようです (まだ付かない?)
  • 通常のBeacon APIと同じであれば、「Private Network Access」の制約を受けそう (まだ制約されない?)
  • ネットワークが切断されて切り替わった場合は、ビーコンは送られないぽい (未確認)

ここらへんは実装が進む中で変わりそう

DNS名前解決エラーもネガティブキャッシュする提案

Negative Caching of DNS Resolution Failures」という提案が、Verisignの方らによって提案されています。

DNSの名前解決の結果はつぎのいずれかです。

  • 1) 要求されたデータを含む応答
  • 2) 要求されたデータが存在しないことを示す応答
  • 3) ネットワークエラーや、データ不整合などの、有用な情報が得られない(失敗)

今回の提案では、(3)のエラーについても最低5秒間ネガティブキャッシュするよう要求します(5分以上キャッシュしてはいけない)。

RFC2308 「Negative Caching of DNS Queries」では、サーバが落ちていたり接続できない場合に、オプショナルでキャッシュする事が記述されてはいます。

モチベーション

提案仕様のなかで、DNSのエラーが起こり、再帰サーバのトラフィックが増大したことを説明しています。幾つかを紹介すると

  • 2018年のDNSSECのKSK Rolloverにおいて、古いトラストアンカーを使用しDNSKEYをリトライする再起リゾルバーがいて、トラフィックが増大した (参考)
  • 2021年、Verisignの研究により、REFUSEDやSERVFAILを返す権威サーバのクエリトラフィックが多いということを示した
  • 2021年 10月、Facebookの障害で権威サーバにアクセスできなくなったとき、再帰サーバでタイムアウトが発生した。.comや.netへのクエリが、7000 query/sec から 900,000 query/secに増大した (参考)

失敗の種類

この提案仕様では、失敗の種類として、以下のものについて言及しています

  • SERVFAILの応答
  • REFUSEDの応答
  • 要求のタイムアウト
  • 委譲のループ (NSレコードの指定先がループしている)
  • エイリアスのループ (CNAMEレコードの指定先がループしている)
  • DNSSECの検証失敗

Twitchの動画配信プロトコルWarpのデモを動かしてみる

TwitchはWebTransport over HTTP/3 で単方向のライブストリーミングを可能にするWarpというプロトコル開発してます。
古い記事ですが、以前紹介した通りで、現在IETFで議論が進められています。
asnokaze.hatenablog.com

その Warpのデモコードが公開されたので動かすところまでやる。
github.com

とはいえ書いてある通りやるだけ。僕は、サーバとブラウザを別環境で動かす都合でちょっと弄っている。先に結果だけ書くと

結果

ブラウザで動画が流れる事を確認しました

また、serverを改造してkeylogfileを出力させ、パケットを復号させると

  • SETTINGSフレームで次の値を投げている
    • SETTINGS_WEBTRANS_DRAFT00 = 0x2b603742,
    • SETTINGS_H3_DATAGRAM_DRAFT04 = 0xffd277,
  • サーバからの単方向ストリーム (WebTransport stream=0x54)でデータを転送していることがわかります
    • データの最初にwarp用のjsonが格納されてます

( パケロス時に再送不要なDATAGRAMフレームも仕様上使えるが、まだ使ってなさそう

以下は手順

前準備

今回はUbuntuで動かしたが、ffmpegやnodejsはaptで降ってくるバージョンだと動かないので適宜新しいものを入れる (ffmpegはsnapでいれて、nodejsはv16を入れた)

コードの取得

$ git clone https://github.com/kixelated/warp-demo.git
$ cd ./warp-demo

流す動画ファイルの取得、変換

$ wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O media/combined.mp4
$ ffmpeg -i media/combined.mp4 -f dash -use_timeline 0 -r:v 24 -g:v 48 -keyint_min:v 48 -sc_threshold:v 0 -tune zerolatency -streaming 1 -ldash 1 -seg_duration 2 -frag_duration 0.01 -frag_type duration media/fragmented.mpd

証明書の準備

$ ./cert/generate
$ ./cert/fingerprint
sB8TJSHn195H3GJm/6WoibD/OGN5M+w4x7a7RpirLo4=

フィンガープリントは後で使う

/ect/hostsの設定

echo '127.0.0.1 localhost.warp.demo' | sudo tee -a /etc/hosts

サーバの起動

Webサーバ、https://localhost.warp.demo:4444/ で動作する。アクセスすると動画プレーヤーが表示される
( 127.0.0.1でLISTENするので、外からアクセスする場合は ./player/package.json で起動を 0.0.0.0 に指定すれば良い

$ cd player
$ yarn install
$ yarn serve

warpサーバ、https://localhost.warp.demo:4443/ で動作する

$ cd server
$ go run ./warp-server

Chromeでアクセスする

先述の通り、warpサーバを起動したとは別のPCからアクセスするので
localhost.warp.demo を該当サーバのIPに向ける必要がある。/etc/hostsで指定してやれば良い

なお、現状はChrome Canaryを使う必要がある。
で、Chromeを起動時に次のオプションを渡す (起動コマンドは chrome://version で確認できる)

--origin-to-force-quic-on="localhost.warp.demo:4443" --ignore-certificate-errors-spki-list="sB8TJSHn195H3GJm/6WoibD/OGN5M+w4x7a7RpirLo4=" https://localhost.warp.demo:4444

https://localhost.warp.demo:4444/にアクセスする

HTTPのやり取りの最中に付帯情報を送れるMETADATAフレーム

METADATA frame for HTTP/2 and HTTP/3」という提案仕様がIETFに提出されている。

これは、HTTPリクエストやレスポンスをやるとりする際に、HTTPメッセージ(ヘッダやコンテンツ)とは別に追加情報を送れるようにします。追加の情報を送るのに、HTTP/2及びHTTP/3の拡張フレームとしてMETADATAフレームを定義します。METADATAフレームはストリーム上で任意のタイミングで送れるため、HTTPリクエストを送受信してる途中, HTTPレスポンスを送受信してる途中に何度も送信できます。

追加の情報の例としては、「CPUコスト」「負荷分散情報」「統計情報」などを送ることが述べられています。他にも、デバッグ情報など送るのに良さそうだなと思います。これらはHTTPセマンティクスではないので、ヘッダとして解釈されるとかはないです。

この機能はenvoyに組み込まれているしくみで、他にもGoogle QUCHEなどすでに実装しています。(具体的にどう使うのか知ってる方が居たら教えてください...)
github.com

HTTP/2の場合

SETTINGSパラーメタのSETTINGS_ENABLE_METADATAを有効にした場合に使えます。

HTTP/2ではメタデータを付加したいHTTPメッセージのストリームIDを指定します。また、メタデータはキーバリュー形式のデータであり、HPACKのフォーマットで格納します。


HTTP/3の場合

SETTINGSパラーメタのSETTINGS_ENABLE_METADATAを有効にした場合に使えます。

HTTP/3ではメタデータを付加したいHTTPメッセージと同じQUICストリーム上で送信します。また、メタデータはキーバリュー形式のデータであり、QPACKのフォーマットで格納します。

HTTP/3のCONNECT-UDPを利用したWebRTC通信

Proxying Listener UDP in HTTP」という提案仕様がIETFに提出されている。

これは、HTTP/3のCONNECT-UDPを介してWebRTC通信を可能にするための提案である。まだ議論の呼び水と鳴るdraftであるため、ここから仕様は大きく変わると思うが、ざっと眺めていく。

HTTP/3のCONNECT-UDP

本論に入る前に、まずCONNECT-UDPについて説明します。

IETFではすでに「Proxying UDP in HTTP」という仕様が議論されている。これが通称CONNECT-UDPと呼ばれているものである。実は、AppleのPrivate Relayでもすでに使用されているものである。

これは、Proxyと確立したHTTP/3コネクションをトンネリングしてUDPパケットを中継させる機能です。

この通信は第三者からはただのHTTP/3通信としてか観測できないという特徴があります。確立した通信路を通して自由なUDPパケットを中継させることが出来ます。

このCONNECT-UDPでは、H3-Datagramという仕様でUDPパケットを扱います。詳しくは以前書いた記事を参照ください。
asnokaze.hatenablog.com

Proxying Listener UDP in HTTP

通常のCONNECT-UDPではProxyを中継して通信する相手は1つでしたが、WebRTCはSTUNと通信した上で、通信相手のブラウザからもパケットを受け付ける必要があります。そこで、既存のCONNECT-UDP拡張機能として「Proxying Listener UDP in HTTP」が登場しました。

(提案者はユースケースとしてWebRTCをあげていますが、それに限定されるものでは有りません)

主な通信の流れ

上記の図に沿って通信の流れを説明します。

CONNECTリクエストとレスポンス

まずは、ブラウザAと中継サーバとでHTTP/3のコネクションを確立したのち、中継の要望を出す。

通常のCONNECT-UDPのように、拡張CONNECTメソッドを用いる(:protocolにconnect-udpが指定される)。

connect-udp-listenヘッダを付けることで、Proxying Listener UDP in HTTP の通信であることを明示する。ヘッダの値は使用するH3-DatagramのContext-IDを指定する。

HEADERS
  :method = CONNECT
  :protocol = connect-udp
  :scheme = https
  :path = /.well-known/masque/udp/*/*/
  :authority = proxy.example.org
  connect-udp-listen = 2
  capsule-protocol = ?1
|<<

中継サーバは、200番のレスポンスを返し、CONNECTリクエストの受け入れる意志を示す。

>||
HEADERS
  :status = 200
  capsule-protocol = ?1
STUNサーバへのUDPパケットの中継

ブラウザA側は、H3-DatagramUDPパケットを中継サーバに送信する。このとき、中継先となるターゲットのIPとポートを付加して送る。

 DATAGRAM
   Quarter Stream ID = 11
   Context ID = 2
   IP Version = 4
   IP Address = 192.0.2.42
   UDP Port = 1234
   UDP Payload = Encapsulated UDP Payload

中継サーバは、指定されたIP宛にUDPパケットを中継する。

中継サーバはSTUNサーバから応答となるUDPパケットを受け取ると、ブラウザA側に中継する。このときもH3-DatagramでUDPパケットを中継することになる。

このときのH3-Datagramでは、どこからきたUDPパケットなのかが付加されている。

DATAGRAM
  Quarter Stream ID = 11
  Context ID = 2
  IP Version = 4
  IP Address = 192.0.2.42
  UDP Port = 1234
  UDP Payload = Encapsulated UDP Payload

ブラウザBからのUDPパケットの中継

STUNとのやり取りが終わって、ブラウザAとブラウザBがやりとりを開始します。ブラウザBから送られるUDPパケットも、中継サーバがブラウザAに中継を行います。このときもH3-DatagramでUDPパケットを中継することになる。

DATAGRAM
  Quarter Stream ID = 11
  Context ID = 2
  IP Version = 4
  IP Address = 203.0.113.33
  UDP Port = 4321
  UDP Payload = Encapsulated UDP Payload