TCPのRSTパケットに拒否理由をつける提案仕様

TCPの接続拒否理由を通知する「TCP RST Diagnostic Paylaod」という提案仕様がIETFに提出されています。

TCPでは様々な理由により接続拒否を示すRSTパケットが送信されます。NATやファイアウォール、リッスンされてないポートへの接続時などです。

しかしRSTパケットの受信者は、RSTパケットが返された理由を知る手段はありませんでした。それを可能にするのが「TCP RST Diagnostic Paylaod」です。

TCP RST Diagnostic Paylaod

TCP RST Diagnostic Paylaod」ではRSTパケットに接続拒否理由の格納方法を指定します

もともと、RSTパケットにデータを含めることができます。TCPの仕様の改訂版である「rfc793bis」でもSHOULDで指定されています。

TCP implementations SHOULD allow a received RST segment to include data (SHLD-2)

そこに次のような情報をCBOR形式で情報を格納します。例えば次のとおりです (例はCDDLで記述します)

[
  12345,
  {
    "reason-code": 2, 
    "reason-description": "brief human-readable description"
  }
]
  • 12345: この仕様のデータが含まれていることを示すマジックナンバー (後で正式に決定)
  • reason-code: 接続拒否理由を示す番号
  • reason-description: 人間が読める理由を示す文字列
reason-code

reason-codeとして次のものが定義されています

  • 1: クローズ後にデータを受信した
  • 2: リッスン状態なのにACKを受信した
  • 3: 不正なメッセージ
  • 4: 許可されていない
  • 5: リソースの超過
  • 6: ネットワークの障害 (中継装置などで利用)
  • 7: ピアによってリセットを受信した (中継装置などで利用)
  • 8: ピアへの到達性がない (中継装置などで利用)
  • 9: コネクションタイムアウト (中継装置などで利用)
Diagnostic Payloadの扱い

このデータはアプリケーションに渡すことが許可されています。デバックに使用することができます。

Memcachedのproxy機能を触ってみる

Memcashedでは、1月にリリースされた version 1.6.13から実験的にProxy機能が搭載されています。

公式のドキュメントはコチラ
github.com

概要

主な通信の流れは次の通りです
f:id:ASnoKaze:20220404003718p:plain
(通信内容は簡易的に記載するが、実際は通常のmemcacheプロトコルです)

  • memcachedをproxyモードで起動する際、グループ毎にサーバプールを指定する (例: foo, bar)
  • Clinetから値をsetする際は、keyにサーバプールを示すプレフィックスを付ける (例: /foo/, /bar/)
  • Proxyは、サーバプールを示すプレフィックスを取り除いたものを、バックエンドのmemcachedサーバにsetする
    • サーバプールに複数サーバがいる場合は、Keyのハッシュ計算に基づいて振り分け先が決まる (例: /foo/key1, /foo/key2)。

上記はsetの例だが、getも同様に動作する

サーバプールの指定

サーバプールの指定は、luaスクリプトで記述する。
サーバプール毎にnameを指定し、バックエンドとなるmemcachedサーバを指定するだけです。

local s = require("lib/simple")

-- by default, sends "/foo/*" to "foo" and "/bar/*" to "bar"
s.pool{
    name = "foo",
    backends = {"127.0.0.1:11212", "127.0.0.1:11213"},
}

s.pool{
    name = "bar",
    backends = {"127.0.0.1:11214", "127.0.0.1:11215"},
}

起動時に proxy_configを読み込む

./memcached -o proxy_config=./example.lua
stats proxy

proxyモードのmemcachedは "stats proxy" コマンドで状態を統計情報を取得できる

$ echo "stats proxy" |nc  localhost 11211
STAT user_simple_failovers 0
STAT cmd_mg 0
STAT cmd_ms 0
STAT cmd_md 0
STAT cmd_mn 0
STAT cmd_ma 0
STAT cmd_me 0
STAT cmd_get 1
STAT cmd_gat 0
STAT cmd_set 3
(略)
END

ビルド

現状この機能を使うには、自分でビルドすることになる。手順は公式ドキュメントの通りです
github.com

終わりに

