若是你用 OpenResty 作過 SSL session reuse,你可能會用到其中的異步獲取 SSL session 的特性,好比在 ssl_session_fetch_by_lua*
裏面發起非阻塞的網絡請求,從 memcache 或別的什麼存儲服務器上讀取 SSL session。ssl_session_fetch_by_lua*
的實現原理,就是在 OpenSSL 的 session get callback 裏執行 Lua 代碼。然而 OpenSSL 的 session get callback 並不支持 yield,因此只靠原生 OpenSSL 是沒辦法支持在該階段發起非阻塞的網絡請求的。所以,Cloudflare 的工程師貢獻了一個 OpenSSL 的補丁給 OpenResty,讓 OpenSSL 的 session get callback 支持 yield。html
要想解釋這個補丁的實現原理,咱們得先說明下什麼是「支持 yield」。在 OpenResty 的語境裏,支持 yield 就是指某個函數(或者階段)能夠被中斷,而後在合適的時間繼續執行。這樣的函數須要至少有三種返回結果:OK
,ERROR
和 AGAIN
。若是函數返回 AGAIN
,表示它的工做被中斷了,好比發起了一個網絡請求,須要等待對端的響應。同時該函數還會把本身的狀態保存到一個 ctx
裏。當合適的機會來臨時,它會被從新調用,讀取 ctx
裏面的狀態繼續執行下去,直到返回 OK
或 ERROR
爲止。nginx
讓 session get callback 支持 yield 的補丁最先是針對 OpenSSL 1.0 開發的。後來 OpenSSL 1.1.0 版本大改了握手流程,因而 Cloudflare 的工程師寫了第二版補丁。以後 OpenSSL 1.1.0 的小版本又有些許小變化,我便幫着潤色了下。git
針對 OpenSSL 1.1.0 版本的補丁,主要作了兩個改動:github
READ_STATE_PROCESS
。這個狀態裏,OpenSSL 會嘗試 session resumption。該過程有可能會返回一個表示重試的錯誤碼,在這種狀況下,若是從新調用 SSL_handshake
, OpenSSL 會從新進入這一狀態。補丁的代碼能夠在這裏看到:https://github.com/openresty/...服務器
除了要給 OpenSSL 打補丁外,該功能還依賴於一個 Nginx 補丁。Nginx 的 ngx_ssl_handshake
只有在 SSL_ERROR_WANT_READ
和 SSL_ERROR_WANT_WRITE
下才會重試 SSL_handshake
。 因此 OpenResty 給 Nginx 打了補丁,讓它在 SSL_ERROR_PENDING_SESSION
下也能重試。網絡
SSL_ERROR_PENDING_SESSION
這個名字來源於 BoringSSL。BoringSSL 是 Google 和 Cloudflare 一塊兒開發的,在這個 SSL 庫裏面,session fetch 是能夠 yield 的。該特性的名字就叫作 PENDING SESSION。ssl_session_fetch_by_lua*
就是先在 Cloudflare 內部使用一段時間,而後再開源出來。天然,這個 Nginx 補丁也是針對 BoringSSL 設計的。爲此,OpenSSL 補丁也給本身的錯誤類型起了個 SSL_ERROR_PENDING_SESSION
的別名。session
補丁的代碼能夠在這裏看到:https://github.com/openresty/...異步
OpenSSL 1.1.1 版本對握手流程又作了新的變化,變化之大,以至於現有的補丁無法再縫縫補補湊合用下去了。因爲此次沒有天降 Cloudflare 工程師來幫忙,我就接下了這一任務,開始針對新的握手流程修改當前的補丁。函數
看了下 OpenSSL 1.1.1 涉及的函數,最大的變化是它把 session resumption 從原來的 READ_STATE_BODY
向後挪到了 READ_STATE_POST_PROCESS
。補丁裏之因此要引入 READ_STATE_PROCESS
把 READ_STATE_BODY
劈成兩半,是由於 READ_STATE_BODY
裏包含了讀取 TLS 數據塊的這一不可重試的操做。可是 READ_STATE_POST_PROCESS
裏就沒這樣的顧忌了。因此咱們能夠把 READ_STATE_PROCESS
這個階段去掉。post
一番調整後,我發現 ssl_session_fetch_by_lua*
如今只要一 yield,就會報 Fatal 錯誤。一路追查下來發現,OpenSSL 有些函數的返回碼改變了。若是照原來的錯誤碼,會走到一個被 SSLfatal
擋路的地方。因而調整了下返回碼。
如今 ssl_session_fetch_by_lua*
已經能 yield 了,只是 resume 的時候會報 no cipher
的錯誤。但是我再三確認,握手時是有 cipher 傳遞的。我拿着 OpenSSL 1.1.0 和 OpenSSL 1.1.1 的源碼,就 server 處理 ClientHello 的 post process 部分的流程對比了下,發現還有一處不一樣。OpenSSL 1.1.0 裏面,關於 cipher 處理部分是在 session resumption 以後進行的。而 OpenSSL 1.1.1 爲了遵循 TLS 1.3,把 cipher 的處理挪到 session resumption 以前完成。cipher 處理過程當中會把 cipher 給消耗掉,致使重入 session resumption 邏輯的時候,報 no cipher
錯誤。解決方法也很簡單,就是在處理 cipher 前把它保存起來,若是要 yield,就恢復它。
這麼改以後,OpenResty 全部關於 SSL session fetch 的測試在 OpenSSL 1.1.1 下終於能跑通了。
補丁的代碼能夠在這裏看到:https://github.com/spacewande...
看上去本文到這裏應該完了,可是,尚未呢! 在開發 patch 的過程當中,我發現有一個 CLIENT_HELLO_CB,好像在我每次改動的地方都能看到它。這種感受,就像一路上發現有人一直跟你同路。這種狀況下,我不禁得對這個 CB 感興趣起來。查了下發現,OpenSSL 1.1.1 起引入了一個 SSL_CTX_set_client_hello_cb
的函數。這個 callback 在 server 處理 ClientHello 時調用,並且支持 yield。這不就是打完 patch 以後的 session get callback 麼! 固然,咱們仍然須要經過 session get callback 來設置 session,但已經不須要要求 session get callback 支持 yield 了。咱們能夠在支持 yield 的 ClientHello callback 中發起非阻塞的網絡請求,而後待到 session get callback 再把準備好的 session 丟出去。這麼一來,就不須要給 OpenSSL 打 patch 了。
因而我着手修改 OpenResty 的代碼,若是當前 OpenSSL 支持 ClientHello callback,就改由 ClientHello callback 執行 ssl_session_fetch_by_lua*
裏的 Lua 代碼。咱們依然須要註冊 session get callback,但若是 ClientHello callback 已經執行了 ssl_session_fetch_by_lua*
,那麼就該 callback 就只須要返回準備好的 session 了。
ClientHello callback 還有一個做用,就是你能夠在這裏面修改 preferred cipher。在沒有 ClientHello callback 的時代,你得本身造一個。其他的 callback 仍是有覆蓋不到的路徑。固然,因爲只在有 session id 的狀況下才會執行 ssl_session_fetch_by_lua*
的邏輯,直接在該階段裏修改 preferred cipher 是行不通的。
最後,還須要修改下 Nginx 的那個補丁,讓它在 SSL_ERROR_WANT_CLIENT_HELLO_CB
下也重試。
你可能會奇怪,爲何我會在開發完對 OpenSSL 1.1.1 的 patch 以後,還要另起爐竈開發 ClientHello callback?由於一直對 OpenSSL 修修補補也不是什麼好辦法。畢竟我不是專業的 OpenSSL 開發者,改 OpenSSL 可能會改出 bug。雖然目前還須要在 Nginx 端維護一個補丁,可是維護 Nginx 的補丁相對來講簡單一些,畢竟 OpenResty 都維護了那麼多 Nginx 的補丁,也不缺能改 Nginx 的人。
使用 ClientHello callback 來執行 ssl_session_fetch_by_lua*
有個致命的問題。在以前的版本里,若是沒法用 session ticket 來 resume,會回退到嘗試用 session ID 來 resume。可是因爲 ClientHello callback 是在處理 session ticket 以前執行的,因此它沒辦法知道可否經過 session ticket 來 resume,亦沒法改用 session ID 繼續重試了。因此後來又切換會繼續 patch OpenSSL 的解決方案了。真是倒黴啊。