1. 程式人生 > >從啟用 HTTP/2 導致網站無法訪問說起

從啟用 HTTP/2 導致網站無法訪問說起

提醒:本文最後更新於 1039 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

最近好幾個朋友在給網站開啟 HTTP/2 後,都遇到了無法訪問的問題。其中有的網站只是 Firefox 無法訪問,通過控制檯網路面板可以看到請求被 Abort;有的網站不但 Firefox 無法訪問,連 Chrome 也會跳到錯誤頁,錯誤程式碼是「ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY」。詭異的是,只要去掉對 HTTP/2 的支援(例如去掉 Nginx listen 配置中的 http2)就一切正常。也就是說無法訪問的現象只存在於 HTTPS + HTTP/2 的組合,單獨提供 HTTPS 服務時就是好的。

這個問題比較有趣,本文除了告訴大家如何解決它之外,還會幫助大家弄清問題的來龍去脈。如果你只關心結論,直接看最後的小結即可。

首先,網站無法訪問有很多種可能,一般要從基本項開始檢查:

  • 是否網路不通(可以通過能否訪問 imququ.com 來排查 ^_^);
  • 網站 DNS 解析是否正常(可以通過 ping、nslookup、dig 等工具來排查);
  • TCP 連線能否建立(可以通過 telnet 來排查,例如 telnet imququ.com 443);

如果 TCP 連線能夠建立,至少說明 Web Server 在執行,本地到 Web Server 網路也正常。如果還有問題,就要開始往應用層去排查,例如:

  • 是否因為域名沒備案被阻止(可以嘗試用 IP,或者換非標準埠訪問);
  • 是否因為 Web 程式太慢,遲遲沒返回響應(通過瀏覽器網路面板可以看到請求狀態一直是 pending);
  • 是否有響應,只是內容為空(根據響應狀態碼,排查服務端配置或業務程式碼);

對於 HTTPS 網站,HTTP 和 TCP 中間多了一層 TLS。在瀏覽器傳送 HTTP 報文前,還得先跟服務端建立 TLS 連線,這個過程非常複雜,也很容易出問題。例如:

  • 沒有合法的證書(已過期、域名不匹配等等,一般瀏覽器都會給出明確的提示。需要特別排查同 IP 部署多 HTTPS 站點時,由於客戶端不支援 SNI 導致的證書不合法問題);
  • 使用了瀏覽器不支援的證書型別(例如沒有打 XP SP3 補丁的 IE6 不支援 SHA-2 證書);
  • 使用了瀏覽器不支援的 TLS 協議版本(例如 IE6 預設只支援 SSLv2 和 SSLv3);
  • 使用了瀏覽器不支援的 CipherSuite(例如 ECDHE-ECDSA-CHACHA20-POLY1305 只有 Chrome 支援);

關於部署 HTTPS 時的一些注意事項,可以參考我之前的「對於關於啟用 HTTPS 的一些經驗分享(二)」這篇文章,這不是本文重點,故不展開討論。

總之前面列了這麼多可能,跟本文要解決的問題基本沒有任何關係!如果是因為響應遲遲沒有回來,或者是證書不合法導致的無法訪問,完全沒有道理不啟用 HTTP/2 就是好的。

實際上,Chrome 這個「ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY」錯誤程式碼已經給出了兩個提示:

  1. 與 HTTP/2 有關。SPDY 是 HTTP/2 的前身,這個錯誤碼應該是從 SPDY 時代沿用下來的;
  2. 與 TLS 安全有關。對於有安全隱患的 HTTPS 站點,現代瀏覽器會阻止 TLS 握手成功。例如最新的 Chrome 48 會拒絕與「以 RC4 做為對稱加密演算法的 CipherSuite」建立 TLS 連線;

通過 Wireshark 抓包可以看到:這個案例中,瀏覽器在 TLS 握手階段傳送了「Encrypted Alert」,然後主動斷開了 TCP。TLS 連線都沒有建立成功,頁面當然無法訪問了。

之前閱讀 HTTP/2 RFC 時,我瞭解到 HTTP/2 協議中對 TLS 有了更嚴格的限制:例如 HTTP/2 中只能使用 TLSv1.2+,還禁用了幾百種 CipherSuite(詳見:TLS 1.2 Cipher Suite Black List)。至此可以肯定,之所以出現這個錯誤,要麼是服務端沒有啟用 TLSv1.2,要麼是 CipherSuite 配置有問題。本案例中,服務端支援 TLSv1.2,只可能是後者有問題。