まだ実装は実験段階だが面白い機能です。
luaスクリプト弄ったらもっといろいろ出来るかもしれないので触ってみる。単純にrepolicationとか出来ると嬉しいかも。

(最近はほぼマネージドサービス使っちゃうので、10年くらい早く欲しかった...)

Sec-CH-IP-Geoヘッダの仕様

The IP Geolocation HTTP Client Hint」という提案仕様が、AppleGoogleの方の共著でIETFに提出されています。

簡単に見ます。

背景

ユーザの位置情報はプライバシーの課題がありつつも、VPNサービスなどによりIPジオロケーションが取れないと困ることもありそうです。権利などの問題で配信できる国が限定されているといったこともあるかと思います。

そのために、例えばPrivate Relayでは IPジオロケーションを公開しています(URL)。

サーバとしても、VPNサービスのIPジオロケーションリストを網羅するのは難しいように思われます。そこで、クライアントからIPジオロケーション情報を通知できるようになると便利そうです。

The IP Geolocation HTTP Client Hint

この仕様では、クライアントの位置情報をHTTP Client Hintの仕組みでサーバに送信します。

サーバは、Accept-CHレスポンスヘッダでSec-CH-IP-Geoを受け入れる意思を伝えます。

    Accept-CH: Sec-CH-IP-Geo

クライアントは、Sec-CH-IP-Geoリクエストヘッダで位置情報をサーバに通知します。

    Sec-CH-IP-Geo = "US,US-AL,Alabaster"

Sec-CH-IP-Geoヘッダの位置情報表記は 「RFC 8805 A Format for Self-Published IP Geolocation Feeds」の仕様の通りになります。

おまけ

クライアントにジオロケーション情報を送ってもらう方式として、もともとAppleの方はGeoHashを用いる提案をしていました。議論のすえ、今回のような仕様に変更されました。

仕様の共同著者らがその点についてgithubで議論してたりもします。参考までに。
github.com

HTTP DatagramsとCapsuleプロトコルについて

HTTP Datagrams and the Capsule Protocol」という提案仕様が出されている。長く議論されている仕様ではあるが、動きがあったので眺めておく

背景

トランスポートプロトコルであるQUICでは、パケットロスがあったとしてもアプリケーションプロトコルのデータは再送してピアに届けられます。

「RFC9221 An Unreliable Datagram Extension to QUIC」という仕様で定義される、QUIC DATAGRAMフレーム拡張では、信頼性のないアプリケーションデータの送受信が可能になります。DATAGRAMフレームのデータはパケットロスで失われても再送する必要はありません。

DATAGRAMフレームは受信したものから処理することができます、アプリケーションの要件によりデータの再送が必要な場合はアプリケーションレイヤで再送信を行うこともできます。

f:id:ASnoKaze:20220331001525p:plain

WebTransportやMASQUE(CONNECT-UDP)といった仕様では、このQUIC DATAGRAM拡張をHTTPで利用します。ただし、普通のQUIC DATAGRAMフレームはストリーム上で送信されません。HTTPでDATAGRAMを送受信するには、HTTPメッセージと関連付ける必要があります。それを定義するのが「HTTP Datagrams and the Capsule Protocol」です。

もともとHTTP/3を前提にしていましたが、途中にProxyが居るケースなどを想定しHTTP/1.1やHTTP/2も想定したCapsuleプロトコルとともに標準化が進められています。

なお、WebTransportやMASQUE(CONNECT-UDP)については過去に書いた記事を御覧ください

(HTTP DatagramはWebTransportなどで利用されます。通常のHTTPリクエスト・レスポンスで使う想定は今のところありません)

HTTP DatagramsとCapsuleプロトコル

HTTP/3, HTTP/2, HTTP/1の場合でそれぞれ、データの送り方が違います。

  • HTTP/3では、QUIC DATAGRAMフレームを利用します
  • HTTP/2では、拡張CONNECTでトンネルを確立し、CAPSULEプロトコルを使ってデータを送信します。(信頼性のない通信はできません)
  • HTTP/1では、HTTP Upgradeの手順でトンネルを確立し、CAPSULEプロトコルを使ってデータを送信します。(信頼性のない通信はできません)

