經加密的 Client Hello (ECH) 是 TLS 擴充功能,可加密用戶端握手訊息中的伺服器名稱指示 (SNI) 欄位。在 Android 17 (API 級別 37) 以上版本中,系統預設支援 ECH。ECH 可防止網路中介者查看應用程式連線的主機名稱,確保使用者網路流量的隱私。
應用程式開發人員專區
如要在應用程式中採用 ECH,請按照下列步驟操作:
- 檢查網路程式庫是否支援 ECH:請確認您使用的程式庫版本支援 Android 上的 ECH。OkHttp 和 HttpEngine 即將支援這項功能。
- 設定網路安全性設定:如果程式庫支援 ECH,所有網域都會預設啟用這項功能。如要停用或強制執行 ECH,請在網路安全性設定中設定
domainEncryption元素。 - 更新目標 SDK 層級:ECH 僅適用於 Android 17 (API 層級 37) 以上版本。
程式庫開發人員專區
如果您要開發自訂 HTTP 網路程式庫或擴充現有程式庫,請與平台 API 互動,實作 ECH 支援功能。
檢查網域加密政策
查詢 ECH 設定或啟動連線前,請先呼叫 NetworkSecurityPolicy.getDomainEncryptionMode,檢查應用程式的網域加密政策。
視傳回的模式而定,處理 ECH 的方式如下:
DOMAIN_ENCRYPTION_MODE_DISABLED和DOMAIN_ENCRYPTION_MODE_UNKNOWN:請勿擷取 ECH 設定或嘗試 ECH。DOMAIN_ENCRYPTION_MODE_ENABLED和DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC:強制執行 ECH。擷取 ECH 設定,並在伺服器支援時使用 ECH。如果伺服器不支援 ECH,請啟用 ECH GREASE。
擷取 ECH 設定
如要連線至 ECH,您必須解析伺服器的 HTTPS DNS 記錄,其中包含 ECH 設定。應用程式使用系統 DNS 時,可以透過下列兩種方法之一擷取這項資料:
方法 1:使用高階 DnsResolver.query API
如果程式庫不需要自訂 DNS 解析機制,可以使用平台的高階 DnsResolver.query API。這個 API 會平行查詢 A/AAAA/HTTPS 記錄,並將結果合併為 HttpsEndpoint。
Kotlin
val resolver = DnsResolver(context, looper)
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
object : DnsResolver.Callback<HttpsEndpoint> {
override fun onAnswer(answer: HttpsEndpoint, rcode: Int) {
val record = answer.httpsRecords.firstOrNull() ?: return
val echConfigList = record.echConfigList ?: return
establishEchConnection(echConfigList)
}
override fun onError(error: DnsResolver.DnsException) { /* Handle error */ }
})
Java
DnsResolver resolver = new DnsResolver(context, looper);
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
new DnsResolver.Callback<HttpsEndpoint>() {
@Override
public void onAnswer(HttpsEndpoint answer, int rcode) {
HttpsRecord record = answer.getHttpsRecords().stream().findFirst().orElse(null);
if (record == null) return;
EchConfigList echConfigList = record.getEchConfigList();
if (echConfigList == null) return;
establishEchConnection(echConfigList);
}
@Override
public void onError(DnsResolver.DnsException error) { /* Handle error */ }
});
方法 2:使用 getAllByName 和 DnsResolver.rawQuery
對於管理自身通訊端連線和 DNS 解析管道的程式庫,您可能偏好使用標準 API 解析 IP 位址,同時另外擷取 HTTPS 記錄:
- 使用
InetAddress.getAllByName解析預設網路或Network.getAllByName的 A/AAAA 記錄。 - 使用
DnsResolver.rawQuery平行擷取原始 HTTPS 記錄。將DnsResolver.TYPE_HTTPS指定為查詢類型。
開發人員責任和極端情況
如果選擇方法 2,程式庫必須承擔額外責任,並考量邊緣情況。
- DNS 記錄剖析:您必須剖析
rawQuery的 DNS 回應原始位元組酬載,才能擷取EchConfigList。 - 處理記錄不符的情況:您必須處理 A/AAAA 和 HTTPS 查詢之間的不一致情況。
- 競爭條件:您必須同步處理平行 DNS 查詢的結果。如果其中一個查詢先於另一個查詢解析,或 HTTPS 查詢逾時,您必須適當回溯 (例如,如果 HTTPS 查詢失敗,請嘗試建立不含 ECH 的標準 TLS 連線,或使用 ECH GREASE (如果政策已啟用)。
設定 TLS
程式庫從 HttpsRecord 擷取 ECH 設定清單 (EchConfigList) 後,請使用 SSLSockets 或 SSLEngines 公用程式 API 傳遞這份清單,然後再啟動 TLS 交握。
Kotlin
fun establishEchConnection(echConfigList: EchConfigList) {
val socket = sslSocketFactory.createSocket(ipAddress, port) as SSLSocket
SSLSockets.setEchConfigList(socket, echConfigList)
socket.startHandshake()
}
Java
public void establishEchConnection(EchConfigList echConfigList)
throws IOException {
SSLSocket socket =
(SSLSocket) sslSocketFactory.createSocket(ipAddress, port);
SSLSockets.setEchConfigList(socket, echConfigList);
socket.startHandshake();
}
處理重試流程
如果伺服器的 ECH 設定不同步,交握就會失敗,並顯示 EchConfigMismatchException (javax.net.ssl.SSLException 的子類別)。伺服器可能會在拒絕要求時加入更新的 ECH 設定,您應使用這些設定建立新連線。如果伺服器提供有效的重試設定,但程式庫未嘗試重試,就必須向呼叫應用程式回報錯誤。
如要處理 ECH 重試,請擷取例外狀況並執行下列步驟:
- 在例外狀況中呼叫
EchConfigMismatchException.getPublicHostname。 - 使用
HostnameVerifier驗證傳回的公開主機名稱。 如果顯示null,請中止連線。 - 如果主機名稱驗證成功,請使用
EchConfigMismatchException.getRetryConfigList檢查更新的設定。 - 如有可用的更新設定,請使用新的
EchConfigList重新嘗試連線。
Kotlin
try {
socket.startHandshake()
} catch (e: EchConfigMismatchException) {
val publicName = e.publicHostname ?: throw e
if (hostnameVerifier.verify(publicName, socket.session)) {
val retryConfigList = e.retryConfigList
if (retryConfigList != null) {
retryConnection(retryConfigList)
}
} else {
throw e // Hostname mismatch
}
}
Java
try {
socket.startHandshake();
} catch (EchConfigMismatchException e) {
String publicName = e.getPublicHostname();
if (publicName == null) {
throw e;
}
if (hostnameVerifier.verify(publicName, socket.getSession())) {
EchConfigList retryConfigList = e.getRetryConfigList();
if (retryConfigList != null) {
retryConnection(retryConfigList);
}
} else {
throw e; // Hostname mismatch
}
}
如要進一步瞭解重試流程,請參閱 RFC 9849,特別是為何需要驗證公開名稱。