本記事ではIETFで標準化が行われている、QUICなどのロギングを行うフォーマットであるqlogの、QUICに関するイベントを定義している「QUIC event definitions for qlog」について紹介します。
QUIC event definitions for qlog とは
qlogには発生したイベントを記録するevent
という情報をロギングする仕組みがあります。「QUIC event definitions for qlog」では、qlogにQUICの情報をログとして記録するための、イベント定義が行われています。
具体手的には、event
に対する、イベントの種類を特定するためのname
(category
+ event
)と定義されたイベントをロギングするときに含む具体的なデータの定義が行われています。
それぞれのイベントには、重要性、data
に含まれるデータの定義、そして、想定されているイベントを記録するトリガーが説明されています。
なお、qlogの提案自体は2019年ころから行われていましたが、「QUIC event definitions for qlog」まだWG Draftになって間もない状態です。RFC-9000ではなくおそらくQUIC draft-23を参照しています。
そのため今後の議論によって内容が大きく変わる可能性があります。
レポジトリや、Issueの状況は下記から参照可能です。
レポジトリ
https://github.com/quicwg/qlog
議論
https://github.com/quicwg/qlog/issues
qlog main schemaのおさらい
最初に理解を深めるために、Main logging schema for qlogを見て、event
がどのように格納されているのかおさらいをします。
qlogのトップレベルの構成は、以下のようになっています。traces
の中に、クライアントやサーバーなどで収集したログを格納します。traces
はクライアントとサーバーのログを単一のファイルに格納可能にするため、配列として定義されています。
JSON serialization:
{
"qlog_version": "draft-03-WIP",
"qlog_format": "JSON",
"title": "Name of this particular qlog file (short)",
"description": "Description for this group of traces (long)",
"summary": {
...
},
"traces": [...]
}
traces
には、クライアントやサーバーなどが個別に収集したログが含まれます。
例として、以下のようなフォーマットで格納されています。
{
"title": "Name of this particular trace (short)",
"description": "Description for this trace (long)",
"configuration": {
"time_offset": 150
},
"common_fields": {
"ODCID": "abcde1234",
"time_format": "absolute"
},
"vantage_point": {
"name": "backend-67",
"type": "server"
},
"events": [...]
}
この中の、events
という配列に、実際のQUICクライアント・サーバー実行時に起きたイベントをログとして保存します。
events
には、個別のイベントが含まれます。例として、以下のようなフォーマットで格納されます。
{
time: 1553986553572,
name: "transport:packet_sent",
data: { ... }
protocol_type: ["QUIC","HTTP3"],
group_id: "127ecc830d98f9d54a42c4f0842aa87e181a",
time_format: "absolute",
ODCID: "127ecc830d98f9d54a42c4f0842aa87e181a", // QUIC specific
}
「QUIC event definitions for qlog」では、このイベントに対するname
フィールドと、data
フィールドに記録する情報が定義されています。
name
について
name
フィールドは、data
フィールドに含まれているものを識別するために使用されます。
name
は、category
とtype
を結合したものとして扱われます。
category
とtype
は別々にロギングすることで高レベルでのフィルタリングとイベントタイプの再利用が可能になるようです。ただし、ドラフトではログのサイズが大きく増えるので別々にロギングする方法はあまり使われていないと言及されています。
name
と、category
、type
による記述はそれぞれ以下のようになります。
name
による記述
{
name: "transport:packet_sent"
}
category
とtype
による記述
{
category: "transport",
type: "packet_sent"
}
なお、name
をcategory
とtype
に分ける方法については著者がissueを上げているので今後変わるかもしれません。
QUICに対するname
の定義
現時点では、category
は、connectivity
、security
、transport
、recovery
の4つが定義されています。
現時点のWDでは 「3.1. connectivity」、「3.2. security」、「3.3. transport」、「3.4. recovery」という構成で、それぞれのカテゴリが定義されています。
そして、「3.1.1. server_listening」のようにカテゴリ毎にイベントタイプが定義されています。
イベントのImportanceについて
イベントのImportanceはmain schemaのEvent importance indicatorsで説明されています。
イベントの設計によって、似たような情報や重複がある情報が別々のイベントに含まれるかもしれません。
例えば、QUICのconnection_started
というイベントは、より一般的なconnection_state_updated
と被ります。
このような場合、もし両方のイベントに重複する様な情報が含まれていたとすると、どのイベントを優先するべきかが常に明確とは限りません。(訳注: connection_stated
とconnection_state_updated
自体はログされるデータ自体は現時点では被っていないように思います。)
そのような場合に判断を助けるために、イベントを定義するときには "Importance Indicator" を定義するべきだと書かれています。
現状では、以下の3つの重要度があります。並び順は重要なものから順番に並んでいます。
- Core
- Base
- Extra
Core
Coreは、あるプロトコルのログを含む場合はすべてのqlogファイルに含まれるべきイベントです。
典型的には、基本的なパケットやフレームの解析と生成や、基本的な内部状態のメトリクスに紐づいた情報です。
QUICについて、現時点のドラフトでCoreとしているものは、transport:version_information
、transport:alpn_information
、transport:parameters_set
、transport:packet_received
、recovery:metric_updated
、recovery:packet_lost
の7つです。
Base
"Base"は追加のデバッグのオプションです。
"Base"に含まれるものは多くの場合、"Core"にすべての情報が含まれていれば推測可能ですが、明示的にログを出すことで実装の振る舞いを明確にすることが可能になります。
これらのイベントは、データのバッファへの渡し方、どのように内部の状態が変化したか、実際に受信したデータによってどのような判断が行われたか、などが含まれます。
現状では、14個のイベントが"Base"として定義されています。
実装者が”Core”のイベントをパフォーマンスの問題ですべてロギングしたくないような場合は、代替となるような"Base"のイベントをロギングするべきとドラフトでは書かれています。
Extra
"Extra"は、プロトコルのデバッグよりは実装の低レベルのデバッグに役立つものとして定義されています。
"Extra"として定義されているものにより細かいレベルでの内部の振る舞いが追跡可能になります。
現状では、8個のイベントが"Extra"として定義されています。
qlog main schemaとのつながり
このドキュメントでは、main schemaで定義されたフィールド(例えば、name
, category
, type
, data
, group_id
, protocol_type
, 時間に関連するフィールド、importance, RawInfo
など)が再利用されます。
このドキュメントでQUICのために定義されているイベントをevents
に含める場合、protocol_type
の配列には"QUIC"を含める必要があります。
もし、group_id
を使うのであれば、QUICのOriginal Destination Connection ID (ODCID, クライアントが最初に選んだCID) の使用が推奨されています。これは接続全体で変わらないIDとなるからです。
Raw packet and frame information
main schemaで定義された、RawInfo
も再利用されます。 RawInfo
は、低レベルのバイト長とバイトデータ自身を保存するためのデータ構造です。
class RawInfo {
length?:uint64; // the full byte length of the entity (e.g., packet or frame) including headers and trailers
payload_length?:uint64; // the byte length of the entity's payload, without headers or trailers
data?:bytes; // the contents of the full entity, including headers and trailers
}
Events not belonging to a single connection
特定のQUIC接続に紐づけられないイベントもあります。例えばヘッダに未知のconnection_idがある場合などが考えられます。典型的にはqlogは単一の接続を紐づけるため、それらのイベントをどのようにログするかは明確ではありません。
理想的には特定の接続に紐づけられないエンドポイントレベルのトレースファイルを作り、接続に紐づかないイベントはそこに保存するべきです。しかし、実装によっては現実的でないようです。これらのイベントのほとんどのセマンティクスはプロトコルで十分に定義されており、また、コネクションに属するものとして誤認することは困難であるため、実装者は、たとえ単一のコネクションに強く関連するイベントであっても、特定のコネクションに属さないイベントを他のトレースに記録することを選択してもよいからです。
これによって複数のvantage pointからなるログをマッチさせることが難しくなります。
クライアント側では、version negotiaionやretryを同じトレースに入れるのは簡単ですが、サーバーでは異なるファイルに記録されます。サーバーは、これらのイベントを1つのトレースに入れるために追加の処理が必要になります。
イベントの定義
繰り返しになりますが、「QUIC event definitions for qlog」ではconnectivity
、security
、transport
、recovery
の4つのcategory
が定義されています。それぞれのcategory
に対して event typeが定義されています。
Connectivity
connectivity
には、接続の開始、終了、接続状態の変更などの接続に関するイベントタイプが定義されています。
現状では、検討中のものを含めて7個のイベントタイプが定義されています。
event type | Importance | サマリー |
---|---|---|
server_listening | Extra | サーバーが接続をlistenし始めた時にIPアドレスやポート番号を記録します |
connection_started | Base | クライアントが新しい接続を開始しようとした時と、サーバーが新しい接続をacceptしたときに使われます |
onnection_closed | Base | 接続が閉じたことを記録します |
connection_id_updated | Base | サーバーかクライアントのどちらかが接続IDを更新したことを記録します |
spin_bit_updated | Base | spin bit の更新を記録します |
connection_retried | 検討中 | 検討中 |
connection_state_updated | Base | TLSのハンドシェイクや、接続を閉じるときなどの状態を表します |
マイグレーションに関わるイベントも現在検討中のようです。
server_listening
Importance: Extra
server_listening
はサーバーが接続をacceptしたときに記録されます。server_listening
には、IPアドレスとポート番号を記録できます。
また、サーバーが1-RTT接続を行わない設定の場合に、サーバーが常にretryを送信する設定かを記録するretry_required?
というフィールドがあるようです。
Data:
{
ip_v4?: IPAddress,
ip_v6?: IPAddress,
port_v4?: uint32,
port_v6?: uint32,
retry_required?:boolean // the server will always answer client initials with a retry (no 1-RTT connection setups by choice)
}
connection_started
Importance: Base
connecion_started
は、クライアントが新しい接続を開始しようとした時と、サーバーが新しい接続をacceptした時の両方に使われます。
このイベントは、connection_state_updated
と被ります。connection_state_updated
に対して追加でログするべき情報があるのでconnection_started
はconnection_state_updated
とは別のイベントとして定義されています。
Data:
{
ip_version?: "v4" | "v6",
src_ip?: IPAddress,
dst_ip?: IPAddress,
protocol?: string, // transport layer protocol (default "QUIC")
src_port?: uint32,
dst_port?: uint32,
src_cid?: bytes,
dst_cid?: bytes,
}
connection_closed
Importance: Base
coinnection_closed
は、典型的にはエラーやタイムアウトが起きて接続が閉じたことを記録するために使われます。
このイベントは、connectivity_connection_state_updated
やCONNECTION_CLOSE
フレームと被ります。しかし、運用的には接続が閉じたことを表すイベントがあったほうが便利なため定義されているようです。
このイベントには上記のイベントと異なり理由を定義するフィールドがあります。例えばタイムアウトが起きて接続を閉じる場合、タイムアウトが起きたということを記録するのがほかのイベントでは難しいためこのイベントが役に立ちます。
このイベントはQUICで定義されている、接続エラーやアプリケーションエラーだけでなく、実装に特有の内部エラーコードにも対応できるようになっています。
{
owner?:"local"|"remote", // which side closed the connection
connection_code?:TransportError | CryptoError | uint32,
application_code?:ApplicationError | uint32,
internal_code?:uint32,
reason?:string
}
このイベントには、以下のようなトリガーが想定されています。
- clean
- handshake_timeout
- idle_timeout
- error: 規格で、"immediate close" とされている https://datatracker.ietf.org/doc/html/rfc9000#section-10.2
- stateless reset
- version_mismatch
- application: HTTP3のGOAWAYフレームなど
connection_id_updated
Importance: Base
このイベントは、サーバーかクライアントのどちらかが接続IDを更新したことを記録します。
典型的には接続の中で散発的にしか起こらないので、このイベントタイプを使うことでCIDをすべてのpacket_sent
やpacket_received
のイベントに出力するよりも効率的に記録できます。
もし、新しいconnection id
をピアから受け取ったら、dst_ fieldsがセットされます。
もし、自身のconnectin_id
をNEW_CONNECTION_ID によってセットする場合は、src_ fieldsがセットされます。
訳注: 自身が更新した場合は、owner
を"local"に、ピアが更新した場合はowner
を"remote"に設定するか、あるいは、src_cid
、dst_cid
を設定するかのどちらかになるのではないかと思います。
Data:
{
owner: "local" | "remote",
old?:bytes,
new?:bytes,
}
spin_bit_updated
Importance: Base
このイベントは、spin bitの値が変わったときだけ記録されます。spin bitの値が変わらないときに記録しようとするとすべての1-RTTパケットを記録することになるため、値が変わったときだけ記録します。
Data:
{
state: boolean
}
connection_retried
このイベントはまだ定義されていないようです。
connection_state_updated
Importance: Base
このイベントは、ハンドシェイクの状態と、接続を閉じるときの状態が記録できます。
それぞれの状態を独立に記録できるようにConnectionState
という詳細なオプションが提供されています。
一方で、より簡単でシンプルな実装として、SimpleConnectionState
というものも提供されています。
Data:
{
old?: ConnectionState | SimpleConnectionState,
new: ConnectionState | SimpleConnectionState
}
enum ConnectionState {
attempted, // initial sent/received
peer_validated, // peer address validated by: client sent Handshake packet OR client used CONNID chosen by the server. transport-draft-32, section-8.1
handshake_started,
early_write, // 1 RTT can be sent, but handshake isn't done yet
handshake_complete, // TLS handshake complete: Finished received and sent. tls-draft-32, section-4.1.1
handshake_confirmed, // HANDSHAKE_DONE sent/received (connection is now "active", 1RTT can be sent). tls-draft-32, section-4.1.2
closing,
draining, // connection_close sent/received
closed // draining period done, connection state discarded
}
enum SimpleConnectionState {
attempted,
handshake_started,
handshake_confirmed,
closed
}
これらの状態は、クライアントとサーバーの以下の状態にマッピングされます。
Client
状態名 | 状態 |
---|---|
send inital | attempted |
get initial | peer_validated |
get first handshake packet | handskae_statrted |
get Handshake packet containing Server Finished | handshake_complete |
send ClientFinished | early_write(1RTT が送信可能) |
get HANDSHAKE_DONE | handshake_confirmed |
Server
状態名 | 状態 |
---|---|
get initial | attempted |
send intial | 検討中 他のフレームと一緒に送信されるから必要ないかもしれない。 |
send handshake EE, Cert, CV | handshake_started |
send ServerFinished | early_write (1RTT が送信可能) |
get first handshake packet / something using a server-issued CID of min length | validated |
get handshake packet containing ClientFinished | handshake_complete |
state | handshake_coinfirmed |
attemptedは、概念的には、クライアント観点のconnection started
と同じです。
また、closing or drainingは、connection closed
イベントと同じです。
MIGRATION-related events
マイグレーションに関わるイベントはまだ検討中のようです。
security
sercurity
には鍵の更新のイベントと鍵のリトライのイベントの二つが現状では定義されています。
event type | Importance | サマリー |
---|---|---|
key_updated | Base | 鍵の更新を記録します |
key_retried | Base | ? |
key_updated
Importance: Base
このイベントは鍵の更新を記録します。secret_updated
のほうがより正確ですが、現状のドラフトでは、KEY_UPDATEとの一貫性のためこの名前にしているようです。
Key Updateについては、QUIC-TLSの6. Key Update を参照ください。
Data:
{
key_type:KeyType,
old?:bytes,
new:bytes,
generation?:uint32 // needed for 1RTT key updates
}
トリガーは以下の項目が考えられています。
- "tls": 例えば、TLSによるinitial, handshake, 0-RTTのキーの生成が例としてあります。
- "remote_update"
- "local_update"
key_retired
Importance: Base
現状では説明は特に書かれていませんが、
Retry Packetと関連するのではないかと思います。
Data:
{
key_type:KeyType,
key?:bytes,
generation?:uint32 // needed for 1RTT key updates
}
トリガーは以下の項目が考えられます。
- "tls" // (e.g., initial, handshake and 0-RTT keys are dropped implicitly)
- "remote_update"
- "local_update"
transport
transport
にはQUICのバージョンやALPNの結果、trasnport parameter、パケットの送受信のタイミングやそれにかかわるデータなどの情報が含まれます。
event type | Importance | サマリー |
---|---|---|
version_information | Core | QUICのバージョン情報を記録します |
alpn_information | Core | ALPN Negotiationの情報を記録します |
parameters_set | Core | transport parameter を記録します |
parameters_restored | Base | 0-RTT接続の場合に、前の接続から復元して使うtransport parameterを記録します |
packet_sent | Core | 送信するQUICパケットの情報を記録します |
packet_received | Core | 受信したQUICパケットの情報を記録します |
packet_dropped | Base | QUICパケットが、解析に失敗などしてドロップした場合に使われます |
packet_buffered | Base | パケットがバッファリングされて処理されていないときに使います |
packets_acked | Extra | QUICパケットがAckされた場合に使用します |
datagrams_sent | Extra | UDPのデータグラムをソケットにパスしたときにログに書き込みます(DATAGRAM拡張ではありません) |
datagrams_received | Extra | UDPのデータグラムをOS側から受信したときに使います |
datagram_dropped | Extra | UDPのデータグラムがドロップしたときに使われます |
stream_state_updated | Base | ストリームの状態が更新されたときに発行されます |
frames_processed | Extra | QUICのフレームが処理されたことを表すときに使われます |
data_moved | Base | データが異なるレイヤを移動したことを記録するときに使われます |
version_information
Importance: Core
QUICのエンドポイントはサーバー・クライアントともにサポートしているQUICのversionを保持しています。クライアントは最もそれらしいバージョンを最初のinitialに使います。
サーバーはそのバージョンをサポートしていない場合、サーバーがサポートしているバージョンが含まれているversion_negotiation パケットを送信します。
クライアントはその中からバージョンを選択します。
このイベントはそれらの情報を一つのイベントにまとめます。このイベントは、エンドポイントがサポートしているバージョンを、バージョンネゴシエーションが実際に行われたタイミングじゃなくてもログに記録することができます。
参考: 6. Version Negotiation、15. Versions
Data:
{
server_versions?:Array<bytes>,
client_versions?:Array<bytes>,
chosen_version?:bytes
}
このイベントは以下のような使い方が想定されています。
- クライアントがinitialを送ったときに、
client_version
とchosen_version
をセットしてログを記録します。 - サーバーがクライアントからのサポートしているバージョンを含むinitialを受け取ったら、このイベントにを
server_version
とchosen_version
をセットしてログを書き込みます。 - サーバーがサポートしていないバージョンをクライアントから受け取ったら、
sever_version
にサーバーがサポートしているバージョンを、client_version
にクライアントが使おうとしたバージョンをログに記録します。chosen_version
を空にすることで、クライアントとサーバーのバージョンにマッチするものがなかったことを現わせます。 - クライアントは、version negotiation packetをサーバーから受け取ったら、
client_version
の集合と、server_versions
の集合とchosen_version
(次のinitial packetで使う)をログに書き込みます。
alpn_information
Importance: Core
QUICの実装はそれぞれアプリケーションレベルの実装の一覧とサポートしているバージョンを持っています。
TLSのAPLN拡張として、クライアントは自身がサポートしている一覧をinitialに入れて送ります。
もし共通のものがあればサーバーは最適なものを選んでクライアントに返します。
共通のものがない場合はコネクションを閉じます。
Data:
{
server_alpns?:Array<string>,
client_alpns?:Array<string>,
chosen_alpn?:string
}
以下のような使い方が想定されています。
- initialを送るときにクライアントは
client_alpns
の集合をログに記録する。 - サーバーはサポートしているalpnを受信した時に、サーバーのalpn、クライアントのalpn、選んでクライアントに送ったalpnをサーバーのログに含めます。
- クライアントは、alpnをinitialで受信したら
chosen_alpn
をログに記録します。 - クライアントは最初のイベントをログせずに、サーバーからintialを受信したときに
client_alpn
とchosen_alpn
を一緒に保存することもできます。
parameters_set
Importance: Core
parameters_set
は、transport parameter、 TLC ciphersなどいくつかの異なる情報を一つのイベントとして定義します。このように定義しているのは、イベントの量を最小限に抑え、概念的な設定の影響をその基礎となるメカニズムから切り離すことで、高レベルの推論を容易にするためです。
典型的にはこれらの設定は一度設定されたら変更されません。しかし、典型的には接続の異なるタイミングで使用されます。そのため、parameter_set
のイベントは異なるフィールドを持ったインスタンスが存在することになります。
設定には、ローカルでセットしたものとリモートのピアからリクエストされたものの2種類が存在します。これは"owner"フィールドに反映されます。そのため、このフィールドは、1つのイベントインスタンスに含まれるすべての設定に対して正しくなければなりません。 2つの側からの設定を記録する必要がある場合は 2つの別々のイベントインスタンスを発行しなければなりません。
接続の再開と0-RTTの場合、サーバーのいくつかのパラメータはクライアントに保存され、接続の開始時に使用されます。
それらは後程サーバーからのリプライによって更新されます。この場合、"parameters_restored"を活用し初期値と、更新された値を表すことになります。
Data:
{
owner?:"local" | "remote",
resumption_allowed?:boolean, // valid session ticket was received
early_data_enabled?:boolean, // early data extension was enabled on the TLS layer
tls_cipher?:string, // (e.g., "AES_128_GCM_SHA256")
aead_tag_length?:uint8, // depends on the TLS cipher, but it's easier to be explicit. Default value is 16
// transport parameters from the TLS layer:
original_destination_connection_id?:bytes,
initial_source_connection_id?:bytes,
retry_source_connection_id?:bytes,
stateless_reset_token?:Token,
disable_active_migration?:boolean,
max_idle_timeout?:uint64,
max_udp_payload_size?:uint32,
ack_delay_exponent?:uint16,
max_ack_delay?:uint16,
active_connection_id_limit?:uint32,
initial_max_data?:uint64,
initial_max_stream_data_bidi_local?:uint64,
initial_max_stream_data_bidi_remote?:uint64,
initial_max_stream_data_uni?:uint64,
initial_max_streams_bidi?:uint64,
initial_max_streams_uni?:uint64,
preferred_address?:PreferredAddress
}
interface PreferredAddress {
ip_v4:IPAddress,
ip_v6:IPAddress,
port_v4:uint16,
port_v6:uint16,
connection_id:bytes,
stateless_reset_token:Token
}
このイベントは、unknown (greased)なトランスポートパラメータや、独自仕様のトランスポートパラメータを含むことが可能です。
parameters_restored
Importance: Base
0-RTTを使う時、クライアントは以前の接続のサーバーのトランスポートパラメータを記憶して保存していることが想定されます。このイベントは、どのパラメータが復元されたのかを記録できます。
再利用が禁止されているものもあるため、すべてのトランスポートパラメータ―が復元されるべきではありません。ここに定義されているものは0-RTTを正しく使うために役立つと期待されるものです。
Data:
{
disable_active_migration?:boolean,
max_idle_timeout?:uint64,
max_udp_payload_size?:uint32,
active_connection_id_limit?:uint32,
initial_max_data?:uint64,
initial_max_stream_data_bidi_local?:uint64,
initial_max_stream_data_bidi_remote?:uint64,
initial_max_stream_data_uni?:uint64,
initial_max_streams_bidi?:uint64,
initial_max_streams_uni?:uint64,
}
parameter_set
と同じように、このイベントには仕様書では記述されていない追加のフィールドやカスタムパラメータを持つことが可能です。
packet_sent
Importance: Core
このイベントには、送信されるQUIC Packetの情報が含まれます。具体的には、PacketHeaderの情報、そのQUIC Packetに含まれるQUIC Frame などがあります。
Data:
{
header:PacketHeader,
frames?:Array<QuicFrame>, // see appendix for the definitions
is_coalesced?:boolean, // default value is false
retry_token?:Token, // only if header.packet_type === retry
stateless_reset_token?:bytes, // only if header.packet_type === stateless_reset. Is always 128 bits in length.
supported_versions:Array<bytes>, // only if header.packet_type === version_negotiation
raw?:RawInfo,
datagram_id?:uint32
}
このイベントには以下のようなトリガーが想定されています。
- "retransmit_reordered" // draft-23 5.1.1
- "retransmit_timeout" // draft-23 5.1.2
- "pto_probe" // draft-23 5.3.1
- "retransmit_crypto" // draft-19 6.2
- "cc_bandwidth_probe" // needed for some CCs to figure out
bandwidth allocations when there are no normal sends
packet_received
Importance: Core
このイベントには、送信されるQUIC Packetの情報が含まれます。
具体的には、PacketHeaderの情報、そのQUIC Packetに含まれるQUIC Frame などがあります。
Data:
{
header:PacketHeader,
frames?:Array<QuicFrame>, // see appendix for the definitions
is_coalesced?:boolean,
retry_token?:Token, // only if header.packet_type === retry
stateless_reset_token?:bytes, // only if header.packet_type === stateless_reset. Is always 128 bits in length.
supported_versions:Array<bytes>, // only if header.packet_type === version_negotiation
raw?:RawInfo,
datagram_id?:uint32
}
以下のようなトリガーが想定されています。
- "keys_available": パケットがバッファされたタイミング。そのタイミングで復号可能になるため。
packet_dropped
Importance: Base
このイベントはQUICレベルのパケットのドロップが、部分的に解析した後、あるいは、解析無しで見つかったことを表します。
Data:
{
header?:PacketHeader, // primarily packet_type should be filled here, as other fields might not be parseable
raw?:RawInfo,
datagram_id?:uint32
}
このイベントに対しては、デバッグの助けになるので、trigger
フィールドはセットされるべきです。
trigger
フィールドにセットされる値は、以下のようなものが想定されています。
- "key_unavailable"
- "unknown_connection_id"
- "header_parse_error"
- "payload_decrypt_error"
- "protocol_violation"
- "dos_prevention"
- "unsupported_version"
- "unexpected_packet"
- "unexpected_source_connection_id"
- "unexpected_version"
- "duplicate"
- "invalid_initial"
packet_buffered
Importance: Base
このイベントは、パケットがバッファリングされている時に、処理されていないことを記録するために使います。典型的には、パケットの解析が終わっていないので、生の情報をログする形になります。
Data:
{
header?:PacketHeader, // primarily packet_type and possible packet_number should be filled here, as other elements might not be available yet
raw?:RawInfo,
datagram_id?:uint32
}
以下のようなトリガーが想定されています。
- "backpressure": パーサーの処理が追いつかないことを示し、後の処理のためにパケットを一時的にバッファします。
- "keys_unavailable": 適切な鍵がないため複合ができない場合
packets_acked
Importance: Extra
このイベントは送信した(グループの)QUICパケット(複数)がリモートのピアによってackされた場合にログに記録します。
この情報は、ACKフレームの中身から推測ができます。しかし、ACKフレームによって、パケットがACKされたかを確認しようとした場合、繰り返し同じ範囲が含まれるため、あるパケットが初めてACKされたのがそのACKフレームなのかを判断するロジックが必要になります。
packet_acked
イベントによってそれを回避することが可能になります。
Data:
{
packet_number_space?:PacketNumberSpace,
packet_numbers?:Array<uint64> }
もし、packet_number_spaceが捨てられた場合、PacketNumberSpace.application_dataのデフォルトの値を仮定します。なぜなら、それが一般的に最もよく使用されるQUICのパケットだからです。
datagrams_sent
Importance: Extra
UDPレベルのデータグラムをソケットにパスしたときにログに書き込みます。(Datagram拡張ではありません)
これにより、QUICのパケットバッファがどのようにOSに渡されているかが分かります。
Data:
{
count?:uint16, // to support passing multiple at once
raw?:Array<RawInfo>, // RawInfo:length field indicates total length of the datagrams, including UDP header length
datagram_ids?:Array<uint32>
}
QUIC自体にはdatagram_idのコンセプトはありません。
このフィールドは、qlogレベルで、複数のQUICパケットがUDPのデータグラムでどのように融合されたのかトラッキングするための、qlog特有のものになります。
これは、QUICハンドシェイクにとって重要なオプションになります。実装では、ユニークIDをそれぞれのデータグラムに割り当て、どのパケットが同じデータグラムに融合されたのかを記録します。
パケットの融合は典型的にはハンドシェイクの時に起きるため(少なくとも一つlong header packetを必要とする)、このイベントはほとんどオーバーヘッドなしで扱えるようです。
datagrams_received
Importance: Extra
1または複数のUDPレベルのデータグラムを受信したときに使います。
どのようにデータグラムがOSからユーザースペースに渡されているかを判断するのに役に立ちます。
Data:
{
count?:uint16, // to support passing multiple at once
raw?:Array<RawInfo>, // RawInfo:length field indicates total length of the datagrams, including UDP header length
datagram_ids?:Array<uint32>
}
datagram_dropped
Importance: Extra
UDP-levelのデータグラムがドロップしたときに、使われます。
これは典型的には、validなQUICパケットを含んでいない場合に使われます。(そのような場合はpacket_dropped
が使われます。)
Data:
{
raw?:RawInfo
}
stream_state_updated
Importance: Base
このイベントは、内部のストリームの状態が更新された時に発行されます。
draft-23のセクション3に書かれているように、多くの場合はいくつかのタイプのフレームの情報を元に判断します。
しかし、明示的なシグナルがあることでこの判断が行いやすくなります。
Data:
{
stream_id:uint64,
stream_type?:"unidirectional"|"bidirectional", // mainly useful when opening the stream
old?:StreamState,
new:StreamState,
stream_side?:"sending"|"receiving"
}
enum StreamState {
// bidirectional stream states, draft-23 3.4.
idle,
open,
half_closed_local,
half_closed_remote,
closed,
// sending-side stream states, draft-23 3.1.
ready,
send,
data_sent,
reset_sent,
reset_received,
// receive-side stream states, draft-23 3.2.
receive,
size_known,
data_read,
reset_read,
// both-side states
data_received,
// qlog-defined
destroyed // memory actually freed
}
QUICの実装では、より細かいストリームの状態(例:data_sent、reset_received)ではなく、簡略化された双方向(HTTP/2-alike)のストリームの状態(例:idle、open、closed)を主に記録すべきです。 後者は主により詳細なデバッグのためのものです。 ツールは両方のタイプを同等に扱うことができるべきです。
frames_processed
Importance: Extra
packet_received
は受信したタイミングですが、このイベントは、QUICフレームを処理して適用した結果を記録できるようです。例えば、
このイベントの目的は、特定の目的のために大量にイベントが定義されるのを防ぐためにあります。
例えば、packet_acknowledged
, flow_control_updated
, stream_data_received
といったものを個別に定義する場合が考えられます。
packet_received
のようにパケットレベルの詳細を記録することなく、このタイプの情報を(選択的に)記録する機会を実装に与えたいという意図があります。
ほとんどの場合、実装の内部状態にフレームを適用した効果はそのフレームの内容から推測できるため、これらのイベントを「frames_processed
」イベントに集約しています。
このイベントは、パースの直接の結果ではなく内部状態が変わったことシグナルに使うことができます。例えば、フレームがパースされ、データがバッファに入れられ、そののちに処理されたあとで、このイベントを記録します。
packet_received
にはすべてのパケットの構成要素を含まれているので、それを実装する場合はframe_processed
はログされることを予期しません。むしろ、すべてのパケットをログしたくない、あるいは、フレームが処理された場合追加の情報を明示的に出したいにこのイベントが使えます。
いくつかのイベントに対して、このアプローチは情報を失います(例えば、暗号レベルのパケットのAck?)。もしそういった情報が重要ならpacket_received
を使うべきです。
いくつかの実装では、packet_sent
やpacket_received
イベントを使ってもフレームを直接ログするのは難しいです。
その場合、このイベントが、packet_numberフィールドを直接含み、明示的にpacket_sent
、packet_received
とリンクされます。
Data:
{
frames:Array<QuicFrame>, // see appendix for the definitions
packet_number?:uint64
}
data_moved
Importance: Base
例えば、HTTPなどのアプリケーションプロトコルからQUICのストリームバッファへの受け渡しや、アプリケーションプロトコル間(HTTPとユーザーのプロトコルの間)のデータの移動などのデータが異なるレイヤを移動しているときに使われます。
これによりデータのフローが明らかになり、どのくらいの間バッファにデータが滞留しているかや、それによるそれぞれのレイヤのオーバーヘッドが分かります。
例えば、QUICのストリームに渡されたデータがアプリケーションプロトコルに即座に渡されたか(例えば受信したパケット毎に渡されているか)のか、巨大なバッチ(すべてのクイックパケットが最初に処理されてそれからアプリケーションレイヤが新しく利用可能になったストリームのデータを読み込む)として扱われたのかが明確になります。
これにより、ボトルネックやスケジューリングの問題が明らかにできます。
Data:
{
stream_id?:uint64,
offset?:uint64,
length?:uint64, // byte length of the moved data
from?:string, // typically: use either of "user","application","transport","network"
to?:string, // typically: use either of "user","application","transport","network"
data?:bytes // raw bytes that were transferred
}
"direction"フィールド("up"/"down") はデータフローを記述するためには使われません。これは、いくつかの最適化で、データが独立したレイヤをスキップするからです。
さらに、"from"、"to"によって、柔軟な、概念的なレイヤ間のデータの渡し方を定義できます。
例えば、QUIC CRYPTフレームがTLSレイヤに渡される、あるいは、HTTP/3からQPACKに渡される、といったことが記述できます。
このイベントタイプは、transportのカテゴリにありますが、実際に、他の例やでも適用することができます。
これによって、いくつかの抽象化の漏れが起きます(例えば、ストリームIDあるいはストリームオフセットがロギングポイントで使えない、あるいは、rawデータがバイトアレイに含まれない)。
このような場合、実装は新しい、in-contextフィールドを手動のデバッグのために定義することが可能です。
recovery
recovery
のカテゴリには、ロス検出や輻輳制御に関連するイベントが定義されています。
おそらく、QUIC Loss Detection and Congestion Control に関連するものをまとめているのではないかと思います。
event type | Importance | サマリー |
---|---|---|
parameters_set | Base | ロス検出や輻輳制御に関するパラメータを記録します |
metrics_updated | Core | 1または複数のリカバリ用のメトリクスに変更があったことを記録します |
congestion_state_updated | Base | 輻輳制御が何らかの新しい状態に入ったことや振る舞いが変わったことを記録します |
loss_timer_updated | Extra | リカバリロスタイマーが変更したときに発行されます |
packet_lost | Core | パケットロスが検出されたときに発行されます |
marked_for_retransmit | Extra | どのデータがパケットロスにより再走されたものかをマークします |
このカテゴリのほとんどのイベントは、異なるリカバリと多くの輻輳制御のアルゴリズムの方法をサポートするために汎用的に作られています。
したがって、ツールを作る場合はこれらのイベントに不明なフィールドがあったとしてもサポートと可視化する努力をするべきと書かれています(例えば、可視化の時に、未知の輻輳制御の状態名をタイムライン上にプロットできるようにする)。
parameters_set
Importance: Base
このイベントは、ロス検出と輻輳制御に関する初期のパラメータをひとまとめにしています。
これらの設定は、典型的に一度設定されると変わりませんが、実装で何らかの理由で更新されることがあります。その場合、parameter_setのログを2回書き込むことがあるかもしれません。
Data:
{
// Loss detection, see recovery draft-23, Appendix A.2
reordering_threshold?:uint16, // in amount of packets
time_threshold?:float, // as RTT multiplier
timer_granularity?:uint16, // in ms
initial_rtt?:float, // in ms
// congestion control, Appendix B.1.
max_datagram_size?:uint32, // in bytes // Note: this could be updated after pmtud
initial_congestion_window?:uint64, // in bytes
minimum_congestion_window?:uint32, // in bytes // Note: this could change when max_datagram_size changes
loss_reduction_factor?:float,
persistent_congestion_threshold?:uint16 // as PTO multiplier
}
このイベントは異なるリカバリの方法をサポートするためにここでは記述されていないフィールドを持つかもしれません。
metrics_updated
Importance: Core
このイベントは、1または複数のリカバリ用のメトリクスの変更があった場合に発行されます。
このイベントグループは、一度に起きたあるいは同時に起きたすべてのメトリクスの更新をグループにするべきです。(例えばmin_rttとsmoothed_rttが同時に変更されたら、一つのmetrics_updated エントリに入れます)。
結果、metric_updated
イベントは、リストされたメトリクスから少なくとも一つのものが含まれていることが保証されます。
Data:
{
// Loss detection, see recovery draft-23, Appendix A.3
min_rtt?:float, // in ms or us, depending on the overarching qlog's configuration
smoothed_rtt?:float, // in ms or us, depending on the overarching qlog's configuration
latest_rtt?:float, // in ms or us, depending on the overarching qlog's configuration
rtt_variance?:float, // in ms or us, depending on the overarching qlog's configuration
pto_count?:uint16,
// Congestion control, Appendix B.2.
congestion_window?:uint64, // in bytes
bytes_in_flight?:uint64,
ssthresh?:uint64, // in bytes
// qlog defined
packets_in_flight?:uint64, // sum of all packet number spaces
pacing_rate?:uint64 // in bps
}
ロギングを簡単にするためには値の更新がなかったパラメータについてもロギングしたほうがよいかもしれないです。しかし、アプリケーションは1回、実際に更新されたものだけをログするべきです。
また、このイベントは未定義のフィールドを含むかもしれません。
congestion_state_updated
Importance: Base
このイベントは、輻輳制御が何らかの新しい状態に入ったことや振る舞いが変わったことを表します。
このイベントの定義は、異なる輻輳制御アルゴリズムをサポートするために汎用的に定義されています。
例えば、Recovery に定義されたアルゴリズム ("enhanced" New Reno) は以下のような状態が定義されます。
- slow_start
- congestion_avoidance
- application_limited
- recovery
Data:
{
old?:string,
new:string
}
トリガーフィールドは、状態変化を起こすものが複数ある場合は保存するべきです。しかし、単一のイベントのみによって起きる場合無視することができます。例えば、slow startはssthreshが超えた場合のみ抜け出します。
"enhanced" New Reno の場合は以下のようなトリガーが考えられます。
- persistent_congestion
- ECN
loss_timer_updated
Importance: Extra
このイベントは、リカバリロスタイマーが変更したときに発行されます。
おもに3つのタイミングがあります。
- set: タイマーを次にイベントが発生するdelta timeoutにセットします。
- expired: delta taimeoutが切れた場合
- canceld: タイマーがキャンセルされた場合(すべてのパケットがackされて、アイドル状態になる)
アクティブなタイマーのタイムアウトを示すために、新しい"set"イベントが使われる。
Data:
{
timer_type?:"ack"|"pto", // called "mode" in draft-23 A.9.
packet_number_space?: PacketNumberSpace,
event_type:"set"|"expired"|"cancelled",
delta?:float // if event_type === "set": delta time in ms or us (see configuration) from this event's timestamp until when the timer will trigger
}
複数のタイマーを使うような場合は現在検討中のようです。
packet_lost
Importance: Core
このイベントは、パケットがロス検出によってロスされたと判断されたときに発行されます。
Data:
{
header?:PacketHeader, // should include at least the packet_type and packet_number
// not all implementations will keep track of full packets, so these are optional
frames?:Array<QuicFrame> // see appendix for the definitions
}
トリガーは、以下のようなものが想定されます。
- "reordering_threshold",
- "time_threshold"
- "pto_expired" // draft-23 section 5.3.1, MAY
marked_for_retransmit
Importance: Extra
このイベントは、どのデータがパケットロスによる再送されたものをマークします。
frame_processed
と同様の理由で、異なるイベントを少なくするために、すべてのタイプの再走されるデータを単一のイベントで定義します。
フルパケットあるいはフレームを直接再送するような実装は、このログをロスしたパケット全体を記録するために使えます。
(あるいは、このイベントは使わずに、packet_lost
イベントを使います。)
より複雑な実装(例えば、送信中のストリームの範囲を記録している)場合や、フレーム全体を記録していない(例えばストリームのオフセットと長さを記録している)ような場合、それらの内部の振る舞いを適切にフレームに変換する必要があります。
Data:
{
frames:Array<QuicFrame>, // see appendix for the definitions
}
QUIC data field definitions
enumなどの再利用可能なものはドキュメントの最後のAppendix A に定義されています。
IPAddress
class IPAddress : string | bytes;
IPアドレスは、人間が読める形式か生のバイトデータのどちらも可能です。
PacketType
enum PacketType {
initial,
handshake,
zerortt = "0RTT",
onertt = "1RTT",
retry,
version_negotiation,
stateless_reset,
unknown
}
PacketNumberSpace
enum PacketNumberSpace {
initial,
handshake,
application_data
}
PacketHeader
class PacketHeader {
// Note: short vs long header is implicit through PacketType
packet_type: PacketType;
packet_number: uint64;
flags?: uint8; // the bit flags of the packet headers (spin bit, key update bit, etc. up to and including the packet number length bits if present) interpreted as a single 8-bit integer
token?:Token; // only if packet_type == initial
length?: uint16, // only if packet_type == initial || handshake || 0RTT. Signifies length of the packet_number plus the payload.
// only if present in the header
// if correctly using transport:connection_id_updated events,
// dcid can be skipped for 1RTT packets
version?: bytes; // e.g., "ff00001d" for draft-29
scil?: uint8;
dcil?: uint8;
scid?: bytes;
dcid?: bytes;
}
Token
class Token {
type?:"retry"|"resumption"|"stateless_reset";
length?:uint32; // byte length of the token
data?:bytes; // raw byte value of the token
details?:any; // decoded fields included in the token (typically: peer's IP address, creation time)
}
initial packetで運ばれるトークンは、
①Retryパケットのretry token
②Stateless reset packetからのstateless reset token
③接続を再開するための、サーバーから提供されるNewTokenフレーム(アドレス検証プロセス)
のどれかです。
Retryとresumptionトークンは典型的にはメタデータにエンコードされ、妥当性を検証されます。しかし、このメタデータとフォーマットは実装依存です。なので、このフィールドは汎用的なdetailsというフィールドにされています。
KeyType
enum KeyType {
server_initial_secret,
client_initial_secret,
server_handshake_secret,
client_handshake_secret,
server_0rtt_secret,
client_0rtt_secret,
server_1rtt_secret,
client_1rtt_secret
}
QUIC Frames
type QuicFrame = PaddingFrame | PingFrame | AckFrame | ResetStreamFrame | StopSendingFrame | CryptoFrame | NewTokenFrame | StreamFrame | MaxDataFrame | MaxStreamDataFrame | MaxStreamsFrame | DataBlockedFrame | StreamDataBlockedFrame | StreamsBlockedFrame | NewConnectionIDFrame | RetireConnectionIDFrame | PathChallengeFrame | PathResponseFrame | ConnectionCloseFrame | HandshakeDoneFrame | UnknownFrame;
PaddingFrame
QUICでは、PADDINGフレームは1つの0バイトを表しています。論理的には、各パディングバイトがパディングフレームとして記録されます。
しかし、そうするとログのオーバーヘッドが大きくなってしまうため、実装は一つのPaddingFrameだけを発行し、paload_length
プロパティでパケットに含まれるPADDINGバイトの量を表すことができます。
class PaddingFrame{
frame_type:string = "padding";
length?:uint32; // total frame length, including frame header
payload_length?:uint32;
}
PingFrame
class PingFrame{
frame_type:string = "ping";
length?:uint32; // total frame length, including frame header
payload_length?:uint32;
}
AckFrame
class AckFrame{
frame_type:string = "ack";
ack_delay?:float; // in ms
// first number is "from": lowest packet number in interval
// second number is "to": up to and including // highest packet number in interval
// e.g., looks like [[1,2],[4,5]]
acked_ranges?:Array<[uint64, uint64]|[uint64]>;
// ECN (explicit congestion notification) related fields (not always present)
ect1?:uint64;
ect0?:uint64;
ce?:uint64;
length?:uint32; // total frame length, including frame header
payload_length?:uint32;
}
AckFrame.acked_rangesは、順番になっていなくてもよいです。ログを出す側は[120,120]を[120]と出すべきです。ただし、ツールは両方対応できないといけません。
ResetStreamFrame
class ResetStreamFrame{
frame_type:string = "reset_stream";
stream_id:uint64;
error_code:ApplicationError | uint32;
final_size:uint64; // in bytes
length?:uint32; // total frame length, including frame header
payload_length?:uint32;
}
StopSendingFrame
class StopSendingFrame{
frame_type:string = "stop_sending";
stream_id:uint64;
error_code:ApplicationError | uint32;
length?:uint32; // total frame length, including frame header
payload_length?:uint32;
}
CryptoFrame
class CryptoFrame{
frame_type:string = "crypto";
offset:uint64;
length:uint64;
payload_length?:uint32;
}
NewTokenFrame
class NewTokenFrame{
frame_type:string = "new_token";
token:Token
}
StreamFrame
class StreamFrame{
frame_type:string = "stream";
stream_id:uint64;
// These two MUST always be set
// If not present in the Frame type, log their default values
offset:uint64;
length:uint64;
// this MAY be set any time, but MUST only be set if the value is "true"
// if absent, the value MUST be assumed to be "false"
fin?:boolean;
raw?:bytes;
}
MaxDataFrame
class MaxDataFrame{
frame_type:string = "max_data";
maximum:uint64;
}
MaxStreamDataFrame
class MaxStreamDataFrame{
frame_type:string = "max_stream_data";
stream_id:uint64;
maximum:uint64;
}
MaxStreamsFrame
class MaxStreamsFrame{
frame_type:string = "max_streams";
stream_type:string = "bidirectional" | "unidirectional";
maximum:uint64;
}
DataBlockedFrame
class DataBlockedFrame{
frame_type:string = "data_blocked";
limit:uint64;
}
StreamDataBlockedFrame
class StreamDataBlockedFrame{
frame_type:string = "stream_data_blocked";
stream_id:uint64;
limit:uint64;
}
StreamsBlockedFrame
class StreamsBlockedFrame{
frame_type:string = "streams_blocked";
stream_type:string = "bidirectional" | "unidirectional";
limit:uint64;
}
NewConnectionIDFrame
class NewConnectionIDFrame{
frame_type:string = "new_connection_id";
sequence_number:uint32;
retire_prior_to:uint32;
connection_id_length?:uint8;
connection_id:bytes;
stateless_reset_token?:Token;
}
RetireConnectionIDFrame
class RetireConnectionIDFrame{
frame_type:string = "retire_connection_id";
sequence_number:uint32;
}
PathChallengeFrame
class PathChallengeFrame{
frame_type:string = "path_challenge";
data?:bytes; // always 64-bit
}
PathResponseFrame
class PathResponseFrame{
frame_type:string = "path_response";
data?:bytes; // always 64-bit
}
ConnectionCloseFrame
raw_error_codeは、実際には数値のコードです。これは、一部のエラータイプが複数のコードにまたがっている場合に便利です。
type ErrorSpace = "transport" | "application";
class ConnectionCloseFrame{
frame_type:string = "connection_close";
error_space?:ErrorSpace;
error_code?:TransportError | ApplicationError | uint32;
raw_error_code?:uint32;
reason?:string;
trigger_frame_type?:uint64 | string; // For known frame types, the appropriate "frame_type" string. For unknown frame types, the hex encoded identifier value
}
HandshakeDoneFrame
class HandshakeDoneFrame{
frame_type:string = "handshake_done";
}
UnknownFrame
class UnknownFrame{
frame_type:string = "unknown";
raw_frame_type:uint64;
raw_length?:uint32;
raw?:bytes;
}
TransportError
enum TransportError {
no_error,
internal_error,
connection_refused,
flow_control_error,
stream_limit_error,
stream_state_error,
final_size_error,
frame_encoding_error,
transport_parameter_error,
connection_id_limit_error,
protocol_violation,
invalid_token,
application_error,
crypto_buffer_exceeded
}
CryptoError
このエラーは、TLSのドキュメントに「TLSアラートは、1バイトのエラーコードをQUICエラーコードに変換することでQUIC接続エラーに変換される。このアラートは、0x100からQUICのエラーコードのために予約される」と記されています。
このアプローチは、事前に定義されたenumです。crypto_error
文字列は、hex-encodedされたTLSアラートを表すダイナミックコンポーネントとして定義します。
enum CryptoError {
crypto_error_{TLS_ALERT}
}
終わりに
長くなりましたが、「QUIC event definitions for qlog」でこれから定義しようとしている、qlogにQUICのイベントを記録するための情報の定義を紹介しました。
個人的には、ログされるイベントから規格の理解を深めることもできるのではないかと思いました。