常にHTTP/3(QUIC)が使えるとは限りません、QUICがブロックされている環境では、HTTP/2やHTTP/1.1でDatagramメッセージを送れる必要があります。このときCAPSULEプロトコルという仕組みでは、TCPなHTTPでDatagramメッセージをやりとりできるようになります。

HTTP/3

HTTP/3では、QUIC DATAGRAMフレームに以下のデータを格納します
f:id:ASnoKaze:20220331002111p:plain

  • Quarter Stream IDは、このDatagramが関連付けられれたHTTPリクエストのストリームIDが格納されます (クライアントが開始したストリームが前提なので4で割った数値で十分)
  • HTTP Datagram Payloadは、アプリケーションのデータです

ご覧の通り、以前まであった、一つのストリームに複数のDatagaram フローを関連付ける仕組みはなくなりました (CONNECT-UDPといったより上位のプロトコルで定義してたりします)。

なおこの拡張を利用するには、H3_DATAGRAM SETTINGSパラメータが有効になっている必要があります。

HTTP/1.1とHTTP/2におけるコネクションの確率

DatagramメッセージをTCPなHTTPプロトコルで送信できるようにします。

HTTP/1.1の場合は Upgradeの手順をもって通信を一旦確立します。その後、そのコネクション上でデータをやり取りします
```
GET https://proxy.example.org/.well-known/masque/udp/192.0.2.42/443/ HTTP/1.1
Host: proxy.example.org
Connection: upgrade
Upgrade: connect-udp
```

HTTP/2の場合は拡張CONNECTの手順をもって通信を一旦確立します。その後、そのコネクション上でデータをやり取りします。
```
HEADERS
:method = CONNECT
:protocol = connect-udp
:scheme = https
:path = /.well-known/masque/udp/192.0.2.42/443/
:authority = proxy.example.org
```

やりとりするデータはCAPSULEプロトコルに則ります

CAPSULEプロトコル

CAPSULEプロトコルでは確立された通信路で、次の形式でメッセージを送受信します。ここに、Datagramデータが入ることになります。
f:id:ASnoKaze:20220331003315p:plain

(なお、CAPSULEプロトコルはType, Length, Valueの形式をしており、独自のTypeを定義し任意のメッセージをカプセル化できます)

おまけ、あるいは動向

HTTP Datagrams and the Capsule Protocol」ではあくまでHTTP DatagramsとCapsuleプロトコルを定義しているだけで、上位のプロトコルがどのように使うかは定義されていません。

WebTransportやCONNECT-UDPの仕様を見るとよりイメージが湧くかと思います。

先日行われたIETF113では、WebTransport over HTTP/2でCAPSULEプロトコルを利用するのか議論がありましたが結論はでてません。まだまだ動きがありそうです。

HTTP/3でQUICv2を使うためのalt-svc拡張

An Alt-Svc Parameter for QUIC Versions」という提案仕様が提出されています。

仕様の内容自体は簡単だが、背景から軽く説明していきます。

背景: HTTP/3とalt-svcヘッダ

サーバがHTTP/3に対応しているかクライアントに通知する方法がalt-svcです。

HTTP/2の頃はTLSハンドシェイク中にHTTP/1.1を使うかHTTP/2を使うかネゴシエーションしていました。HTTP/3ではそもそもQUIC(UDP)で通信がはじまりますので、その前にサーバHTTP/3に対応しているか知る必要があります。

クライアントは一度HTTP/2で接続し、サーバ側からこのようなalt-svcヘッダを返すことでHTTP/3に対応している事を通知します。

alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

なお最近ではDNS HTTPS レコードを用いて、サーバのHTTP/3対応を知らせる方法も利用されています。
asnokaze.hatenablog.com

背景2: QUICv2とHTTP/3

RFC9000としてQUICが標準化されましたが、これはQUICバージョン1です。QUICの新しいバージョンであるQUICv2の標準化が進められています。

クライアントとサーバでどのバージョンのQUICで通信するかはQUICのバージョンネゴシエーションにより決定されます。互換性のあるバージョン同士では追加のRTTはありませんが、互換性のないバージョンを使う場合は追加のRTTが発生します。