CipherSuite,也就是加密套件,在整個 TLS 協議中至關重要,詳細介紹可以參考我之前的文章

建立 TLS 連線時,瀏覽器需要在 Client Hello 握手中提供自己支援的 CipherSuite 列表和應用協議列表(通過 TLS ALPN 擴充套件),服務端則通過 Server Hello 握手返回選定的 CipherSuite 和應用協議。如果服務端選定的應用協議是 HTTP/2,瀏覽器就需要檢查 CipherSuite 是否在 HTTP/2 的黑名單之中,如果存在就終止 TLS 握手。

當然,如果瀏覽器本身不支援 HTTP/2,Client Hello 握手中的 ALPN 擴充套件中就不會包含 h2(實際上,ALPN 擴充套件都不一定存在),服務端也不會選定 HTTP/2 做為後續應用協議。實際上,這個過程就是 HTTP/2 協議協商機制。

HTTP/2 對 CipherSuite 有更嚴格的限制,用於承載 HTTP/1.1 加密流量的 CipherSuite,不一定能用於承載 HTTP/2 加密流量。這也導致之前執行良好的 HTTPS 站點,在啟用 HTTP/2 後,可能會由於 CipherSuite 被禁用導致無法通過 HTTP/2 訪問。

明白了原理,再來看一個具體案例(注:本案例來自於本部落格網友評論,via):

在 Nginx 中配置以下 CipherSuite 並啟用 HTTP/2,在最新的 Firefox 中無法訪問:

ssl_ciphers ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;

注:上述配置中的 CHACHA20/POLY1305,由 Google 開發。以前需要使用 LibreSSLBoringSSL 或者 CloudFlare 的 OpenSSL Patch 才能支援它,最新版的 OpenSSL 已經內建了對它的支援(via)。

先來看看上述配置指定的 CipherSuite 具體有哪些(注:以下命令中的 openssl 版本是 LibreSSL 2.3.1):

openssl ciphers -V 'ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4' | column -t

執行結果如下:

0xCC,0x13  -  ECDHE-RSA-CHACHA20-POLY1305  TLSv1.2  Kx=ECDH  Au=RSA  Enc=ChaCha20-Poly1305  Mac=AEAD
0xC0,0x30  -  ECDHE-RSA-AES256-GCM-SHA384  TLSv1.2  Kx=ECDH  Au=RSA  Enc=AESGCM(256)        Mac=AEAD
0xC0,0x28  -  ECDHE-RSA-AES256-SHA384      TLSv1.2  Kx=ECDH  Au=RSA  Enc=AES(256)           Mac=SHA384
0xC0,0x14  -  ECDHE-RSA-AES256-SHA         SSLv3    Kx=ECDH  Au=RSA  Enc=AES(256)           Mac=SHA1
0xC0,0x2F  -  ECDHE-RSA-AES128-GCM-SHA256  TLSv1.2  Kx=ECDH  Au=RSA  Enc=AESGCM(128)        Mac=AEAD
0xC0,0x27  -  ECDHE-RSA-AES128-SHA256      TLSv1.2  Kx=ECDH  Au=RSA  Enc=AES(128)           Mac=SHA256
0xC0,0x13  -  ECDHE-RSA-AES128-SHA         SSLv3    Kx=ECDH  Au=RSA  Enc=AES(128)           Mac=SHA1
0x00,0x9D  -  AES256-GCM-SHA384            TLSv1.2  Kx=RSA   Au=RSA  Enc=AESGCM(256)        Mac=AEAD
0x00,0x3D  -  AES256-SHA256                TLSv1.2  Kx=RSA   Au=RSA  Enc=AES(256)           Mac=SHA256
0x00,0x35  -  AES256-SHA                   SSLv3    Kx=RSA   Au=RSA  Enc=AES(256)           Mac=SHA1
0x00,0x9C  -  AES128-GCM-SHA256            TLSv1.2  Kx=RSA   Au=RSA  Enc=AESGCM(128)        Mac=AEAD
0x00,0x3C  -  AES128-SHA256                TLSv1.2  Kx=RSA   Au=RSA  Enc=AES(128)           Mac=SHA256
0x00,0x2F  -  AES128-SHA                   SSLv3    Kx=RSA   Au=RSA  Enc=AES(128)           Mac=SHA1
0xC0,0x12  -  ECDHE-RSA-DES-CBC3-SHA       SSLv3    Kx=ECDH  Au=RSA  Enc=3DES(168)          Mac=SHA1
0x00,0x0A  -  DES-CBC3-SHA                 SSLv3    Kx=RSA   Au=RSA  Enc=3DES(168)          Mac=SHA1

