Http演進之路之七

lonnieZ:Http演進之路之七?

zhuanlan.zhihu.com圖標

QUIC

QUIC(Quick UDP Internet Connections)協議(與名英文quick同音)是Google提出的一種基於UDP的低延時、多並發的傳輸協議。換句話說,QUIC實現了基於UDP上的HTTP/2功能。其結構圖如下所示。

QUIC類似在UDP之上實現了TLS+HTTP/2的功能。QUIC將原來TCP在OS內核空間中的實現移到了用戶空間(例如擁塞避免、流量控制、丟包重傳等),使QUIC使用起來更加靈活、開發迭代速度更快。

HTTP/3

Http-Over-QUIC已經正式更名為HTTP/3,這標誌著QUIC會在隨後的幾年內成為支持HTTP的主要協議。

為何選用QUIC

由於QUIC的大部分實現都是基於UDP之上,而非在OS的內核中進行開發,因此開發迭代速度相對較快,部署成本低、阻力小。此外,正如其名一樣,QUIC的最大優點是「快」,這主要體現在以下幾點:

  • UDP之上的多路復用(不再有隊頭阻塞的困擾)
  • 0 RTT
  • FEC(Forward Error Connection)
  • PP(Package Pacing)
  • 連接遷移

HTTP/1.x的隊頭阻塞

在HTTP/1.0中雖然已經加入了「長連接」的概念,但是由於當時的網頁相對簡單,網頁中包含的資源相對較少。因此,默認連接並非「長連接」,當用戶申請完以後資源後會斷開連接重新建連去申請下一個資源;隨著互聯網的發展,網頁的資源不斷豐富、增多,每次重新建連會消耗大量的資源。因此,在HTTP/1.x中,將「長連接」作為了默認選項。即使如此,由於HTTP協議的無狀態性,每個請求必須one by one的進行,即第二個請求必須等到第一個請求的response返回後才能發起,這就是一種隊頭阻塞。隨後,為了進一步優化(減少等待時間),發明瞭pipeline的機制,即第二請求不必等待上一個請求response返回才能發起,而是可以直接發起,但是在服務端必須要one by one in order的處理和返回,即服務端必須先返回第一個response才能返回第二個response,因此pipeline並沒有解決隊頭阻塞的問題。後來,又有了parallel connection,即對於同一個host可以發起多個connection去同時申請資源,這在一定程度上增加了訪問速度,提供了請求的並行度,但並沒有解決隊頭阻塞的問題,同時,多個connection對於服務端而言也是一種資源消耗。

HTTP/2的多路復用

為瞭解決HTTP/1.x中存在的隊頭阻塞問題,HTTP/2提出了多路復用的概念。即將一個request/response作為一個stream,並將一個stream根據負載分為多種類型的frame(例如 header frame,data frame等),在同一條connection之上可以混合發送分屬於不同stream的frame,這樣就實現了同時發送多個request的功能,解決了隊頭阻塞的問題。

下面這張圖很形象的把HTTP/1.x和HTTP/2進行了對比:

TCP層的隊頭阻塞

HTTP/2的多路復用,主要針對HTTP/1.x中的隊頭阻塞進行了修正,但是隊頭阻塞並不只在HTTP層,在TCP層仍然存在隊頭阻塞。由於TCP的工作原理,當數據包按順序到達後才能push到上層去處理,如果這中間有一個數據(例如下圖中的數據2)沒有大的,但其後面的數據(例如數據3、4)已經到了。那麼,只有當數據2到達後,才能把數據3、4發送到上層去。即數據2對後面的數據造成了隊頭阻塞。

QUIC的多路復用

由於QUIC是建立在UDP之上的,因此並不存在TCP層的隊頭阻塞。其中即使屬於某個stream的某個frame丟失了,也僅僅只是影響這個stream,而不會影響到其他的stream。因此我們說QUIC也解決了「傳輸層」的隊頭阻塞問題。

但QUIC並非沒有隊頭阻塞,由於QUIC使用的是HPACK對header進行壓縮,受演算法限制,在傳遞header幀的時候仍然存在隊頭阻塞的問題。

HTTP/HTTPS的握手

傳統TCP三次握手需要1個RTT來完成。SSL的完整握手需要2個RTT完成,而Resumption的握手需要一個1RTT完成。因此,一次HTTPS的握手需要2-3次RTT的時間。一次RTT的實際大約在100-150ms,那麼完成一次握手至少需要200-450ms,RTT的時間越久,用戶體驗就越差。

Diffie-Hellman祕鑰交換演算法

為了保證安全,QUIC也是加密數據傳輸,因此在建連的過程中雙方也要協商出一個加密祕鑰。與TLS不同,QUIC僅需要使用1個RTT就可以完成祕鑰交換。其採用的是迪菲-赫爾曼密鑰交換(Diffie–Hellman key exchange,簡稱「D–H」)演算法,具體演算法可以參見這裡。簡單來說,A和B各自保存自己的私鑰,A選擇了一個素數P和其原根a,根據P、a以及自己的私鑰X可以計算出公鑰Y,然後A把P、a和Y告訴B;B再根據自己的私鑰M、P和a計算出公鑰N並告訴A;這樣雙方就有了對方的公鑰以及自己的私鑰,再根據對方公鑰、自己的私鑰以及P計算出對稱祕鑰K並使用K進行加密。從下圖可以看出整個祕鑰交換過程僅需要一個RTT就可以完成。DH演算法最重要的特點是:前向安全性。

RSA與ECDHE祕鑰交換演算法