ここでは詳しくは説明しませんが、気になる方は以前書いた記事を参照ください

asnokaze.hatenablog.com


「An Alt-Svc Parameter for QUIC Versions」

現状では、サーバがどのQUICバージョンに対応しているかクライアントは通信を行うまで分かりません。

現状だとクライアントはQUICv1を介してバージョンネゴシエーションを行うという戦略が妥当ですが、それだとサーバはいつまでもQUICv1をサポートしなければなりません。

そこで、サーバが対応しているQUICバージョンをalt-svcに記載するのがこの提案です。

例を見ると分かりやすいかと思います。

Alt-Svc: h3=":443"; quicv="1"
Alt-Svc: h3=":443"; quicv="709a50c4,1", h3=":1001"; quicv="709a50c4"

(ブログタイトルはQUICv2と書きましたが、将来のQUICバージョンで使えます。)

これで、クライアントはサーバが対応しているQUICバージョンを知ることが出来ます。

本提案によって、QUICv2以降のQUICv1と互換性がないQUICバージョンを利用する際も余計なネゴシエーションオーバヘッドが必要なくなります。

HTTPで再開可能なアップロードを可能にする提案仕様

HTTPでファイルをアップロードする時、ネットワークの寸断などによりアップロードが中途半端に終わってしまうことがあります。アップロードが途中まで成功したとしても、一度切断するとそこから再開する方法は標準化されていません。
(PUTリクエスト + Rangeヘッダによる再開をサポートしているシステムもありますが標準化されてはいません。 参考URL)

そこで、再開可能なアップロードの仕組みを定義した「Resumable Uploads Protocol」という仕様がIETFに提出されています。この仕様は、Transloadit Ltd, Apple Inc, Cloudflare などの方々の共著になっています。

Resumable Uploads Protocol の概要

Resumable Uploads Protocol」は、一度中断しても再開可能なアップロード方式を定義しています。

簡単な流れは次のとおりです

  • クライアントから、POSTやPUTでファイルのアップロードを開始する。この時、再開用トークンをupload-tokenヘッダで送る。
  • サーバは、Resumable Uploads Protocolに対応していれば、104 レスポンスを送る。
  • その後、アップロードが中断した場合は、クライアントは再開用のトークンでアップロードを再開する。

(100番台のレスポンスは、サーバからレスポンスを返しても通信は終わらない点に注意してください。通信を維持し、最終的に改めて正式なレスポンスコードを返します。)

Resumable Uploads Protocolの流れ

Resumable Uploads Protocolにおいて、アップロードの開始および再開の流れについて説明していきます
f:id:ASnoKaze:20220227230313p:plain

アップロードの開始

Resumable Uploads Protocolに対応したクライアントは、まずPOSTやPUTリクエストでファイルのアップロードを開始します。この時次のヘッダを付けます。

  • upload-token: 今回のファイルアップロードを識別するためのトークン。再開時にも利用する
  • upload-draft-interop-version: 準拠している仕様のバージョン。本仕様では1が指定される。
104レスポンス

HTTPの仕様として、100番台のレスポンスは、本来のレスポンスに先んじて返されます。その後正式なレスポンス (200~500番台)が返されます。ですので、100番台のレスポンスを返しても通信は終わりはしません。

Resumable Uploads Protocolでは、サーバがこの仕様に対応している場合は、104レスポンスを返します。104レスポンスは、ボディを受信仕切る前に返せるので、通信が途中で中断される前にクライアントに通知することが出来ます。

(なお、サーバがResumable Uploads Protocolに対応してない場合は、通常通りのPOSTリクエストとして処理されます)

通信の中断

ユーザのアクションや、ネットワークの切断、ソフトウェアの終了などの理由により、アップロードが中断される可能性があります。

アップロードの再開

ファイルのアップロードを再開するにあたって、サーバがどこまで受信したのかをまず確認します。upload-tokenのついたHEADリクエストを送り、サーバから upload-offset を取得します。これが、サーバが受信したデータです。

そのオフセットからファイルアップロードを再開します。POSTリクエストにはupload-tokenおよびupload-offsetを付け送信します。

その他

