在 Zoraxy 加入 TLS SNI 功能
Toby
Toby
最近我在開發的 Zoraxy v3

在我開發 Zoraxy 之前我也不知道 https 證書這東西對於 reverse proxy 伺服器來說到底有多複雜。一開始的時候因為我都是用一個域名(domain name),所以 reverse proxy 裡面也是只需要處理一個主機名稱(host name)。而初代的 Zoraxy 則是直接用 TLS hello info 裡面的 server name 作為 certificate 的 key 來找到合適的 certificate 並回傳給客戶端,所以時間複雜度(time complexity)而言是 O(1) 的速度。換成程式碼大概長這樣:

http.ListenAndServeTLS(":443", "server.crt", "server.key", nil)

到後來發現 virtual directory 帶來的麻煩後,Zoraxy v2 加入了 sub-domain 的支援。為了解決找 certificate 的問題, v2 的做法是讓使用者把每個 sub-domain 都 map 到一張 certificate 上面,所以結果就是同一個域名下會有很多張證書(例如說 a.example.com 會有 a.example.com 的證書、b.example.com 會有 b.example.com 的證書)。這樣設計的好處是一來找 certificate 的演算法比較簡單,二來我們可以根據 hello info 的 server name 進行也是 O(1) 時間複雜度的 certificate lookup,不用任何存取 file system 的 loop 便可以直接找到證書。

if fileExists(helloInfo.ServerName+".pem") && fileExists( helloInfo.ServerName+".key") {
  //Direct hit
  pubKey = helloInfo.ServerName+".pem"
  priKey = helloInfo.ServerName+".key"
}

然後在 Zoraxy v2 用了這一年多之後就又出現問題了。coauthor 的其中一張證書裡面包含了幾個不同的 domain 跟 subdomain,也有使用者的證書是含 wildcard 的,也有是舊版用 Common Name 來定義(而不是 DNS entry)來標記 host name 的,結果就是一大堆 issue 就出現在 repo 上面。

SNI 的基本概念

SNI 的原理大概就是由伺服器端跟據 TLS Hello Info 的 Server Name 來自動回傳合適的 certificate 給 Client (這裡因為 Zoraxy 是一個 http proxy,那我就以瀏覽器為 client 的例子)。對於很直接的域名例如說 v1 跟 v2 裡面出現的,基本上就是只要把 certificate map 到一個 string 轉 certificate 的資料結構裡面就可以了。但是對於 v3 之後的複雜案例,則是需要一些更複雜的邏輯來處理。

相信來得我部落格的人不是工程師應該都是技術大佬,所以我就直接把 code 拿出來講好了。這裡是 Zoraxy v3 TLS 解釋器裡面最重要的 function

func(m * Manager) CertMatchExists(serverName string) bool {
  for _, certCacheEntry: = range m.LoadedCerts {
    if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
      return true
    }
  }
  return false
}
func(m * Manager) GetCertByX509CNHostname(serverName string)(string, string) {
  for _, certCacheEntry: = range m.LoadedCerts {
    if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
      return certCacheEntry.PubKey, certCacheEntry.PriKey
    }
  }

  return "", ""
}

這裡有兩個很重要的點,就是

  • 雖然說現代的 certificate 一般都是用 DNS 作為儲存 host name 的欄位,但是有一些較舊的 certificate / certificate generator 還是使用 Common Name (通稱 CN)來放 host name
  • 由於從 x509 Certificate 抓出來的 host name 有機會是 wildcard、甚至在上面還會有 exclusion rules,所以是沒辦法把所有可能性都抓出來建 map 進行 O(1) time complexity 的搜尋

所以唯一能做來加速 O(n) 運算的就只有 in-memory 的核對跟 caching 了(哭喔

(噢,你說這兩個 function 可以整合做一個? O(n) + O(n) 的 time complexity 都是 O(n) 沒差啦,而且那樣又不符合 clean code 設計原則了)

Zoraxy v3 的做法總結

所以 Zoraxy v3 保留了原先 v1 跟 v2 的做法,並在 v1 跟 v2 都沒有 hit 的時候再跑上面那兩段 function 來檢查有哪張證書是能用的,如果都沒有就會掉到去最下面使用 fall-back certificate 。雖然說這樣子 worst case 的 TLS certificate resolve time complexity 掉到了 O(n),但是一般而言對於像我的網站這種簡單 user case 一般都會落在 O(1) 的速度,再加上使用了 in-memory 的 certificate validation (就是預先把 certificate 都載入到 RAM 裡面),所以就算是 O(n) 速度還算是可以接受的水平。如果之後還是覺得慢的話,還可以加入基於 sync.Map 的 cache 來把最近 resolve 過的 hostname 和對應的 certificate 塞進去,但是現在來說這速度已經夠用了。