DoH 與 DoT 把 DNS 查詢藏進了加密通道,但你連到 secret.example.com 的那一刻,主機名仍然以明文寫在 TLS 握手的第一個封包裡——任何在路徑上的人都讀得到。SNI 是 TLS 1.3 握手裡最後一截裸露的 metadata,ECH 要做的就是把它也封進加密層。
Encrypted Client Hello 進 NGINX——把最後一截明文 SNI 藏進加密握手
TLS 1.3 把幾乎整個握手都搬進了加密通道——憑證、ALPN、擴充清單,在 ServerHello 之後就全部受 AEAD 保護。唯一的例外是 ClientHello 本身:它是握手的第一個訊息,發出去時雙方還沒有共享金鑰,所以裡面所有欄位都是明文。其中最敏感的是 Server Name Indication(SNI),它告訴前端伺服器「我要連的是哪個 virtual host」,因此必須在握手最前面、在伺服器挑憑證之前就送達。這個「必須最早送、又還沒有金鑰可加密」的時序矛盾,讓 SNI 成為十年來都填不上的洞。Encrypted Client Hello(ECH)——RFC 9849,2026 年 3 月轉正的 Standards Track 文件——用一個雙 ClientHello 的結構繞過這個矛盾,而 NGINX 1.29.4 是第一個把它帶進主流 reverse proxy 的正式版本。
下面這張圖是整篇文章的骨架。一次帶 ECH 的 TLS 握手經過五個責任分明的環節:客戶端先從 DNS 的 HTTPS RR 抓到伺服器發佈的 ECHConfig,用裡面的 HPKE 公鑰把「真正的」inner ClientHello 加密,塞進一個只帶 cover domain 的 outer ClientHello,送給 client-facing server 解密。點擊任一環節,看它各自藏住或揭露哪一截 metadata、又對相鄰環節一無所知哪一塊。
click a stage above
DNS · HTTPS RR · 責任
伺服器把它的 ECHConfig(含 HPKE 公鑰、config_id、cipher_suites、public_name)序列化成 ECHConfigList,發佈在域名的 HTTPS(或 SVCB)資源記錄裡,欄位是 RFC 9848 定義的 ech SvcParam。客戶端在送握手之前先做這次 DNS 查詢,拿到公鑰。
不知道的事:哪個客戶端、會連哪個 inner 名稱。它只發佈公鑰,不參與握手本身。這也是為什麼 DNS 查詢本身必須走 DoH/DoT——否則發佈公鑰的這次查詢又洩漏了你要連的域名。
HPKE seal · 責任
客戶端用 ECHConfig 裡的 KEM(強制套件是 DHKEM(X25519, HKDF-SHA256))對伺服器公鑰做一次封裝,產出臨時公鑰 enc 與共享密鑰,再用 KDF(HKDF-SHA256)導出對稱金鑰,以 AEAD(AES-128-GCM)把整個 inner ClientHello 加密。config_id 是一個位元組,讓伺服器在多把私鑰裡快速定位該用哪一把解。
不知道的事:伺服器內部 virtual host 的拓樸。HPKE 只負責「對這把公鑰封一段密文」,它是密碼學原語,不理解 TLS 語意。
outer ClientHello · 責任
這是真正送上線的那個 ClientHello。它的 SNI 欄位填的是 cover——也就是 ECHConfig 裡的 public_name,一個刻意公開、許多網站共用的門面域名;它的 encrypted_client_hello 擴充攜帶 stage 2 產出的密文、config_id 與 enc。其餘敏感擴充也填無害的占位值。
不知道的事:它攜帶的密文內容。outer 自己讀不懂 inner——它只是一個信封,連自己裝了什麼都不知道。on-path 旁觀者讀到的就只有這一層。
inner ClientHello · 責任
這才是握手的真正意圖:真 SNI(secret.example)、真 ALPN、真擴充清單,全部在這裡。它從不單獨上線——永遠被 HPKE 封進 outer 的 encrypted_client_hello 擴充。伺服器解密成功後,會把它當作「真正的」ClientHello 拿去跑握手,挑對應的 virtual host 憑證。
不知道的事:自己被裝進哪個 cover 信封。inner 與 outer 是兩份獨立構造的 ClientHello,inner 不引用 public_name。
client-facing server · 責任
持有 ECHConfig 對應的 HPKE 私鑰。收到 outer 後,用 config_id 找到私鑰、解開 encrypted_client_hello、還原 inner ClientHello,再以 inner 的 SNI 完成 TLS 握手。在 shared-mode 下,這台同一台機器既解密、也終結 TLS、也服務內容——NGINX 1.29.4 目前只支援這個模式。
不知道的事:在 shared-mode 下沒有「不知道的事」——它什麼都解、什麼都看得到。真正的隱私分層要 split-mode(client-facing 解密後把 inner 轉給後端 origin),但那需要尚未合併進 OpenSSL 的修改。
ECH 握手鏈的五個責任邊界
ECH 把真 SNI 用 HPKE 封進密文,線上旁觀者只讀到 outer 的 cover domain,真實目的地完全不可見。
底下四節依序走:為什麼 SNI 到 TLS 1.3 還是明文(概覽)、雙 ClientHello 與 HPKE 的機制、NGINX 1.29.4 的整合面(directive 與變數),最後是 shared-mode 的邊界——也就是這套東西今天能買到什麼、買不到什麼。
SNI 為什麼到 TLS 1.3 還是明文
這個洞難補,是因為 TLS 握手的訊息順序固定:客戶端先送 ClientHello,伺服器才回 ServerHello、憑證、Finished。而 SNI 必須放在 ClientHello 裡——一台機器後面常掛幾十、上百個 virtual host 共用同一個 IP,伺服器在挑憑證、挑金鑰之前,必須先知道客戶端要連哪個主機名。這形成死結:SNI 要在握手最前面送達,但那一刻雙方還沒協商出任何共享金鑰,沒有金鑰就沒辦法加密。SNI 因此天生是明文。
TLS 1.3 已把 ServerHello 之後的所有內容(含伺服器憑證,1.2 時代還是明文的)都搬進加密通道,但 ClientHello 在金鑰協商之前送出是協議的時序本質,沒辦法靠「加密更多東西」解決。於是 SNI 成了 TLS 1.3 握手裡幾乎唯一的明文主機名洩漏點。
有人會問:DNS 不是早就洩漏了嗎?你連 secret.example.com 之前總要先查它的 IP。這正是 DoH(DNS over HTTPS,RFC 8484)與 DoT(DNS over TLS,RFC 7858)解決的事——把 DNS 查詢包進加密通道。但關上 DNS 這條路徑之後,握手裡的明文 SNI 立刻變成最顯眼的那一截:你查域名的動作藏起來了,連線時卻還是把域名大喇喇寫在第一個封包裡,成了 on-path 觀察者現成的答案。
第一次補洞的是 ESNI(Encrypted SNI),2018 年前後由 Cloudflare 等推動,思路很直接:只加密 SNI 一個欄位,其餘 ClientHello 維持明文。它一度在 Firefox 與 Cloudflare 之間實驗上線,但失敗的原因有三。第一,只加密 SNI 不夠——其他擴充(ALPN、supported groups、甚至擴充的排列順序)合起來就是一份有指紋性的 fingerprint,旁觀者仍能交叉比對推回你連的是誰。第二,沒有乾淨的「解不開時怎麼辦」機制——金鑰輪替或客戶端拿到過期 key 時握手會硬失敗,缺乏優雅的 retry。第三,對 middlebox 相容性差,部分設備看到不認識的加密欄位會直接 drop 連線。ECH 是對這三點的全面重寫:它加密「整個 ClientHello」而非一個欄位、內建 retry_configs、設計 GREASE 來餵養 middlebox。draft-ietf-tls-esni 這份文件編號沿用、設計推倒重來,被現實打磨六年後,於 2026 年 3 月以 RFC 9849 定案,配套的 DNS 發佈機制則是 RFC 9848;NGINX 1.29.4 在同一個季度跟上,背後是 OpenSSL ECH feature branch 與 defo-project 多年的鋪墊。
要標清楚的是:ECH 補的是 metadata 層的洩漏,不是內容層。TLS 早就保護你傳輸的資料,ECH 保護的是「你在跟誰說話」。對審查規避、對「連到某域名本身就敏感」的場景(醫療、異議、特定金融服務),明文 SNI 是封鎖的主要抓手——封鎖方不用解密任何內容,看 SNI 就能按域名做 selective blocking,ECH 把這個抓手拿掉。但它不取代 TLS 內容加密、也不取代 DoH/DoT,而是把三者拼成完整的隱私拼圖:少了 ECH,旁觀者仍能從明文 SNI 重建你的軌跡;少了 DoH/DoT,那次「為了拿 ECHConfig 而做的 DNS 查詢」又把域名漏回去。三層必須一起部署,這也是 ECH 落地比想像中慢的原因之一——它不是獨立開關,而是對整條鏈路都有前置要求的機制。
雙 ClientHello 與 HPKE 的封裝機制
ECH 的核心結構是兩份 ClientHello。RFC 9849 稱它們為 ClientHelloInner 與 ClientHelloOuter。inner 是你真正想送的——真 SNI、真 ALPN、真擴充清單,是握手的真實意圖。outer 是一個信封,它的 SNI 填的是 cover(public_name),其餘敏感欄位填無害的占位值,並攜帶一個 encrypted_client_hello 擴充,裡面裝的是被加密過的 inner。實際送上線的只有 outer;inner 從不單獨出現在網路上。
加密用的是 HPKE(Hybrid Public Key Encryption,RFC 9180)——一套把「非對稱金鑰封裝」與「對稱 AEAD 加密」組合起來的標準原語。流程是:客戶端從 ECHConfig 取得伺服器的 HPKE 公鑰,做一次 KEM(Key Encapsulation Mechanism)封裝,產出臨時公鑰 enc 與共享密鑰;接著用 KDF 導出對稱金鑰,再以 AEAD 把整份 inner ClientHello 加密成密文。RFC 9849 規定的強制相容套件是 DHKEM(X25519, HKDF-SHA256) 當 KEM、HKDF-SHA256 當 KDF、AES-128-GCM 當 AEAD。
送到線上的 encrypted_client_hello 擴充裡有幾個關鍵欄位。config_id 是一個位元組,不是金鑰而是「索引提示」——伺服器可能同時持有多把 ECH 私鑰(輪替期間新舊並存),它讓伺服器一眼挑出該用哪把解密,省掉逐把試的成本。enc 是 KEM 產出的臨時公鑰份額,伺服器用自己的私鑰加上 enc 就能重建共享密鑰。payload 是 AEAD 加密後的 inner 密文本體。
ClientHelloOuter {
SNI = "cover.example" // = ECHConfig.public_name,明文
ALPN = [占位值]
extensions = {
encrypted_client_hello = {
config_id = 0x0b // 一個位元組,挑哪把私鑰
enc = X25519 臨時公鑰份額
payload = AEAD_seal(key, ClientHelloInner) // inner 密文
}
...
}
}
ClientHelloInner { // 從不單獨上線
SNI = "secret.example" // 真實目的地
ALPN = ["h2"] // 真實協議
extensions = { ...真實擴充清單... }
}
// 線上旁觀者讀到的只有 ClientHelloOuter——也就是 cover.example
那 ECHConfig(伺服器公鑰)怎麼送到客戶端?答案是 DNS。伺服器把它序列化成 ECHConfigList,發佈在域名的 HTTPS 資源記錄(或 SVCB 記錄)裡,欄位是 RFC 9848 定義的 ech SvcParam;客戶端在握手前先做一次 DNS 查詢,解析出 HPKE 公鑰與 public_name。這次 DNS 查詢本身必須走 DoH/DoT,否則「為了加密 SNI 而做的查詢」反而把域名洩漏了——ECH 與加密 DNS 互補,少了任何一邊另一邊都漏。
ECHConfig 結構裡除了 HPKE 公鑰(裝在 key_config 內,含 kem_id 與 cipher_suites),還有兩個值得注意的欄位。public_name 是 cover 域名,RFC 9849 對它有嚴格的合法性要求——必須是 preferred name syntax 的合法主機名(LDH 標籤、不可是純數字、不可是 IPv4 字面值),因為當 ECH 被拒絕時,客戶端要拿伺服器憑證去驗證這個 public_name。maximum_name_length 則給客戶端做 padding 的提示:若不 padding,密文長度會隨 inner SNI 字元數變化,旁觀者就算解不開也能光看長度把候選域名縮到很小的範圍;伺服器透過這個欄位告訴客戶端「我這邊最長的主機名有多長」,所有 inner 補齊到同一長度,消滅這個 side channel——ESNI 當年在這塊也不夠嚴謹。
DNS 端還有一個實務區分:ECHConfigList 既可發在 HTTPS 資源記錄,也可發在 SVCB 記錄,兩者都帶 ech SvcParam。HTTPS RR 是 SVCB 針對 https:// 起源的專用化,多數瀏覽器查 A/AAAA 時就會順手查 HTTPS RR,所以把 ECHConfig 放進 HTTPS RR 不會多一次 round-trip。公鑰不走握手通道(那有時序死結)、改走本來就要做的 DNS 查詢,是 ECH 能在不增加握手延遲下取得伺服器公鑰的關鍵。
關於「旁觀者到底看到什麼」這件事,下面這個拉桿把同一條鏈路的兩種狀態疊在一起:左邊是 ECH 關閉時 on-path 觀察者讀到的 ClientHello(真 SNI 裸露),右邊是 ECH 開啟後同一個觀察者讀到的(只剩 public_name)。拖動中間的分隔線,看真正的 metadata 差在哪一行。
拖動中間分隔線比較 on-path 觀察者讀到的 ClientHello · ECH off ↔ on
同一條鏈路、同一個目的地,ECH off 與 on 下旁觀者讀到的明文差異
ECH 開啟後 ClientHello 的 SNI 換成 cover domain,真 SNI 僅存在 HPKE payload 密文裡。
最後一個機制細節是 GREASE(Generate Random Extensions And Sustain Extensibility)。若只有「要用 ECH 的人」才送 encrypted_client_hello 擴充,那擴充的出現本身就是「這個人在隱藏什麼」的訊號,反而讓連線更顯眼。GREASE 的解法是:即使客戶端手上沒有真實 ECHConfig(DNS 查不到、或對方不支援),它也送一個帶隨機 config_id、enc、payload 的假擴充——非 ECH 伺服器會忽略它、握手照常,旁觀者則無法區分「有沒有真 ECH」。它同時是一個持續的 middlebox 相容性測試,讓網路設備習慣這個擴充,避免將來真 ECH 上線時被當成異常 drop。
NGINX 1.29.4 的整合面:directive 與變數
機制講完,回到落地。NGINX 1.29.4 是第一個原生支援 ECH 的正式版本。但有一個明確的前置條件:ECH 還沒進穩定版 OpenSSL,要等 OpenSSL 4.0(預計 2026 年春季)才會收進去。在那之前,你必須從原始碼編譯 NGINX,並把它連結到 OpenSSL 的 ECH feature branch——不是 distro 套件管理器裝的那個 libssl。
# 1. 取 OpenSSL 的 ECH feature branch(4.0 stable 釋出前的暫時路徑)
$ git clone https://github.com/openssl/openssl
$ cd openssl && git checkout feature/ech
# 2. 編 NGINX 連結到這份 OpenSSL,而非系統 libssl
$ cd nginx-1.29.4
$ auto/configure --with-openssl=/path/to/openssl-for-nginx
$ make && make install
設定面的核心是一個新 directive:ssl_ech_file。它指向一個 PEM 檔,這個 PEM 同時包含 HPKE 私鑰(伺服器用來解密 inner ClientHello)與要發佈到 DNS 的 ECHConfig 結構。它可放在 http 或 stream 區塊,也可下放到 server;當一個 server 配了多個 ECH PEM 時,第一個被設定的 PEM 會被當作該伺服器的 ECH retry configuration——ECH 被拒絕時要回給客戶端的那份新 ECHConfig。
http {
server {
listen 443 ssl;
server_name secret.example;
ssl_certificate /etc/nginx/secret.example.crt;
ssl_certificate_key /etc/nginx/secret.example.key;
# ECH:PEM 內含 HPKE 私鑰 + 要發佈到 DNS 的 ECHConfig
ssl_ech_file /etc/nginx/ech/secret.example.ech.pem;
location / {
# 把 ECH 結果寫進 access log,方便觀測採用率
add_header X-Debug-ECH $ssl_ech_status always;
}
}
}
# PEM 裡 ECHConfig 那段需要 base64 抽出來,發佈成 DNS HTTPS RR:
// secret.example. 3600 IN HTTPS 1 . ech="AED+DQA…(ECHConfigList)"
觀測面則是兩個新變數。$ssl_ech_status 報告這次握手的 ECH 結局,共五個離散值;$ssl_ech_outer_server_name 在 ECH 被接受時揭露 outer SNI 裡那個對外的 public_name——也就是 cover 域名。把 $ssl_ech_status 寫進 access log,是評估 ECH 採用率的第一手資料。它的五個值分清楚是運維的關鍵,因為 FAILED 與 GREASE 的意義天差地遠,混為一談會讓你誤判金鑰是否輪替正確。下面這組分頁逐一拆解每個值代表握手走到了哪一步。
SUCCESS · ECH 正常運作
伺服器收到帶 encrypted_client_hello 的 outer,用 config_id 找到正確私鑰、成功解密、還原出 inner ClientHello,並以 inner 的真 SNI 完成握手。這是你部署正確時想看到的多數值——access log 裡 SUCCESS 的比例就是 ECH 的有效採用率。同時 $ssl_ech_outer_server_name 會填上客戶端用的 cover(public_name)。
NOT_TRIED · 客戶端沒嘗試 ECH
客戶端送來的根本是一個普通 ClientHello,沒有 encrypted_client_hello 擴充——可能是不支援 ECH 的舊客戶端,或它查不到你的 DNS HTTPS RR、手上沒有 ECHConfig。握手照常以明文 SNI 進行。如果你剛上線 ECH 卻看到大量 NOT_TRIED,第一個要查的是 DNS HTTPS RR 是否正確發佈、TTL 是否已生效。
FAILED · 嘗試了但解不開
客戶端送了真 ECH 擴充,但伺服器無法解密——通常是客戶端拿到的是過期或不匹配的 ECHConfig(你輪替了 HPKE 金鑰,DNS 還沒更新、或客戶端快取了舊值)。此時伺服器以 outer 的 public_name 繼續握手,並在 EncryptedExtensions 裡回 retry_configs(即第一個 ssl_ech_file 那份),客戶端可用新 config 重試。看到 FAILED 飆高,幾乎都是金鑰輪替與 DNS 發佈沒對齊。
BACKEND · split-mode 後端角色
對應 split-mode 部署裡的後端 origin 角色——前端 client-facing server 解密了 outer、把還原出的 inner 轉給後端,後端看到的是已解密的 inner ClientHello。由於 NGINX 1.29.4 目前只支援 shared-mode(解密與服務同一台),這個值在今天的標準部署裡基本不會出現;它的存在是為將來 split-mode 預留的語意位。
GREASE · 假擴充
客戶端送的是 GREASE ECH——一個帶隨機 config_id、隨機 enc、隨機 payload 的假擴充,目的是讓「用 ECH」與「不用 ECH」的連線在線上無法區分,並持續測試 middlebox 相容性。伺服器辨識出這是 GREASE、不嘗試真解密,以明文路徑握手。把 GREASE 跟 FAILED 分開很重要——前者是設計上的正常雜訊,後者是真的出錯。
$ssl_ech_status 的五個值對應握手的五種結局。運維時最該盯的對比是 FAILED(金鑰/DNS 不同步,需處理)與 GREASE(設計上的正常雜訊,不需處理)。$ssl_ech_status 的五個值對應握手的五種結局
FAILED 代表金鑰/DNS 不同步需處理,GREASE 是設計上的正常雜訊,混淆兩者會誤判輪替狀態。
把這些值串起來看一次故障排除:你發佈了 DNS HTTPS RR、配了 ssl_ech_file,但 SUCCESS 比例偏低。若 NOT_TRIED 與 GREASE 高而 FAILED 低,那不是你的問題,是客戶端還沒拿到或還不支援 ECH(DNS 傳播、客戶端版本)。反之若 FAILED 偏高,問題在你這側:HPKE 私鑰與 DNS 上的 ECHConfig 不一致,最常見的觸發是你換了 PEM 卻沒同步更新 DNS HTTPS RR,或 TTL 還沒過、客戶端仍在用舊公鑰加密。此時 $ssl_ech_outer_server_name 很有用——它告訴你客戶端拿哪個 public_name 進來,若跟預期的 cover 不符,多半是 ECHConfigList 發佈錯了域名。
retry_configs 讓金鑰輪替不致硬中斷:客戶端拿舊 config 加密失敗時,伺服器以 public_name 完成握手、附帶新 config,客戶端下次就用對。但這條 graceful 路徑只在 public_name 憑證有效時成立(客戶端會拿伺服器憑證驗證它),所以 cover 域名必須是你真持有有效憑證的名稱——這正呼應 RFC 9849 對 public_name 必須合法可驗證的要求。配合「第一個被設定的 PEM 當作 retry configuration」這條規則,金鑰輪替的正確順序是:先把新 PEM 加進設定,等 DNS HTTPS RR 的舊值過了 TTL、確定客戶端都拿到新 config,再撤舊 PEM;順序顛倒就會在傳播窗口內讓還拿舊 config 的客戶端撞 FAILED。ECHConfigList 支援同時掛多份 config,正是為輪替期間新舊並存設計的。
shared-mode 的邊界、split-mode 的缺口
NGINX 1.29.4 的 ECH 有一個必須講清楚的限制:今天只支援 shared-mode——同一台 NGINX 實例既解密 outer、又終結 TLS、又服務內容,解密與後端服務沒有分離。這是 RFC 9849 兩種拓樸裡較簡單的那一種;另一種 split-mode 把「解密 outer」與「服務 origin」分到兩台機器,client-facing server 只解開 outer、把還原出的 inner 轉給後端 origin,由後端完成握手——關鍵是它在 split-mode 下從不接觸應用層明文。
下面這張表把兩種模式的責任分工攤開——誰持有 HPKE 私鑰、誰解密、誰終結 TLS、誰看得到應用層內容。它也標出今天 NGINX 1.29.4 落在哪一格。
| 面向 | shared-mode(今天的 NGINX 1.29.4) | split-mode(尚需 OpenSSL 改動) |
|---|---|---|
| 持有 HPKE 私鑰 | 同一台 NGINX | 僅 client-facing server |
| 解密 outer ClientHello | 是(自己) | client-facing server |
| 終結 TLS | 同一台 | 後端 origin server |
| 看得到應用層明文 | 看得到(同一台全包) | client-facing 看不到,只後端看得到 |
| $ssl_ech_status 典型值 | SUCCESS / FAILED / GREASE | BACKEND(後端側) |
| 部署現況 | NGINX 1.29.4 原生支援 | OpenSSL 改動未合併 · defo-project PoC |
shared-mode 的隱私邊界止於「on-path 旁觀者」——它擋的是線上 metadata,不是解密方自己
NGINX 1.29.4 只支援 shared-mode,解密與服務在同一台;split-mode 需等 OpenSSL 端未合併的修改。
這個限制必須誠實標出:shared-mode 擋的是「on-path 旁觀者」——路徑上的 ISP、middlebox、被動監聽者看不到真 SNI;但解密那台 NGINX 自己當然看得到真 SNI、也終結 TLS、也讀得到應用層內容。所以若你的威脅模型是「不希望託管網站的前端伺服器知道使用者連哪個 inner host」,shared-mode 給不了你,那是 split-mode 的領域。對絕大多數「想把網站從審查與被動監聽裡藏起來」的場景已經足夠,因為威脅來自路徑而非伺服器本身;但對 CDN/多租戶前端這種「前端不該知道後面是誰」的拓樸,今天的 NGINX 還做不到。
為什麼 split-mode 卡在 OpenSSL 而不是 NGINX?因為它要求 client-facing server 把還原出的 inner 原封不動轉給後端、後端拿這個被轉發的 inner 當作真正的 ClientHello 繼續握手,這需要 OpenSSL 在 API 層暴露「只解密不終結」「接收外部還原的 inner」這類目前還沒有的鉤子;shared-mode 不需要,因為解密與終結在同一個 SSL context 裡。換句話說 NGINX 1.29.4 已把能做的整合都做了,瓶頸在底層密碼庫的 API 表面,目前只有 defo-project 有 PoC。等 OpenSSL 4.0 收進 ECH、後續再補 split-mode 的 API,NGINX 端的設定面才接得上。BACKEND 這個 $ssl_ech_status 值就是伏筆——shared-mode 標準部署裡你應該只看到 SUCCESS、FAILED、GREASE、NOT_TRIED 這四個。
算一筆帳。收益面:ECH 補上 TLS 1.3 握手裡最後一截明文主機名,配合 DoH/DoT 把「使用者在連哪個網站」從 on-path 觀察者眼前拿走,而且 NGINX 把整件事做成可觀測的($ssl_ech_status)。成本面不輕:你得從原始碼編譯、連結 OpenSSL feature branch(直到 OpenSSL 4.0 約 2026 年春季才進穩定版),維護 DNS HTTPS RR 與金鑰輪替的同步否則 FAILED 會飆,而且被綁在 shared-mode。還有一個更根本的限制:ECH 的防護強度等於匿名集的大小。若一個 public_name 後面只藏你一個網站,cover 本身就洩漏了真實目的地;它要靠大型前端讓很多真實網站共用同一個 cover 才兌現價值——一台孤零零的自架 NGINX 配 ECH,匿名集是 1,防護接近於零。它不是裝上去就有用的開關,而是依賴生態系規模的隱私機制。
What this enables:把 DoH/DoT 關掉的 DNS 洩漏,與 ECH 關掉的 SNI 洩漏合起來,一次帶 ECH 的 TLS 1.3 握手第一次做到「on-path 旁觀者只知道你連到某個共享 cover、無法判定後面的真實目的地」——前提是 cover 後面的匿名集夠大、DNS 與金鑰輪替保持同步、而你接受今天只有 shared-mode 可用。