詳しく説明しませんが、仕様では次のケースについても書かれています

  • サーバ側で、受け取ったupload-tokenに紐づく途中ファイルがない場合の扱い
  • クライアント側から、途中ファイルを破棄するようにサーバに通知する方法
  • 予め長さが分かってないファイルのアップロード ( Upload-Incomplete )

Twitchの QUICを用いたライブストリーミングプロトコル Warp

Twitchは、HLSの代わりとなる、ライブ配信プロトコルWarpを開発している
(情報: MLへの投稿カンファレンスでの発表

このWarpと呼ばれるプロトコルの仕様「Warp - Segmented Live Video Transport」が、IETFに提出されています。

以前書いたFacebookのRushとは異なり、アップロードではなく配信がわのプロトコルのようです。面白そうなので、軽く目を通していこうかと思います

asnokaze.hatenablog.com

Warp

WarpはWebTransport上で実行されるようですが、今回提出された仕様では、QUICやWebTransportのコネクション確立には触れてません。

コネクションの確立後に、QUICの単方向ストリームを用いて、MP4(CMAF)を送信する方法について説明しています。

f:id:ASnoKaze:20220212003940p:plain

仕様の用語を用いると、通信はMedia producerからMedia consumerにセグメント化されたMP4が配信されます。

簡単にまとめると次の通り

  • QUIC単方向ストリームでセグメントを送信する
  • パケロス時の再送はすべてQUICレイヤに任せる (DATAGRAMフレーム使わない)
  • 各セグメントを異なるストリームを用いて、パケロス時のヘッドオブラインブロッキングを回避する
  • Media producerは各セグメントにPriorityを設定でき、高 priorityから優先して送信される (基本 最新のデータの方が優先度が高い。なので、パケロスの再送は相対的に優先度が低くなる)。オーディオストリームと、ビデオストリームで異なるPriorityの設定もできる。
  • Media consumerは、パケロスしたデータに対してどれくらい待つか自身が判断する (巻き戻し出来るようにデータは受け取って起きたいなど。) ただし、ストリームの並列数に上限があるので、優先度の低いものからMedia consumerによってクローズされる。

(疑問: リクエストURLは、WebTransport確立時に与えられているってことでいいのかな)

メッセージとセグメント

各ストリームではメッセージと一つ以上のセグメントを送ることが出来る。メッセージはセグメントのメタデータで、識別子やPriorityが含まれる。セグメントは、 初期化セグメント(ftyp, moovなど)とメディアセグメント(styp, styp, mdat)の2種類がある (CMAFの要件を満たす)。

メッセージはjson形式であり、仕様上はinit, media, priority という値が定義されている。なお拡張可能になっている。(今後形式は変わるかも)

以下に例を示すが、mediaとpriorityなどJSONは結合してもよい。

init: 後続に初期化セグメントが続くことを示すメッセージ。初期化セグメントのIDを持つ。1つずつ増加していく

{
  init: {
    id: int
  }
}

media: 後続にメディアセグメントが続くことを示すメッセージ。関連する初期化セグメントのIDとタイムスタンプを持つ。

{
  segment: {
    init: int,
    timestamp: int,
  }
}

priority: ストリームの優先度を示す。

{
  priority: {
    precedence: int,
  }
}

その他の補足

いくつかの疑問にメーリングリスト上で回答している
https://mailarchive.ietf.org/arch/msg/moq/0ZNlt5SvEzH3mroPOHjlAFpfHDI/

  • 『なぜWebRTCではないのか』
    • => 既存の仕組みよりもユーザエクスペリエンスが良くなかった
  • 『なぜDatagramを使わないのか』
    • => 輻輳制御, 再送制御, キャンセル, 多重化などを再実装することになるから
  • 『なぜHTTP上をつわかないのか』
    • =>Warpの利点である優先制御はHTTP/2やHTTP/3でも動くが、プッシュベースのプロトコルのほうが都合が良かった
  • 『なぜFragmented MP4を使うのか』
    • =>HLSやDASHが使用しており、配信プロトコルに依存せず使える。
  • 『なぜJSONを使うのか』
    • => 最初のDraftで厳密な定義を与えるのが面倒だったため