首頁  >  文章  >  運維  >  透過QUIC協議,來看看怎麼學習網路協議

透過QUIC協議,來看看怎麼學習網路協議

青灯夜游
青灯夜游轉載
2022-03-01 09:57:243283瀏覽

這篇文章帶大家了解QUIC協議,並以QUIC協議為例,來聊聊如何學習網路協議,希望對大家有幫助!

透過QUIC協議,來看看怎麼學習網路協議

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

透過QUIC協議,來看看怎麼學習網路協議

那麼,如果你就是要詳盡地了解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):

透過QUIC協議,來看看怎麼學習網路協議

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

透過QUIC協議,來看看怎麼學習網路協議

這兩段程式碼建構了一個最簡單的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 的路徑放進去就可以了:

透過QUIC協議,來看看怎麼學習網路協議

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

透過QUIC協議,來看看怎麼學習網路協議

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

客戶端發送的首包

我們看客戶端發送的第一個訊息:

透過QUIC協議,來看看怎麼學習網路協議

#這個報文包含了非常豐富的資訊。首先,和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 報文,有以下幾種類型:

透過QUIC協議,來看看怎麼學習網路協議

既然有 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:

透過QUIC協議,來看看怎麼學習網路協議

可以看到,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:

透過QUIC協議,來看看怎麼學習網路協議

服务器的回包

我们来看 server 的回包:

透過QUIC協議,來看看怎麼學習網路協議

这里有一些新东西。首先,一个 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 报文中:

透過QUIC協議,來看看怎麼學習網路協議

服务器发送了自己的证书,并结束了 TLS handshake。

客户端结束 Handshake

我们再看第三个包,客户端发送给服务器结束 TLS 握手:

透過QUIC協議,來看看怎麼學習網路協議

这个包依旧包含两个 QUIC 报文,其中第一个就是一个 ACK frame,来确认收到了服务器的 Server Hello 那个 QUIC 报文;第二个包含一个 ACK frame,确认服务器的 Handshake,随后有一个 CRYPTO frame 结束客户端的 TLS handshake。

TLS 握手结束之后,客户端和服务器就开始应用层的数据交换,此刻,所有数据都是加密的。

客户端发送一个 “hello” 文本

在我们的  echo client/server 代码中,客户端连接到服务器后,就可以等待用户在 stdin 的输入,然后将其发送到服务器。服务器收到客户端数据,原封不动发回,客户端再将其显示到 stdout 上。在这个过程的前后,客户端和服务器间有一些用于连接管理的 QUIC 报文,比如 PING。我们就略过,只看发送应用层数据的报文。下图是客户端发送的包含 “hello” 文本的报文:

透過QUIC協議,來看看怎麼學習網路協議

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

透過QUIC協議,來看看怎麼學習網路協議

我們來看STREAM frame 的length(6) 和Data(68 65 6c 6c 6f 0a )。 Data 裡的內容如果用 ASCII 表示,剛好是 “hello”,它的長度是 6 個位元組。

伺服器回覆「hello」文字

#最後是伺服器echo back:

透過QUIC協議,來看看怎麼學習網路協議

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

賢者時刻

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

  • QUIC 透過哪個 frame 類型來做多路復用的?

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

相關推薦:

web伺服器安全性

以上是透過QUIC協議,來看看怎麼學習網路協議的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:微信公众号-程序人生。如有侵權,請聯絡admin@php.cn刪除