通過QUIC協定,來看看怎麼學習網路協定

2022-03-01 10:00:07
本篇文章帶大家瞭解一下QUIC協定,並以QUIC協定為例,來聊聊如何學習網路協定,希望對大家有所幫助!

在之前釋出的關於 s2n-quic 的文章中,有讀者問我如何學習像 QUIC 這樣的網路協定。對於大部分網際網路從業者來說,雖然大家每天都在跟網路打交道,但很少有人會(需要)關心 HTTP 之下的網路協定的細節,大部分時候,瞭解個大概,知道如何使用就可以了。如果你對 QUIC 一點概念都沒有,那麼,下面這個圖能幫助你很好地瞭解 QUIC 在 HTTP/3 生態中的地位:

1.png

那麼,如果你就是要詳盡地瞭解一下 QUIC 的知識,該如何入手呢?

作為一個曾經的網路協定和網路裝置的開發者,我自己的心得是:從 RFC 入手,輔以 wireshark 抓包,來快速掌握目標協定。

對於 QUIC 而言,我們首先需要閱讀的是 RFC9000。協定的閱讀是非常枯燥的事情,需要一定的耐心,如果英文不太好,可以用 google translate 將其翻譯成中文,快速瀏覽一番(泛讀)。第一遍閱讀主要了解裡面的主要概念,以及主要流程。

之後,我們就可以撰寫使用 QUIC 協定的程式,然後通過 wireshark 抓包,通過研究實際的報文,對比 RFC 協定中的內容(精讀),來更深入地理解協定的本質。

我們還是以上一篇文章中的程式碼為基礎,構建 echo 使用者端和伺服器。為方便大家閱讀,我把程式碼也貼上來。感興趣的同學可以自行 clone 我的 repo,並執行 client/server 程式碼。

使用者端程式碼(見 github: tyrchen/rust-training/live_coding/quic-test/examples/client.rs):

2.png

伺服器端程式碼(見 github: tyrchen/rust-training/live_coding/quic-test/examples/server.rs):

3.png

這兩段程式碼構建了一個最簡單的 echo server。我們可以使用 wireshark 監聽本地 loopback 介面下的 UDP 包,進行抓包。要注意的是,對於 QUIC 這樣使用了 TLS 協定的流量,即便抓到了包,可能只有頭幾個包可讀,後續的包都是加密內容,無法閱讀。因此,我們在構建 client/server 時,需要想辦法把伺服器和使用者端之間協商出來的 session key 抓取下來,供 wireshark 解密使用。一般 SSL/TLS 庫都會提供這個功能。比如對於 Rustls,我們可以在 tls config 中使用 key_log。如果你仔細看上面 server 的程式碼,會看到這句:

let config = Builder::new()
    .with_certificate(CERT_PEM, KEY_PEM)?
    .with_key_logging()? # 使能 keylogging
    .build()?;

使用了 key_log 後,在啟動 server 的時候,我們只需要指定 SSLKEYLOGFILE 就可以了:

SSLKEYLOGFILE=session.log cargo run --example server

在抓包完成後,開啟 wireshark 的 preference,選擇 TLS 協定,把 log 的路徑放進去就可以了:

4.png

以下是一次完整的使用者端和伺服器的互動的抓包,我們看到,所有 「protected payload」 都被正常顯示出來了:

5.png

因為我們的 echo client 只做了最簡單的動作(只開了一個 bidirectional stream),所以通過這個抓包,我們重點可以研究 QUIC 協定建立連線的過程。

使用者端傳送的首包

我們看使用者端發的第一個報文:

6.png

這個報文包含了非常豐富的資訊。首先,和 TCP 握手不同的是,QUIC 的首包非常大,有 1200 位元組之多(協定要求 UDP payload at least 1200 bytes),包含 QUIC 頭,一個 255 位元組的 CRYPTO frame,以及 890 位元組 PADDING frame。從 header 可以看到,這個 QUIC 包的型別是 Initial。

QUIC 報文型別

對於 QUIC 包來說,Header 是明文,之後的所有 frame payload 都是密文(除了頭幾個包)。我們看到這個首包是一個 Long Header 報文,在 RFC9000 的 17.2 節中,定義了 Long Header Packet:

Long Header Packet {
   Header Form (1) = 1,
   Fixed Bit (1) = 1,
   Long Packet Type (2),
   Type-Specific Bits (4),
   Version (32),
   Destination Connection ID Length (8),
   Destination Connection ID (0..160),
   Source Connection ID Length (8),
   Source Connection ID (0..160),
   Type-Specific Payload (..),
 }

感興趣的可以自行去閱讀 RFC 相應的章節。對於 Long Header 報文,有如下幾種型別:

7.png

既然有 Long Header packet,那麼就有 Short Header packet,Short Header packet 目前的版本只有一種:

1-RTT Packet {
   Header Form (1) = 0,
   Fixed Bit (1) = 1,
   Spin Bit (1),
   Reserved Bits (2),
   Key Phase (1),
   Packet Number Length (2),
   Destination Connection ID (0..160),
   Packet Number (8..32),
   Packet Payload (8..),
}