目前,有大一部分服務端仍然使用的是RSA的祕鑰交換演算法。即,通過在客戶端生成pre-master,然後再通過服務端的共要進行加密傳遞給服務端(通過Client key exchange),隨後生成master key。這種祕鑰交換演算法的問題是:如果服務端的私鑰一旦泄露,那麼之前所有的加密數據就會被泄露。即當有攻擊者進行被動攻擊,收集了大量的數據,等到服務端私鑰泄露的時候,這些數據即被破解。因此,RSA為前向不安全的。

為瞭解決這個問題,有了基於DH演算法的ECDHE祕鑰交換演算法。這種演算法不再通過伺服器的公鑰和私鑰傳遞敏感數據,而是通過Server Key Exchange來傳遞DH演算法的參數並進行簽名,客戶端收到後,進行驗簽並生成對應的公鑰和對稱祕鑰,再把對稱祕鑰通過Client Key Exchange傳遞給服務端。因此,當你在SSL包中看到了Server Key Exchange消息的時候,說明採用的是ECDHE祕鑰交換演算法。這種演算法是前向安全的,因為整個過程中不存在使用伺服器公鑰、私鑰加解密數據的過程。

QUIC的握手

目前QUIC的握手加密演算法採用了DH演算法並通過保存ServerConfig的方式實現了0RTT的握手過程:

  1. Step 0:服務端會生成一個素數p和其原根g,同時生成隨機數Kpri並計算出對應的Kpub = g ^ Kpri mod p;ServerConfig中靜態包含了三元組{p, g, Kpub}供隨後的Client使用。這個三元組會定期進行更新。
  2. Step 1:客戶端發送Inchoate Client Hello消息請求連接;
  3. Step 2:服務端返回Reject消息,其中攜帶了ServerConfig,而ServerConfig中包含了之前靜態保存的三元組{p, g, Kpub}
  4. Step 3:動態生成隨機數Kc_pri並根據ServerConfig中三元組的p和g計算出對應公鑰Kc_pub再計算出對稱祕鑰(初始密鑰)K1=Kpub^Kc_pri mod p;通過K1加密data
  5. Step 4:服務端根據Kc_pub計算生成對稱祕鑰K1』=Kc_pub^Kpri mod p;K1』 = K1;使用K1』解密對應data;由於Kpri非前向安全,因此K1非前向安全算。服務端動態生成隨機數Kn_pri並根據p和g生成對應公鑰Kn_pub=g^Kn_pri mod p;再根據Kc_pub計算出新的對稱祕鑰K2=Kc_pub^Kn_pri mod p;通過K2加密data;K2為前向安全祕鑰,即僅在服務端更新了公鑰和私鑰
  6. Step 5:使用K1解密得到Kn_pub;再根據Kn_pub計算出新的對稱祕鑰K2』=Kn_pub^Kc_pri mod p;K2』 = K2;使用K2』解密data並在隨後的通信中使用K2』加解密數據
  7. Step 6:隨後使用K2加密數據
  8. Step 7:客戶端緩存了服務端的ServerConfig即三元組{p, g, Kpub},因此可以從Step3開始做起。動態生成隨機數Kc_pri2並根據ServerConfig中三元組的p和g計算出對應公鑰Kc_pub2;再計算出對稱祕鑰(初始密鑰)K1=Kpub^Kc_pri2 mod p;通過K1加密data
  9. Step 8:該過程同Step4;服務端根據Kc_pub2計算生成對稱祕鑰K1=Kc_pub2^Kpri mod p K1 = K1;使用K1解密對應data;由於Kpri非前向安全,因此K1非前向安全算。服務端動態生成隨機數Kn_pri2並根據p和g生成對應公鑰Kn_pub2=g^Kn_pri2 mod p;再根據Kc_pub2計算出新的對稱祕鑰K2=Kc_pub2^Kn_pri2 mod p;通過K2加密data;K2為前向安全祕鑰,即僅在服務端更新了公鑰和私鑰
  10. Step 9:過程同Step5;使用K1解密得到Kn_pub2;再根據Kn_pub2計算出新的對稱祕鑰K2=Kn_pub2^Kc_pri2 mod p;K2 = K2;使用K2解密data並在隨後的通信中使用K2』加解密數據
  11. Step 10:隨後使用K2加密數據

0RTT的安全問題

  • 前向安全

ServerConfig(保存了三元組)一般會保存1周左右,在這段時間內存在前向安全的隱患

  • 重放攻擊

由於是0RTT,Server端無法通過(發送給Client端)隨機數來更新祕鑰,因此存在重放攻擊的風險。可以通過在ClientHello中添加隨機數並在Server端保存記錄的方式解決,但對於服務端的成本會比較大。因此大多數伺服器對於使用0RTT進行交互的early data限定只能為GET請求。

FEC(Forward Error Correction)

QUIC使用FEC來恢複數據;FEC採用異或(XOR)的方式。每發送一組數據(這組數據中每個包都帶有group id),對這組數據進行異或運算,得出的結果作為一個FEC包發送出去。接收方收到這一組數據後根據數據包和FEC包即可進行校驗和糾錯。它的優點:實現簡單,無需改變數據包內容。它的缺點:每個group裡面僅能恢復一個包;相當於增加了UDP數據包的實際負載。

Package Pacing

QUIC使用PP來探測網路帶寬;QUIC會通過追蹤包到達的時間來預測當前網路帶寬的使用情況,以決定是否提高、保持或降低發送包的速率來避免網路擁塞和丟包的情況發生。

連接遷移(Connection Migration)

?一條TCP連接是由四元組確立的(源IP+源埠+目的IP+目的埠)。在網路(環境)發生變化的時候(例如4G與wifi的切換或從一個地方移動到另一個地方),則需要重新建立連接。

?一條QUIC連接是由一個64位隨機數作為ID進行標識的。這也無論IP或者埠發生變化,只要ID不變,則該連接依然有效,無需進行重連,而上層業務不會感知變化。


推薦閱讀:
查看原文 >>
相關文章