QUIC 之路
QUIC (Quick UDP Internet Connections,快速 UDP 網絡連接) 是一個新的默認加密的互聯網傳輸協議,它提供了大量改進用于加速 HTTP 流量并使它更安全,以實現最終在 Web 上替換 TCP 和 TLS 的目標。這篇博文中我們將概述 QUIC 的一些關鍵特性,及它們如何使 Web 受益,以及在支持這個基礎的新協議上的一些挑戰。
事實上有兩個協議,它們共享相同的名字: “Google QUIC”(簡稱 “gQUIC”)是 Google 的工程師在幾年前設計的最初的協議,它在經過了幾年的實驗之后,現在已經被 IETF (Internet Engineering Task Force,互聯網工程任務組) 采納為標準。
“IETF QUIC”(從現在開始是 “QUIC”)已經與 gQUIC 有了相當大的區別,因此它可以被認為是一個獨立的協議。從數據包的格式,到握手和 HTTP 映射,多虧了大量組織和個人的開放合作,并以使互聯網更快更安全為共同目標,QUIC 已經改進了最初的 gQUIC 設計。
然而,QUIC 都提供了哪些改進呢?
內置安全性(和性能)
與現在備受推崇的 TCP 相比,QUIC 的一個更根本的改變是,它聲明了默認提供安全傳輸協議的設計目標。QUIC 通過提供安全特性來實現這一目標,如認證和加密由傳輸協議自身來處理,典型地,它們由一個更高層的協議(如 TLS)來處理。
初始 QUIC 握手結合了典型的 TCP 三次握手與 TLS 1.3 握手,其中后者提供端點認證和加密參數協商。對于那些熟悉 TLS 協議的人來說,QUIC 用它自己的幀格式替換了 TLS 記錄層,同時保留了相同的 TLS 握手消息。
這不僅確保連接總是認證且加密的,它還使最初的連接建立更快速:典型的 QUIC 握手只耗費客戶端和服務端之間一個單獨的往返來完成,對比 TCP 和 TLS 1.3 握手合起來所需的兩次往返。
但 QUIC 甚至走得更遠,它還加密了額外的連接元數據,這些元數據可能被中間人濫用來干擾連接。例如,當使用連接遷移時,鏈路上的被動攻擊者可以使用包號關聯多個網絡路徑上的用戶活動(見下面)。通過加密包號,QUIC 確保它們不會被除連接的端點外的任何實體用來關聯用戶活動。
加密也是解決僵化問題的有效方法,它們使協議內置的靈活性(比如協商協議的不同版本號)在實踐中由于實現的錯誤假設而無法使用(僵化正是TLS 1.3 推遲部署這么久的原因,只有在一些改變采用之后才可能,以防止僵化的中間設備錯誤地阻塞了新版本的 TLS 協議)。
隊首阻塞
HTTP/2 帶來的一個主要改進就是在相同的 TCP 連接上多路復用不同 HTTP 請求的能力。這使得 HTTP/2 應用可以并發地處理請求并更好地利用它們可用的網絡帶寬。
這對于當時的現狀而言是巨大的改進,如果應用想要并發地處理多個 HTTP/1.1 請求,其需要應用初始化多個 TCP + TLS 連接(比如當瀏覽器需要同時獲取 CSS 和 Javascript 資源渲染網頁時)。創建新連接需要重復初始握手多次,也需要經歷初始擁塞窗口爬坡,這意味著網頁渲染變慢了。多路復用 HTTP 交換避免了這一點。
然而這也有不利之處:由于多個請求/響應由相同的 TCP 連接傳輸,它們會同等地受到丟包影響(比如由于網絡擁塞),即使數據丟失只影響到一個單獨的請求。這稱為“隊首阻塞”。
QUIC 做的更深入一些,它為多路復用提供了一流的支持,這樣不同的 HTTP 流就可以被映射到不同的 QUIC 傳輸流上,但是,它們依然共享相同的 QUIC 連接,因此不需要額外的握手,且共享擁塞狀態,QUIC 流是獨立傳輸的,因此在大多數情況下丟包只影響一個流而不影響其它的。
可以極大地縮短呈現完整 Web 頁(其中包含 CSS,Javascript,圖片,和其它種類的資源)面所需的時間,特別是當穿越高度擁塞的網絡,并具有很高的丟包率時。
那么容易,呃?
為了兌現承諾,QUIC 協議需要打破一些許多網絡應用程序想當然的假設,這潛在地使 QUIC 的實現和部署更加困難。
QUIC 設計基于 UDP 數據報傳輸,這是為了簡化部署并避免丟棄未知協議數據包的網絡應用所帶來的問題,由于大多數應用已經支持了 UDP。這也使得 QUIC 協議實現可以運行在用戶空間,因此,比如,瀏覽器將能夠實現新的協議特性并把它們帶給用戶而無需等待操作系統更新。
然而,盡管預期的目標是避免破壞,但它也使防止濫用和正確路由數據包到正確的端點更具挑戰性。
NAT 把它們都帶來了并在黑暗中約束它們
典型的 NAT 路由器可以使用傳統的 4 元組(源 IP 地址和端口,目的 IP 地址和端口)追蹤 TCP 連接通過它們,并通過觀察網絡中的 TCP SYN,ACK 和 FIN 包傳輸,它們可以探測新的連接何時建立何時終止。這使它們可以精確地管理 NAT 綁定的生命周期,內部 IP 地址和端口與外部 IP 地址和端口的關聯。
對于 QUIC,這還不可能,因為今天四處部署的 NAT 路由器還無法理解 QUIC,因此,它們通常會退回到默認策略,即不太精確地處理 UDP 流,這通常涉及使用任意的,有時非常短的超時,這可能會影響長時間運行的連接。
當發生 NAT 重綁定時(比如由于超時),NAT 邊界外的端點將看到包來自于一個不同的源端口,而不是連接最初建立時所觀察到的哪個,這使得只使用 4 元組追蹤連接變得不可能。
還不只是 NAT!QUIC 想要提供的一個特性稱為“連接遷移”,它允許 QUIC 端點隨意遷移連接到不同的 IP 地址和網絡路徑。比如,一個移動客戶端將能夠在無線數據網絡和 WiFi 之間遷移 QUIC 連接,當一個已知的 WiFi 網絡可用時(比如當它的用戶進入他們最喜愛的咖啡店時)。
QUIC 試圖通過引入連接 ID 的概念來解決這個問題:可變長度的任意不透明數據塊,由 QUIC 數據包攜帶,它們可被用于標識一個連接。端點可以使用這個 ID 追蹤它們所代表的連接而無需檢查 4 元組(實踐中可能有多個 IDs 標識相同的連接,比如為了避免在連接遷移被使用時鏈接不同的路徑,但那種行為由端點控制而不是中間節點)。
然而,這也給使用 anycast 尋址和 ECMP 路由 的網絡運營商帶來了一個問題,即一個目標 IP 地址可能潛在地標識數百甚至數千臺服務器。由于這些網絡使用的邊緣路由器還不知道如何處理 QUIC 流量,可能發生的一種情況是屬于相同 QUIC 連接(即具有相同的連接 ID)的 UDP 數據包具有不同的 4 元組(由于 NAT 重綁定或連接遷移),它們可能最終被路由到不同的服務器,這就打破了連接。
為了解決這個問題,網絡運營商可能需要使用更智能的 4 層負載均衡解決方案,這可以用軟件實現并在無需觸碰邊緣路由器的情況下部署(請參考 Facebook 的 Katran 項目作為例子)。
QPACK
HTTP/2 引入的另一個好處是 首部壓縮 (或 HPACK),它允許 HTTP/2 端點通過移除 HTTP 請求和響應的冗余減少大量的網絡數據傳輸。
特別的,在其他技術中,HPACK 使用前面的請求(或響應)將要發送(或接收)的頭部填充的動態表,使得端點可以在新請求(或響應)中引用前面遇到的頭部,而不是再次重新傳送它們。
HPACK的動態表需要在編碼器(發送 HTTP 請求或響應的部分)和解碼器(接收它們的部分)之間進行同步,否則解碼器將無法解碼它收到的數據。
基于 TCP 的 HTTP/2 的這種同步是透明的,因為傳輸層(TCP)負責以與發送它們相同的順序傳送 HTTP 請求和響應,所以更新表的指令可以由編碼器作為請求(或響應)本身的一部分發送,這使得編碼非常簡單。但是 QUIC 要復雜得多。
QUIC 可以在不同的流中獨立地傳輸多個 HTTP 請求(或響應),這意味著,雖然它負責按照單個流的順序交付數據,但在跨多個流的順序則無法保證。
比如,如果一個客戶端通過 QUIC 流 A 發送了 HTTP 請求 A,通過流 B 發送 請求 B,可能發生的情況是,由于網絡中數據包的重排序或丟失,服務器在收到請求 A 之前就收到了請求 B,且如果請求 B 被編碼為引用一個請求 A 的頭部,則服務器將無法解碼它,因為它還沒有看到請求 A。
在 gQUIC 協議中,這個問題通過簡單地以相同的 gQUIC 流序列化所有的 HTTP 請求和響應頭部(但不包含 bodies)來解決,這意味著無論如何,頭部都會按順序傳遞。這是一個非常簡單的方法,它允許實現復用大量它們已有的 HTTP/2 代碼,但是另一方面它加劇了 QUIC 設計用于減少的隊首阻塞問題。IETF QUIC 工作組因此設計了一個新的 HTTP 和 QUIC 間的映射(“HTTP/QUIC”) 以及一個新的首部壓縮方法稱為 “QPACK”。
在最新的 HTTP/QUIC 映射草案和 QPACK 規范中,每個 HTTP 請求/響應交換使用它自己的雙向 QUIC 流,因此沒有隊首阻塞問題。此外,為了支持 QPACK,每個端點創建兩個額外的單向 QUIC 流,一個用于向另一個端點發送 QPACK 表更新,一個用于確認另一邊收到的更新。這樣,QPACK 編碼器只能在解碼器顯式地確認動態表引用之后才能使用它。
偏轉反射
基于 UDP 的 協議 的一個常見問題是它們對于反射攻擊的敏感性,其中攻擊者欺騙原本無辜的服務器向第三方受害者發送大量數據,通過欺騙針對服務器的數據包的源 IP 地址,使它們看起來像是來自受害者。
當服務器發送的響應恰巧比它接收的請求大時,這種攻擊可能非常有效,在這種情況下,我們談論的是“放大”。
TCP 不常遭遇這種類型的攻擊是由于,在它的握手期間傳輸的初始數據包(SYN,SYN+ACK,…)具有相同的長度,因此它們不提供任何放大的可能性。
另一方面,QUIC 握手是非常不對稱的:像 TLS 那樣,在第一次傳輸中,QUIC 服務器通常發送自己的證書鏈,這可能非常大,同時客戶端只需要發送一些字節(TLS ClientHello 消息嵌入在一個 QUIC 數據包中)。因此,客戶端發送的初始 QUIC 數據包不得不填充到一個特定的最小長度(即使數據包的實際內容小得多)。然而,這種緩解仍然不夠,因為典型的服務器響應跨越多個包,因此仍然可能比填充的客戶端包大得多。
QUIC 協議還定義了一種顯式的源地址驗證機制,其中服務器不發送它的長長的響應,而只發送一個小得多的“重試”數據包,其中包含一個唯一的加密令牌,隨后客戶端將在一個新的初始數據包中向服務器回顯它。這樣,服務器可以更有信心,客戶端沒有欺騙它自己的源 IP 地址(因為它收到了重試數據包),并可以完成握手。這種緩解的缺點是它增加了初始握手的時間,從一個來回到兩個。
另一種可選方案包括減少服務器到端點的響應,其中反射攻擊變得不那么有效,比如通過使用ECDSA 證書(它們典型的比 RSA 證書要小得多)。我們也在實驗使用現成的壓縮算法,如 zlib 和 brotli,壓縮 TLS 證書的機制,這個功能最初有 gQUIC 引入,但當前在 TLS 中無法使用。
UDP 性能
QUIC 的一個反復出現的問題是大量部署的已有的硬件和軟件還無法理解它。我們已經看了 QUIC 如何試圖解決網絡中間節點的問題,如路由器,但另一個潛在的問題區域是在 QUIC 端點自身發送和接收基于 UDP 的數據的性能。過去多年來,大量的工作消失在盡可能大的優化 TCP 實現上了,包括在軟件(如操作系統)和硬件(如網絡接口)中構建卸載功能,但是 UDP 目前沒有這些功能。
然而它只是時間問題,直到 QUIC 實現也可以利用這些能力。看看最近 在LInux上實現 UDP 通用分段卸載 的努力,這將允許應用程序在用戶空間和內核空間網絡堆棧之間捆綁和傳輸多個UDP段,代價是一個(或足夠接近)調用,以及 Linux 上的零拷貝 socket 支持 的努力,它將允許應用程序避免將用戶空間內存拷貝到內核空間的開銷。
結論
像 HTTP/2 和 TLS 1.3 那樣,QUIC 被創建來提供許多新特性,以提升網站的性能和安全性,以及其它基于互聯網的屬性。IETF 工作組當前被建立來在今年底之前提供 QUIC 規范的第一個版本,且 Cloudflare 的工程師也已經在努力工作,以為我們所有的客戶提供 QUIC 帶來的益處。
原文
總結
- 上一篇: EV3 直接命令 - 第 5 课 从 E
- 下一篇: QUIC 之类的可靠传输协议