為什麼需要 connection id?

在我們捕獲的這個報文頭中,我們看到有 Source Connection ID(SCID)和 Destination Connection ID(DCID)這個新的概念。你也許會好奇:QUIC 不是基於 UDP/IP 的協定麼?底層的協定已經有五元組(src ip / src port / dst ip / dst port / protocol)來描述一個連線(connection),為什麼還需要 connection id 這樣一個新的概念?

這是為了適應越來越多的移動場景。有了 QUIC 層自己的 connection id,底層網路(UDP/IP)的變化,並不會引發 QUIC 連線的中斷,也就是說,你從家裡開車出門,即便手機的網路從 WIFI(固網運營商分配給你的 IP)切換到蜂窩網路(行動業者分配給你的 IP),整個 UDP/IP 網路變化了,但你的 QUIC 應用只會感受到細微的延遲,並不需要重新建立 QUIC 連線。

從這個使用場景來看,QUIC 底層使用無連線的 UDP 是非常必要的。

首包中就包含了 TLS hello?

我們接下來看看 CRYPTO frame:

8.png

可以看到,QUIC 在建立連線的首包就把 TLS Client Hello 囊括在 CRYPTO frame 中。並且使用的 TLS版本是 1.3。在 Client Hello 的 extension 中,QUIC 協定使用了一個 quic_transport_parameters 的 extension,用來協商 QUIC 自己的一些初始值,比如支援多少個 stream,這個連線中可以最多使用多少個 active connection id 等等。

QUIC 支援哪些 frame?

現在我們已經見到了兩種 Frame:CRYPTO 和 PADDING。下表中羅列了 QUIC 所有支援的 frame:

9.png

伺服器的回包

我們來看 server 的回包:

10.png

這裡有一些新東西。首先,一個 UDP 包內部可以包含若干個 QUIC payload,我們看到 server 回覆了一個 QUIC Initial 報文和一個 QUIC Handshake 報文。在 Initial 報文中,我們看到了一個 ACK frame,可見 QUIC 雖然構建於 UDP,但在 QUIC 協定內部構建了類似 TCP 的確認機制。

我們之前看到,在 Initial 報文的 CRYPTO frame 中,使用者端傳送了 TLS Client Hello,同樣的,伺服器在 Initial 報文的 CRYPTO frame 中傳送了 TLS Server Hello。這個我們就略過不看。

在 Handshake 報文中:

11.png

伺服器傳送了自己的證書,並結束了 TLS handshake。

使用者端結束 Handshake

我們再看第三個包,使用者端傳送給伺服器結束 TLS 握手:

12.png

這個包依舊包含兩個 QUIC 報文,其中第一個就是一個 ACK frame,來確認收到了伺服器的 Server Hello 那個 QUIC 報文;第二個包含一個 ACK frame,確認伺服器的 Handshake,隨後有一個 CRYPTO frame 結束使用者端的 TLS handshake。

TLS 握手結束之後,使用者端和伺服器就開始應用層的資料交換,此刻,所有資料都是加密的。

使用者端傳送一個 「hello」 文字

在我們的 echo client/server 程式碼中,使用者端連線到伺服器後,就可以等待使用者在 stdin 的輸入,然後將其傳送到伺服器。伺服器收到使用者端資料,原封不動發回,使用者端再將其顯示到 stdout 上。在這個過程的前後,使用者端和伺服器間有一些用於連線管理的 QUIC 報文,比如 PING。我們就略過,只看傳送應用層資料的報文。下圖是使用者端傳送的包含 「hello」 文字的報文:

13.png

可以看到,這裡 QUIC 報文是個 Short Header packet,除了 ACK frame 外,它還有一個 STREAM frame。這個 stream 的 stream ID 最低兩位是 00,代表是使用者端發起的,雙向的 stream。由於使用了兩位來表述型別,所以 QUIC 的 stream 有如下型別:

14.png

我們看 STREAM frame 的 length(6) 和 Data(68 65 6c 6c 6f 0a)。Data 裡的內容如果用 ASCII 表示,正好是 「hello<LF>」,它的長度是 6 個位元組。

伺服器回覆 「hello」 文字

最後是伺服器 echo back:

15.png

這個和上面的報文如出一轍,就不解釋了。

賢者時刻

相信通過上面對照著 wireshark 抓包進行的 QUIC 簡介,能讓你對 QUIC 協定有一個初步的認識。上篇文章,我們說 QUIC 支援多路複用,並且解決了傳輸層隊頭阻塞的問題。通過這篇文章的介紹,你能回答以下兩個問題麼?

  • QUIC 通過哪個 frame 型別來做多路複用的?

  • QUIC 如何解決傳輸層隊頭阻塞的?

相關推薦:

以上就是通過QUIC協定,來看看怎麼學習網路協定的詳細內容,更多請關注TW511.COM其它相關文章!