再通過 Wireshark 獲得 Firefox 在 Client Hello 中傳送的 CipherSuite 列表,如下:

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2B)
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2F)
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xC0,0x0A)
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xC0,0x09)
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xC0,0x13)
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xC0,0x14)
TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x00,0x33)
TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x00,0x39)
TLS_RSA_WITH_AES_128_CBC_SHA (0x00,0x2F)
TLS_RSA_WITH_AES_256_CBC_SHA (0x00,0x35)
TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x00,0x0A)

CipherSuite 協商目的是找出兩端都支援的套件,也就是取出二者的交集:

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2F)
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xC0,0x13)
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xC0,0x14)
TLS_RSA_WITH_AES_128_CBC_SHA (0x00,0x2F)
TLS_RSA_WITH_AES_256_CBC_SHA (0x00,0x35)
TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x00,0x0A)

乍一看選擇餘地還挺大,但別忘了,HTTP/2 協議中還禁用了好幾百個。把這部分去掉後只剩下:

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2F)

奇怪的是,好歹還有一個滿足所有條件的套件,為什麼還是會握手失敗呢?通過 Wireshark 看一下 Server Hello 會發現:在這個案例中,通過 Firefox 訪問,服務端選定的套件是 0xC0,0x14,並不是 0xC0,0x2F

Nginx 有一個 ssl_prefer_server_ciphers 配置,如果設定為 on,表示在協商 CipherSuite 時,算出交集後,會按照服務端配置的套件列表順序返回第一個,這樣可以提高安全性。而那份配置的 ssl_ciphers 中,0xC0,0x14 排在了 0xC0,0x2F 前面,開啟 ssl_prefer_server_ciphers 後,會使得被 HTTP/2 禁用的 0xC0,0x14 選中,從而導致最終 HTTPS + HTTP/2 握手失敗。

那為什麼這份配置在 Chrome 中是正常的呢?Chrome 支援的 CipherSuite 如下,大家可以自己分析下。

TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2B)
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xC0,0x2F)
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x00,0x9E)
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xCC,0x14)
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xCC,0x13)
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xC0,0x0A)
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xC0,0x14)
TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x00,0x39)
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xC0,0x09)
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xC0,0x13)
TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x00,0x33)
TLS_RSA_WITH_AES_128_GCM_SHA256 (0x00,0x9C)
TLS_RSA_WITH_AES_256_CBC_SHA (0x00,0x35)
TLS_RSA_WITH_AES_128_CBC_SHA (0x00,0x2F)
TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x00,0x0A)

針對這個案例,將 Nginx 配置中的 0xC0,0x2F(ECDHE-RSA-AES128-GCM-SHA256)挪到 0xC0,0x14(ECDHE-RSA-AES256-SHA) 之前,即可解決最新 Firefox 下無法訪問的問題。當然,正如我在以往文章中多次強調的,配置 TLS 時務必參考權威文件,例如:Mozilla 的推薦配置CloudFlare 使用的配置。經過測試,使用這兩份配置的 HTTPS 站點在啟用 HTTP/2 後都沒有問題。

簡單小結一下,對於能正常工作的 HTTPS 網站啟用 HTTP/2 後出現無法訪問的問題,請排查服務端這兩點配置:1)是否啟用了 TLSv1.2;2)是否正確配置了 CipherSuite。

本文就寫到這裡。大家平時遇到有關 HTTP(S)、HTTP/2 的問題,歡迎給我留言或者發郵件討論。

--EOF--

提醒:本文最後更新於 1039 天前,文中所描述的資訊可能已發生改變,請謹慎使用。