HTTP/3で通信する
それではHTTP/3でwww.google.com
と通信する様子を実際のログを元に解説していこうと思います。
使用するQUICクライアントはngtcp2のexamples/clientを使います。コマンドラインは以下の通り:
$ examples/client www.google.com 443 https://www.google.com/
I00000000 pkt tx pkn=0 dcid=0x79a9a0491ffc7e17ccc7310e9543c5e75645 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Initial(0x00) len=0 k=0
I00000000 frm tx 0 Initial(0x00) CRYPTO(0x06) offset=0 len=354
I00000000 frm tx 0 Initial(0x00) PADDING(0x00) len=812
I00000000 rcv loss_detection_timer=691118582537024 last_tx_pkt_ts=691117583537024 timeout=999
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x2 1232 bytes
dcidはクライアントが選択したDestination Connection IDですが、これは後にサーバーによって生成し直されます。scidはクライアントのSource Destination IDです。txは送信を意味します。後で出てきますがrxは受信を意味します。InitialパケットのCRYPTOフレームで354バイトのデータを送信します。これにはTLSv1.3のClientHelloが含まれます。UDP datagramのペイロードを最低1200バイト以上にしなければならないため、PADDINGフレームでサイズを増やしています。ECNにECTコードポイントもセットしています。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000003 con recv packet len=1232
I00000003 pkt rx pkn=1 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Initial(0x00) len=1197 k=0
I00000003 frm rx 1 Initial(0x00) ACK(0x02) largest_ack=0 ack_delay=0(0) ack_block_count=0
I00000003 frm rx 1 Initial(0x00) ACK(0x02) block=[0..0] block_count=0
I00000003 rcv latest_rtt=3 min_rtt=3 smoothed_rtt=3 rttvar=1 ack_delay=0
I00000003 rcv pkn=0 acked, slow start cwnd=13552
I00000003 con path is not ECN capable
I00000003 rcv loss_detection_timer=691117593405339 last_tx_pkt_ts=691117583537024 timeout=9
I00000003 frm rx 1 Initial(0x00) CRYPTO(0x06) offset=0 len=90
I00000003 frm rx 1 Initial(0x00) PADDING(0x00) len=1081
I00000003 pkt read packet 1232 left 0
www.google.com
からパケットを受信しました。ECNはサポートされていないようですね。InitialパケットのACKフレームで、0..0の範囲(両端を含む)のパケットを受信したと回答しています。つまり、クライアントが最初に送信したInitialパケット0がackされたということですね。CRYPTOフレームに含まれるのはServerHelloになります。www.google.com
はInitialとHandshakeパケットを一つのUDP datagramに結合せずそれぞれ送るようですね。Initialパケットのえげつないパディングルールを考えると理解できる実装です。サーバーもCRYPTOフレームを含むInitialパケットは最低1200バイトまで拡張する必要があるのでPADDINGフレームが使用されています。
I00000004 pkt tx pkn=1 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Initial(0x00) len=0 k=0
I00000004 frm tx 1 Initial(0x00) ACK(0x02) largest_ack=1 ack_delay=0(0) ack_block_count=0
I00000004 frm tx 1 Initial(0x00) ACK(0x02) block=[1..1] block_count=0
I00000004 frm tx 1 Initial(0x00) PADDING(0x00) len=1175
I00000004 rcv loss_detection_timer=691117593405339 last_tx_pkt_ts=691117583537024 timeout=9
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
クライアントはInitialパケットでサーバーのInitialパケット1を受信したことを伝えるためにACKフレームを送信します。これでクライアントはHandshakeパケットを送受信するための鍵を得たのでInitialパケットはもう使いません。
I00000026 rcv loss detection timer fired
I00000026 rcv pto_count=1
I00000026 rcv loss_detection_timer=691117603273654 last_tx_pkt_ts=691117583537024 timeout=19
I00000026 pkt tx pkn=0 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Handshake(0x02) len=0 k=0
I00000026 frm tx 0 Handshake(0x02) PING(0x01)
I00000026 frm tx 0 Handshake(0x02) PADDING(0x00) len=1180
I00000026 rcv loss_detection_timer=691117603273654 last_tx_pkt_ts=691117583537024 timeout=19
I00000026 rcv loss_detection_timer=691117619495319 last_tx_pkt_ts=691117609627004 timeout=9
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000037 rcv loss detection timer fired
I00000037 rcv pto_count=1
I00000037 rcv loss_detection_timer=691117640021098 last_tx_pkt_ts=691117620284468 timeout=19
I00000037 pkt tx pkn=1 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Handshake(0x02) len=0 k=0
I00000037 frm tx 1 Handshake(0x02) PING(0x01)
I00000037 frm tx 1 Handshake(0x02) PADDING(0x00) len=2
I00000037 rcv loss_detection_timer=691117640088835 last_tx_pkt_ts=691117620352205 timeout=19
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 54 bytes
I00000058 rcv loss detection timer fired
I00000058 rcv pto_count=2
I00000058 rcv loss_detection_timer=691117681336573 last_tx_pkt_ts=691117641863313 timeout=39
I00000058 pkt tx pkn=2 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Handshake(0x02) len=0 k=0
I00000058 frm tx 2 Handshake(0x02) PING(0x01)
I00000058 frm tx 2 Handshake(0x02) PADDING(0x00) len=2
I00000058 rcv loss_detection_timer=691117681405416 last_tx_pkt_ts=691117641932156 timeout=39
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 54 bytes
クライアントがHandshakeパケットを3個送信していますが、これはサーバーからの返答がRTTから予想するよりも遅いため、PTOタイマーが3回発火し、規定どおりサーバーがackしなければならないようなHandshakeパケットを送信しています。サーバーはHandshakeパケットを受け取れば、クライアントのアドレスの検証をおえることができ、amplification limitを解除できるのでもしもそこで止まっているのであればこれで先にすすむことができます。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000068 con recv packet len=1232
I00000068 pkt rx pkn=2 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=1198 k=0
I00000068 frm rx 2 Handshake(0x02) CRYPTO(0x06) offset=0 len=1177
I00000068 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000088 con recv packet len=1232
I00000088 pkt rx pkn=3 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=1198 k=0
I00000088 frm rx 3 Handshake(0x02) CRYPTO(0x06) offset=1177 len=1176
I00000088 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000096 con recv packet len=1232
I00000096 pkt rx pkn=4 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=1198 k=0
I00000096 frm rx 4 Handshake(0x02) CRYPTO(0x06) offset=2353 len=1176
I00000096 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 357 bytes
I00000102 con recv packet len=357
I00000102 pkt rx pkn=5 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=323 k=0
I00000102 frm rx 5 Handshake(0x02) CRYPTO(0x06) offset=3529 len=301
I00000102 pkt read packet 357 left 0
サーバーからのHandshakeパケットが到着しました。HandshakeパケットのCRYPTOフレームにはEncryptedExtensions、Server Finishedが含まれます。証明書が入っていて大きいので、4 UDP datagramに分けて送信されています。全部で3830バイトです。
サーバーが送ってきたquic_transport_parameters extensionは以下のようになっています:
I00000102 cry remote transport_parameters stateless_reset_token=0x14d37b8e9e7c07a47a9b655b0043fd91
I00000102 cry remote transport_parameters original_destination_connection_id=0x79a9a0491ffc7e17ccc7310e9543c5e75645
I00000102 cry remote transport_parameters initial_source_connection_id=0x79a9a0491ffc7e17
I00000102 cry remote transport_parameters initial_max_stream_data_bidi_local=131072
I00000102 cry remote transport_parameters initial_max_stream_data_bidi_remote=131072
I00000102 cry remote transport_parameters initial_max_stream_data_uni=131072
I00000102 cry remote transport_parameters initial_max_data=196608
I00000102 cry remote transport_parameters initial_max_streams_bidi=100
I00000102 cry remote transport_parameters initial_max_streams_uni=103
I00000102 cry remote transport_parameters max_idle_timeout=300000
I00000102 cry remote transport_parameters max_udp_payload_size=1472
I00000102 cry remote transport_parameters ack_delay_exponent=3
I00000102 cry remote transport_parameters max_ack_delay=25
I00000102 cry remote transport_parameters active_connection_id_limit=2
I00000102 cry remote transport_parameters disable_active_migration=1
QUICとTLSスタックの実装依存かもしれませんが、このデータをTLSスタックに与えるとClient Finishedを生成してTLSハンドシェーク完了となります。
QUIC handshake has completed
Negotiated cipher suite is TLS_AES_128_GCM_SHA256
Negotiated ALPN is h3-29
ALPNはh3-29、HTTP/3が有効になりました。
HTTP/3では、QPACKでHTTPヘッダーフィールドの圧縮を行いますが、QPACKエンコーダー、QPACKデコーダーそれぞれでunidirectional streamをひとつづつ専有します。HTTP/3ではコントロールストリームと称してunidirectional streamを使うので、最低でも3個のunidirectional streamを使います。どのストリームがどの役割になっているかはストリームの最初のバイトで識別することになっています。クライアントは以下のようにストリームを割り振ります:
http: control stream=2
http: QPACK streams encoder=6 decoder=a
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 80 bytes
I00000106 con recv packet len=80
I00000106 con buffering Short packet len=80
I00000106 pkt read packet 80 left 0
Shortパケットとよばれるパケットを受信したが、クライアントは実装上の都合上一度バッファリングしたということになっています。また後ででてきます。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000106 con recv packet len=1232
I00000106 pkt rx pkn=8 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=1198 k=0
I00000106 frm rx 8 Handshake(0x02) CRYPTO(0x06) offset=0 len=1177
I00000106 pkt read packet 1232 left 0
HandshakeパケットでCRYPTOデータの内容が再送されています。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000106 con recv packet len=1232
I00000106 pkt rx pkn=10 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x79a9a0491ffc7e17 type=Handshake(0x02) len=1198 k=0
I00000106 frm rx 10 Handshake(0x02) CRYPTO(0x06) offset=1177 len=1176
I00000106 pkt read packet 1232 left 0
これも再送ですね。とりあえず先に進みましょう。
I00000106 pkt tx pkn=3 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Handshake(0x02) len=0 k=0
I00000106 frm tx 3 Handshake(0x02) ACK(0x02) largest_ack=10 ack_delay=0(0) ack_block_count=2
I00000106 frm tx 3 Handshake(0x02) ACK(0x02) block=[10..10] block_count=0
I00000106 frm tx 3 Handshake(0x02) ACK(0x02) block=[8..8] gap=0 block_count=0
I00000106 frm tx 3 Handshake(0x02) ACK(0x02) block=[5..2] gap=1 block_count=3
I00000106 frm tx 3 Handshake(0x02) CRYPTO(0x06) offset=0 len=36
I00000106 rcv loss_detection_timer=691117729539486 last_tx_pkt_ts=691117690066226 timeout=39
HandshakeパケットでClient Finishedを送信します。サーバーに送信するACKですが、一部不連続になっています。わざと不連続にしているのかパケットがロスしたのかは定かではないですが、データはすべて受け取っているので問題ないでしょう。
I00000106 pkt rx pkn=6 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000106 frm rx 6 Short(0x70) STREAM(0x08) id=0x3 fin=0 offset=0 len=43 uni=1
先程バッファリングしたShortパケットを処理します。ストリーム3はサーバーのコントロールストリームです。HTTP/3のSETTINGSフレームなどが受信されていると思われます。uni=1
となっているのはunidirectional streamで一方向のストリームです。uni=0
となっているのはbidirectional streamで双方向ストリームです。
さてハンドシェークが終了したのでHTTPリクエストを送信します。以下のようなHTTPリクエストを送信します:
http: stream 0x0 submit request headers
[:method: GET]
[:scheme: https]
[:authority: www.google.com]
[:path: /]
[user-agent: nghttp3/ngtcp2 client]
I00000106 pkt tx pkn=0 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Short(0x70) len=0 k=0
I00000106 frm tx 0 Short(0x70) ACK(0x02) largest_ack=6 ack_delay=0(22) ack_block_count=0
I00000106 frm tx 0 Short(0x70) ACK(0x02) block=[6..6] block_count=0
I00000106 frm tx 0 Short(0x70) NEW_CONNECTION_ID(0x18) seq=1 cid=0x2363ae74b26945636434f43f59be79b014 retire_prior_to=0 stateless_reset_token=0x5ee1770a46b6ecab8feb8fbeab3fd7b1
I00000106 frm tx 0 Short(0x70) STREAM(0x0a) id=0x2 fin=0 offset=0 len=22 uni=1
I00000107 frm tx 0 Short(0x70) STREAM(0x0a) id=0xa fin=0 offset=0 len=1 uni=1
I00000107 frm tx 0 Short(0x70) STREAM(0x0a) id=0x6 fin=0 offset=0 len=1 uni=1
I00000107 frm tx 0 Short(0x70) STREAM(0x0e) id=0x6 fin=0 offset=1 len=35 uni=1
I00000107 frm tx 0 Short(0x70) STREAM(0x0b) id=0x0 fin=1 offset=0 len=9 uni=0
I00000107 rcv loss_detection_timer=691117729539486 last_tx_pkt_ts=691117690066226 timeout=39
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 251 bytes
NEW_CONNECTION_IDフレームは、ネットワークパスが変化したとき等で新たなConnection IDが必要になったときにそなえて、サーバー側にあらかじめ送信しておくConnection IDです。サーバーは状況に応じて、これをDestination Connection IDとして使用します。今回は無視しましょう。
ストリームが複数ありますが、ストリーム2はコントロールストリームでHTTP/3 SETTINGSフレームを送信します。ストリームa(=10)は、QPACKデコーダーストリームで、何も送るデータがないのでストリームタイプだけ送信します。ストリーム6はQPACKエンコーダーストリームで、実装の都合上2つのフレームにわかれていますが、HTTPリクエストのヘッダー圧縮ためにダイナミックテーブルを操作するための命令が含まれます。ストリーム0が本命のHTTPリクエストのストリームになります。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 629 bytes
I00000113 con recv packet len=629
I00000113 pkt rx pkn=11 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000113 frm rx 11 Short(0x70) CRYPTO(0x06) offset=0 len=590
サーバーから受信したShortパケットのCRYPTOフレームには、TLSのセッションチケットが含まれます。次回resumptionを行うときに使用します。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 43 bytes
I00000116 con recv packet len=43
I00000116 pkt rx pkn=12 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000116 frm rx 12 Short(0x70) HANDSHAKE_DONE(0x1e)
I00000116 rcv loss_detection_timer=691117725742313 last_tx_pkt_ts=691117690873998 timeout=34
QUIC handshake has been confirmed
I00000116 rcv loss_detection_timer=691117725742313 last_tx_pkt_ts=691117690873998 timeout=34
I00000116 frm rx 12 Short(0x70) PADDING(0x00) len=6
I00000116 pkt read packet 43 left 0
続いて、HANDSHAKE_DONEフレームを受信しました。これはサーバーがクライアントが送信したClient Finishedを受け取りハンドシェークが完了したこと示すものであり、ここで晴れてクライアントはサーバーが確かにハンドシェークを完了したという確証を得ることができたわけです。これより以降、Handshakeパケットは不要となります。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 44 bytes
I00000116 con recv packet len=44
I00000116 pkt rx pkn=13 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000116 frm rx 13 Short(0x70) ACK(0x02) largest_ack=0 ack_delay=0(0) ack_block_count=0
I00000116 frm rx 13 Short(0x70) ACK(0x02) block=[0..0] block_count=0
I00000116 rcv latest_rtt=9 min_rtt=3 smoothed_rtt=4 rttvar=2 ack_delay=0
I00000116 rcv pkn=0 acked, slow start cwnd=13704
I00000116 rcv loss detection timer canceled
I00000116 frm rx 13 Short(0x70) STREAM(0x08) id=0x7 fin=0 offset=0 len=2 uni=1
I00000116 pkt read packet 44 left 0
さらに続いて、サーバーからShortパケットのACKとストリーム7が送信されてきました。ストリーム7はサーバーのQPACKデコーダーストリームになります。ストリーム0のHTTPヘッダーフィールドの処理が完了したと伝えています。
I00000117 pkt tx pkn=1 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Short(0x70) len=0 k=0
I00000117 frm tx 1 Short(0x70) ACK(0x02) largest_ack=13 ack_delay=0(27) ack_block_count=0
I00000117 frm tx 1 Short(0x70) ACK(0x02) block=[13..11] block_count=2
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 31 bytes
クライアントはShortパケットでACKを送信します。サーバーからのHTTPレスポンスを待っています。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1228 bytes
I00000207 con recv packet len=1228
I00000207 pkt rx pkn=14 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000207 frm rx 14 Short(0x70) STREAM(0x0a) id=0xb fin=0 offset=0 len=579 uni=1
I00000207 frm rx 14 Short(0x70) STREAM(0x08) id=0x0 fin=0 offset=0 len=608 uni=0
I00000207 pkt read packet 1228 left 0
ストリームb(=11)はサーバーのQPACKエンコーダーストリームです。サーバーはストリーム0でHTTPレスポンスを送信します。以下複数パケットに渡ってレスポンスを受信しますが、HTTPレスポンスヘッダーは以下のようになっています:
http: stream 0x0 response headers started
http: stream 0x0 [:status: 200]
http: stream 0x0 [date: Sun, 20 Dec 2020 08:34:46 GMT]
http: stream 0x0 [expires: -1]
http: stream 0x0 [cache-control: private, max-age=0]
http: stream 0x0 [content-type: text/html; charset=ISO-8859-1]
http: stream 0x0 [p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."]
http: stream 0x0 [server: gws]
http: stream 0x0 [x-xss-protection: 0]
http: stream 0x0 [x-frame-options: SAMEORIGIN]
http: stream 0x0 [set-cookie: 1P_JAR=2020-12-20-08; expires=Tue, 19-Jan-2021 08:34:46 GMT; path=/; domain=.google.com; Secure]
http: stream 0x0 [set-cookie: NID=******; expires=Mon, 21-Jun-2021 08:34:46 GMT; path=/; domain=.google.com; HttpOnly]
http: stream 0x0 [alt-svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"]
http: stream 0x0 [accept-ranges: none]
http: stream 0x0 [vary: Accept-Encoding]
http: stream 0x0 headers ended
以下パケットの受信が続きます。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000225 pkt rx pkn=15 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000225 frm rx 15 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=608 len=1193 uni=0
I00000225 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000247 con recv packet len=1232
I00000247 pkt rx pkn=16 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000247 frm rx 16 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=1801 len=1193 uni=0
I00000247 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000268 con recv packet len=1232
I00000268 pkt rx pkn=17 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000268 frm rx 17 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=2994 len=1193 uni=0
I00000268 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000294 con recv packet len=1232
I00000294 pkt rx pkn=18 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000294 frm rx 18 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=4187 len=1193 uni=0
I00000294 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000316 con recv packet len=1232
I00000316 pkt rx pkn=19 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000316 frm rx 19 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=5380 len=1193 uni=0
I00000316 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000336 con recv packet len=1232
I00000336 pkt rx pkn=20 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000336 frm rx 20 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=6573 len=1193 uni=0
I00000336 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000359 con recv packet len=1232
I00000359 pkt rx pkn=21 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000359 frm rx 21 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=7766 len=1193 uni=0
I00000359 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000381 con recv packet len=1232
I00000381 pkt rx pkn=22 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000381 frm rx 22 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=8959 len=1193 uni=0
I00000381 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000428 con recv packet len=1232
I00000428 pkt rx pkn=23 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000428 frm rx 23 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=10152 len=1193 uni=0
I00000428 pkt read packet 1232 left 0
とHTTPレスポンスはまだ途中ですが
I00000453 pkt tx pkn=2 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Short(0x70) len=0 k=0
I00000453 frm tx 2 Short(0x70) ACK(0x02) largest_ack=23 ack_delay=24(3096) ack_block_count=0
I00000453 frm tx 2 Short(0x70) ACK(0x02) block=[23..11] block_count=12
I00000453 frm tx 2 Short(0x70) STREAM(0x0e) id=0xa fin=0 offset=1 len=1 uni=1
I00000453 rcv loss_detection_timer=691118076347357 last_tx_pkt_ts=691118036396757 timeout=39
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 37 bytes
クライアントがACKを送信しています。QPACKデコーダーストリームであるストリームaでヘッダー処理完了を通知。
以後サーバーからのパケット受信が続きます。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 971 bytes
I00000453 con recv packet len=971
I00000453 pkt rx pkn=24 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000453 frm rx 24 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=11345 len=932 uni=0
I00000453 pkt read packet 971 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 251 bytes
I00000461 con recv packet len=251
I00000461 pkt rx pkn=25 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000461 frm rx 25 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=12277 len=212 uni=0
I00000461 pkt read packet 251 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1227 bytes
I00000463 con recv packet len=1227
I00000463 pkt rx pkn=26 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000463 frm rx 26 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=12489 len=1188 uni=0
I00000463 pkt read packet 1227 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 459 bytes
I00000473 con recv packet len=459
I00000473 pkt rx pkn=27 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000473 frm rx 27 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=13677 len=420 uni=0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 247 bytes
I00000477 con recv packet len=247
I00000477 pkt rx pkn=28 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000477 frm rx 28 Short(0x70) STREAM(0x0f) id=0x0 fin=1 offset=14097 len=0 uni=0
Ordered STREAM data stream_id=0x0
HTTP stream 0 closed with error code 256
I00000477 frm rx 28 Short(0x70) PADDING(0x00) len=207
I00000477 pkt read packet 247 left 0
これでHTTPレスポンスの受信が完了です。14097バイトですね。
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1228 bytes
I00000477 con recv packet len=1228
I00000477 pkt rx pkn=30 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000477 frm rx 30 Short(0x70) STREAM(0x0a) id=0xb fin=0 offset=0 len=579 uni=1
I00000477 frm rx 30 Short(0x70) STREAM(0x08) id=0x0 fin=0 offset=0 len=608 uni=0
I00000477 pkt read packet 1228 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000477 con recv packet len=1232
I00000477 pkt rx pkn=32 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000477 frm rx 32 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=608 len=1193 uni=0
I00000477 pkt read packet 1232 left 0
Received packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 976 bytes
I00000477 con recv packet len=976
I00000477 pkt rx pkn=34 dcid=0xdca9c3f5c3b52a64b2628b80af36c87577 scid=0x type=Short(0x70) len=0 k=0
I00000477 frm rx 34 Short(0x70) ACK(0x02) largest_ack=2 ack_delay=0(0) ack_block_count=0
I00000477 frm rx 34 Short(0x70) ACK(0x02) block=[2..0] block_count=2
I00000477 rcv latest_rtt=24 min_rtt=3 smoothed_rtt=6 rttvar=7 ack_delay=0
I00000477 rcv pkn=2 acked, slow start cwnd=13741
I00000477 rcv target_cwnd=35604 max_delivery_rate_sec=1356 min_rtt=3289439
I00000477 rcv loss detection timer canceled
I00000477 frm rx 34 Short(0x70) STREAM(0x0c) id=0x0 fin=0 offset=11345 len=932 uni=0
I00000477 pkt read packet 976 left 0
サーバーから再送がありましたが、すべて受信済みのデータです。
I00000477 pkt tx pkn=3 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Short(0x70) len=0 k=0
I00000477 frm tx 3 Short(0x70) ACK(0x02) largest_ack=34 ack_delay=0(14) ack_block_count=3
I00000477 frm tx 3 Short(0x70) ACK(0x02) block=[34..34] block_count=0
I00000477 frm tx 3 Short(0x70) ACK(0x02) block=[32..32] gap=0 block_count=0
I00000477 frm tx 3 Short(0x70) ACK(0x02) block=[30..30] gap=0 block_count=0
I00000477 frm tx 3 Short(0x70) ACK(0x02) block=[28..24] gap=0 block_count=4
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 37 bytes
クライアントはACKを送信します。
I00007473 pkt tx pkn=4 dcid=0x79a9a0491ffc7e17 scid=0xdca9c3f5c3b52a64b2628b80af36c87577 type=Short(0x70) len=0 k=0
I00007473 frm tx 4 Short(0x70) CONNECTION_CLOSE(0x1c) error_code=NO_ERROR(0x0) frame_type=0 reason_len=0 reason=[]
Sent packet: local=[::]:45540 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 30 bytes
最後にCONNECTION_CLOSEフレームを送信して接続を切ります。
0RTTで優勝していく
www.google.com
は0RTTが利用可能か調べました。0RTTでデータを送るには、セッションチケットをサーバーから得る必要があります。ngtcp2 clientでは、--session-fileでセッションチケットを保存するファイル名を指定します。quic_transport_parametersの一部も覚えておく必要があるので、--tp-fileで保存するファイルを指定します。
$ examples/client www.google.com 443 https://www.google.com/ --session-file session.txt --tp-file tp.txt
一度上記を実行し、セッションチケットとquic_transport_parametersをファイルに保存します。再度同じコマンドを実行すると、0RTTでHTTPリクエストを送信します:
http: control stream=2
http: QPACK streams encoder=6 decoder=a
http: stream 0x0 submit request headers
[:method: GET]
[:scheme: https]
[:authority: www.google.com]
[:path: /]
[user-agent: nghttp3/ngtcp2 client]
I00000001 pkt tx pkn=0 dcid=0x12e2bf61abf71ba2d27beceb3f36e0db01bd scid=0x4865c090d41a5d012f15eac7688f5aaa7e type=Initial(0x00) len=0 k=0
I00000001 frm tx 0 Initial(0x00) CRYPTO(0x06) offset=0 len=670
I00000001 rcv loss_detection_timer=710924755483460 last_tx_pkt_ts=710923756483460 timeout=999
I00000001 pkt tx pkn=0 dcid=0x12e2bf61abf71ba2d27beceb3f36e0db01bd scid=0x4865c090d41a5d012f15eac7688f5aaa7e type=0RTT(0x01) len=0 k=0
I00000001 frm tx 0 0RTT(0x01) STREAM(0x0a) id=0x2 fin=0 offset=0 len=22 uni=1
I00000003 frm tx 0 0RTT(0x01) STREAM(0x0a) id=0xa fin=0 offset=0 len=1 uni=1
I00000003 frm tx 0 0RTT(0x01) STREAM(0x0a) id=0x6 fin=0 offset=0 len=1 uni=1
I00000003 frm tx 0 0RTT(0x01) STREAM(0x0b) id=0x0 fin=1 offset=0 len=38 uni=0
I00000003 frm tx 0 0RTT(0x01) PADDING(0x00) len=361
I00000003 rcv loss_detection_timer=710924755483460 last_tx_pkt_ts=710923756483460 timeout=999
Sent packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x2 1232 bytes
一つのUDP datagramにInitialパケットと0RTTパケットを同居させます。0RTTパケットには複数のSTREAMフレームがありますが、基本的に前項で記載したものと同じで、ストリーム2がHTTP/3のコントロールストリーム、ストリームaがQPACKデコーダーストリーム、ストリーム6がQPACKのエンコーダーストリーム、ストリーム0がHTTPリクエストのストリームです。HTTP/3のSETTINGSの内容は覚えておらず、サーバーの値が分からないのでQPACKのダイナミックテーブルは未使用です。
Received packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000029 con recv packet len=1232
I00000029 pkt rx pkn=1 dcid=0x4865c090d41a5d012f15eac7688f5aaa7e scid=0x12e2bf61abf71ba2 type=Initial(0x00) len=1197 k=0
I00000029 frm rx 1 Initial(0x00) ACK(0x02) largest_ack=0 ack_delay=0(0) ack_block_count=0
I00000029 frm rx 1 Initial(0x00) ACK(0x02) block=[0..0] block_count=0
I00000029 rcv latest_rtt=27 min_rtt=27 smoothed_rtt=27 rttvar=13 ack_delay=0
I00000029 rcv pkn=0 acked, slow start cwnd=13056
I00000029 con path is not ECN capable
I00000029 rcv loss_detection_timer=710923840408191 last_tx_pkt_ts=710923756483460 timeout=83
I00000029 frm rx 1 Initial(0x00) PADDING(0x00) len=1174
I00000029 pkt read packet 1232 left 0
サーバーはInitialパケットでACKを送信し、クライアントのパケットをackしています。細かい部分は前項と同じなので省略します。
サーバーのServerHelloを含むInitialパケットまで飛ばします。
Received packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1232 bytes
I00000144 con recv packet len=1232
I00000144 pkt rx pkn=3 dcid=0x4865c090d41a5d012f15eac7688f5aaa7e scid=0x12e2bf61abf71ba2 type=Initial(0x00) len=1197 k=0
I00000144 frm rx 3 Initial(0x00) CRYPTO(0x06) offset=0 len=96
I00000144 frm rx 3 Initial(0x00) PADDING(0x00) len=1080
I00000144 pkt read packet 1232 left 0
I00000144 con processing buffered handshake packet
サーバーのInitialパケットでServerHelloを受信しました。
Received packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 256 bytes
I00000145 con recv packet len=256
I00000145 pkt rx pkn=4 dcid=0x4865c090d41a5d012f15eac7688f5aaa7e scid=0x12e2bf61abf71ba2 type=Handshake(0x02) len=222 k=0
I00000145 frm rx 4 Handshake(0x02) CRYPTO(0x06) offset=0 len=201
ついでサーバーからHandshakeパケットでEncrypted Extensions、Finishedなどを受信します。証明書がないので201バイトとフルハンドシェークに比べて格段に小さくなります。
QUIC handshake has completed
Negotiated cipher suite is TLS_AES_128_GCM_SHA256
Negotiated ALPN is h3-29
ここでハンドシェークが完了。
重要なのが0RTTパケットをサーバーがackするかどうかですが、
Received packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 43 bytes
I00000151 con recv packet len=43
I00000151 pkt rx pkn=7 dcid=0x4865c090d41a5d012f15eac7688f5aaa7e scid=0x type=Short(0x70) len=0 k=0
I00000151 frm rx 7 Short(0x70) ACK(0x02) largest_ack=0 ack_delay=64(8032) ack_block_count=0
I00000151 frm rx 7 Short(0x70) ACK(0x02) block=[0..0] block_count=0
I00000151 rcv latest_rtt=148 min_rtt=2 smoothed_rtt=32 rttvar=27 ack_delay=64
I00000151 rcv pkn=0 acked, slow start cwnd=14784
I00000151 rcv target_cwnd=35604 max_delivery_rate_sec=14537 min_rtt=2321948
I00000151 rcv loss_detection_timer=710924044616886 last_tx_pkt_ts=710923902158286 timeout=142
I00000151 frm rx 7 Short(0x70) HANDSHAKE_DONE(0x1e)
I00000151 rcv loss_detection_timer=710924069616886 last_tx_pkt_ts=710923902158286 timeout=167
QUIC handshake has been confirmed
I00000151 rcv loss_detection_timer=710924069616886 last_tx_pkt_ts=710923902158286 timeout=167
I00000151 pkt read packet 43 left 0
サーバーはShortパケット7のACKフレームで0RTTパケットをackしています。QUICでは0RTTパケットはShortパケットでackする決まりになっています。これでサーバーが0RTTを受け入れたということになります。
Received packet: local=[::]:39676 remote=[2404:6800:4004:81a::2004]:443 ecn=0x0 1228 bytes
I00000282 con recv packet len=1228
I00000282 pkt rx pkn=8 dcid=0x4865c090d41a5d012f15eac7688f5aaa7e scid=0x type=Short(0x70) len=0 k=0
I00000282 frm rx 8 Short(0x70) STREAM(0x0a) id=0xb fin=0 offset=0 len=579 uni=1
I00000282 frm rx 8 Short(0x70) STREAM(0x08) id=0x0 fin=0 offset=0 len=608 uni=0
0RTTで送信したHTTPリクエストに対するHTTPレスポンスがサーバーから送信されていることがわかります。
ngtcp2クライアントを使ってみたいですか?
ngtcp2にビルドの方法が記載されています。
Dockerが分かる人ならdocker buildするのがてっとり速いでしょう。