QUIC 是 HTTP/3 底下的傳輸協定,跑在 UDP 上,把 TCP 的可靠傳輸、TLS 1.3 的加密、以及多串流整合成一層。這份講義先講「TCP 到底卡在哪裡」,再用逐步動畫拆解交握、遺失重傳與連線遷移,最後落地到 Wireshark 怎麼抓、怎麼看、怎麼解密。
TCP 設計於 1970 年代,HTTPS 則是在 TCP 上再疊 TLS。這個組合跑了幾十年,但在行動網路與大量小檔案的現代網頁上,暴露出四個結構性問題。
TCP 三向交握 1 RTT,TLS 1.2 再 2 RTT(TLS 1.3 為 1 RTT)。在 RTT 100 ms 的行動網路上,光「還沒開始傳」就要 200~300 ms。
QUIC 把 TLS 1.3 交握訊息直接放進自己的封包,首次連線 1 RTT 就能送出請求;曾連過的伺服器還能 0-RTT,第一個封包就帶資料。
HTTP/2 在一條 TCP 上多工傳 CSS、JS、圖片,但 TCP 只認得「一條位元組流」。中間掉一段,後面已到的資料全部卡在核心緩衝區,等重傳補齊才交給應用程式。
每個 STREAM frame 帶有 Stream ID 與 offset,遺失只影響該串流;其他串流的資料一到就能交付。
TCP 連線由四元組(來源 IP、來源埠、目的 IP、目的埠)識別。手機從 Wi‑Fi 切到 4G/5G、或 NAT 對應改變,連線就斷了,得重新交握。
連線身分是 Connection ID,不是 IP。位址改變後,經過一次路徑驗證(PATH_CHALLENGE / PATH_RESPONSE)就能繼續傳。
TCP 實作在作業系統核心,標頭又是明文,路上大量防火牆、NAT、中介設備會檢查甚至改寫它。新功能(如 TCP Fast Open)常被中介設備擋掉,部署困難。
QUIC 建在 UDP 上、多半在應用程式裡實作(瀏覽器、函式庫),更新快;除了少數欄位外連傳輸層標頭都加密,中介設備沒辦法依賴它的內容,協定才能持續演進。
注意 QUIC 不是「UDP 上的 TCP」而已——TLS 被吸收進 QUIC 內部,HTTP/2 的串流管理也往下搬了。
以 RTT = 50 ms 為例,從用戶端開始連線到「伺服器收到第一個 HTTP 請求」之前,雙方必須來回的次數:
Alt-Svc: h3=":443",下次才改用 QUIC;或是 DNS 的 HTTPS 記錄(type 65)事先宣告支援 h3。若 UDP/443 被擋,瀏覽器會自動退回 TCP——這點在資安管控上很重要(見第 09 節)。點「播放」看動畫,或用上一步/下一步逐格講解。右側面板會顯示每個 UDP 資料包裡裝了哪些 QUIC 封包、帶什麼 frame,以及對應的 Wireshark 過濾條件。建議先看 01 的 TCP 對照組,再看 02。
| # | 方向 | 內容 | 重點 |
|---|
一個 UDP 資料包可以裝好幾個 QUIC 封包(稱為 coalescing,交握時很常見)。每個 QUIC 封包的第一個位元決定它是哪種標頭。滑鼠移到欄位上可看說明。
QUIC 有四種加密層級、三個獨立的封包編號空間。每個空間的 Packet Number 都從 0 開始、只增不減,ACK 也各自分開。
| 封包類型 | 標頭 | 封包編號空間 | 金鑰從哪來 | 誰能解密 | 內容 |
|---|---|---|---|---|---|
| Initial | Long (0x0) | Initial | 由用戶端選的 DCID + 公開的版本 salt 推導 | 任何人(包括 Wireshark、防火牆) | ClientHello / ServerHello、ACK |
| 0-RTT | Long (0x1) | Application Data | 上次連線的 PSK(session ticket) | 兩端 | 提前送出的應用資料 |
| Handshake | Long (0x2) | Handshake | TLS 1.3 (EC)DHE 交換後的 handshake 金鑰 | 兩端 | Certificate、Finished 等 |
| 1-RTT | Short | Application Data | TLS 1.3 應用流量金鑰 | 兩端 | HTTP/3 資料、控制 frame |
| Retry | Long (0x3) | 沒有封包編號 | 只有完整性標籤(固定金鑰) | 任何人 | 位址驗證 token |
QUIC 封包的酬載由一個或多個 frame 組成。封包只是「信封」,真正的語意都在 frame。重傳時重送的是 frame 的內容,而不是原封包。
| 類型值 | 名稱 | 用途 |
|---|---|---|
| 0x00 | PADDING | 填充。用戶端 Initial 必須墊到 UDP 酬載至少 1200 bytes |
| 0x01 | PING | 保活、引發對方回 ACK |
| 0x02–0x03 | ACK | 確認收到的封包編號範圍(可描述多個缺口,0x03 另帶 ECN 計數) |
| 0x04 | RESET_STREAM | 傳送端放棄某個串流 |
| 0x05 | STOP_SENDING | 接收端要求對方停止傳某個串流 |
| 0x06 | CRYPTO | 承載 TLS 交握訊息(ClientHello、Certificate…) |
| 0x07 | NEW_TOKEN | 給用戶端下次連線用的位址驗證 token |
| 0x08–0x0f | STREAM | 串流資料,帶 Stream ID、offset、FIN |
| 0x10–0x11 | MAX_DATA / MAX_STREAM_DATA | 流量控制:整條連線/單一串流可接收上限 |
| 0x12–0x13 | MAX_STREAMS | 允許對方開幾個雙向/單向串流 |
| 0x14–0x17 | *_BLOCKED | 告知「我被流量控制卡住了」 |
| 0x18–0x19 | NEW / RETIRE_CONNECTION_ID | 發放或退役 Connection ID(連線遷移用) |
| 0x1a–0x1b | PATH_CHALLENGE / PATH_RESPONSE | 路徑驗證:8 bytes 隨機值,對方原樣回傳 |
| 0x1c–0x1d | CONNECTION_CLOSE | 關閉連線(傳輸層錯誤/應用層錯誤) |
| 0x1e | HANDSHAKE_DONE | 伺服器告知交握已確認(只能由伺服器送) |
HTTP/3 的每個請求/回應
HTTP/3 中一般不使用
HTTP/3 控制串流、QPACK 編碼器串流
HTTP/3 控制串流(SETTINGS)等
| 面向 | TCP (+TLS) | QUIC |
|---|---|---|
| 底層 | IP 協定號 6 | UDP(通常 443 埠) |
| 實作位置 | 作業系統核心 | 多在使用者空間(瀏覽器、函式庫) |
| 連線識別 | 四元組 | Connection ID |
| 建立連線 | 1 RTT + TLS 1~2 RTT | 1 RTT(恢復時 0-RTT) |
| 加密 | 選用;TCP 標頭永遠明文 | 強制;連傳輸層控制資訊都加密 |
| 序號 | 位元組序號,重傳沿用同序號(有重傳歧義) | 封包編號只增不減,重傳用新編號(無歧義) |
| 多工 | 無;一條連線一個位元組流 | 原生多串流,彼此獨立 |
| 隊頭阻塞 | 有(傳輸層) | 只影響單一串流 |
| 換 IP | 斷線 | 連線遷移 |
| ACK | 累積 ACK + SACK(最多約 3~4 區段) | ACK frame 可描述大量範圍,且回報 ACK 延遲 |
| 關閉 | FIN / RST(明文,可被偽造) | CONNECTION_CLOSE(加密);Stateless Reset 需知道 token |
建議用 Wireshark 3.6 以上(較新版本對 QUIC v1/v2、HTTP/3 的解析更完整)。
開 Chrome 或 Edge 連到支援 HTTP/3 的網站(例如大型 CDN 服務的網站),在 DevTools → Network 加開「Protocol」欄,看到 h3 就表示走 QUIC。第一次載入可能是 h2,重新整理後才會切換(Alt-Svc 機制)。
| 過濾條件 | 用途 |
|---|---|
quic | 所有 QUIC 封包 |
udp.port == 443 && !quic | UDP/443 但沒被辨識為 QUIC 的流量(可疑或非標準) |
quic.header_form == 1 | Long Header(交握) |
quic.long.packet_type == 0 | Initial 封包 |
quic.long.packet_type == 1 | 0-RTT 封包 |
quic.long.packet_type == 2 | Handshake 封包 |
quic.long.packet_type == 3 | Retry 封包 |
quic.version == 0 | Version Negotiation |
tls.handshake.extensions_server_name | 從解開的 Initial 讀 SNI(不需金鑰) |
quic.frame_type == 0x1a || quic.frame_type == 0x1b | 路徑驗證(連線遷移)——需解密 1-RTT |
quic.frame_type == 0x1c || quic.frame_type == 0x1d | CONNECTION_CLOSE——需解密 |
http3 | HTTP/3 內容——需解密 |
dns.qry.type == 65 | DNS HTTPS 記錄查詢(探索 h3 支援) |
註:欄位名稱以近年 Wireshark 版本為準,若舊版不支援某欄位,可在封包詳細窗格對該欄位按右鍵 →「套用為過濾器」確認實際名稱。
QUIC 跟 TLS 一樣支援 SSLKEYLOGFILE。設定環境變數後啟動瀏覽器,瀏覽器會把每條連線的 TLS 祕密寫進檔案,Wireshark 讀進來就能解開 Handshake 與 1-RTT 封包。
接著 Wireshark → 編輯 → 偏好設定 → Protocols → TLS → (Pre)-Master-Secret log filename 指向該檔。QUIC 會共用 TLS 的設定。
傳統依賴 TLS 解密(SSL inspection)或 TCP 狀態追蹤的防火牆、Proxy、IDS,多半看不懂 QUIC 的 1-RTT 內容。很多組織的做法是封鎖 UDP/443,讓瀏覽器自動退回 TCP+TLS,以維持既有檢測能力。
SNI(解開 Initial 即可,除非使用 ECH)、QUIC 版本、Connection ID、封包大小與時間序列、Spin Bit(若有啟用)。這些已足夠做網域層級的政策與部分流量指紋分析。
UDP 容易偽造來源 IP。QUIC 規定伺服器在驗證用戶端位址前,回傳量不能超過收到量的 3 倍;加上用戶端 Initial 必須墊到 1200 bytes,攻擊者很難把 QUIC 伺服器當反射放大器。伺服器還能用 Retry 先驗證位址。
0-RTT 資料不具前向保密且可被重放:攻擊者把第一個封包錄下來再送一次,伺服器可能重複執行。因此只應允許冪等請求(如 GET),伺服器端需實作防重放或乾脆拒絕 0-RTT。
部分 C2 框架與惡意程式已支援 HTTP/3 / QUIC 作為通道,利用防禦方對 UDP/443 監控較弱的特性。在 NSM 中,對非瀏覽器程序發起的大量 QUIC 連線、或連往罕見網域的 QUIC 流量值得留意。
Initial 洪水會消耗伺服器的解密與狀態資源,可用 Retry(無狀態)緩解;偽造的 CONNECTION_CLOSE 因為需要金鑰而無效,Stateless Reset 也需事先知道 token,比 TCP RST 注入難得多。
TCP 必須先完成三向交握(1 RTT)才能開始傳 TLS ClientHello;QUIC 的第一個 Initial 封包就直接帶著 ClientHello,傳輸層與加密層的交握同時進行。
可以(未使用 ECH 時)。Initial 封包的金鑰由明文 DCID 與 RFC 公開的 salt 推導,任何人都能解開,裡面的 ClientHello 帶有 SNI。
不會。QUIC 用新的封包編號重送遺失 frame 的內容,所以收到 ACK 時能明確知道是確認原封包還是重傳封包,RTT 量測不會有 TCP 的「重傳歧義」。
連線靠 Connection ID 識別,不靠 IP。伺服器送 PATH_CHALLENGE 是為了確認新位址真的屬於用戶端(能收到並回傳 8 bytes 隨機值),避免攻擊者偽造來源位址把流量導向受害者。
配合 3 倍反放大限制:用戶端送得越大,伺服器在驗證前能回的量就越多,足以塞下憑證等交握資料;同時讓偽造小封包換大回應的放大攻擊無利可圖。1200 也確保路徑 MTU 能承載 QUIC 封包。
封鎖 UDP/443(或特定 QUIC 流量),讓瀏覽器退回 HTTP/2 over TCP。副作用是失去 QUIC 的延遲與連線遷移優勢,行動使用者體驗可能變差。