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 支援 的努力,它將允許應用程式避免將使用者空間記憶體拷貝到核心空間的開銷。