Encrypted Client Hello (ECH) هي إضافة إلى بروتوكول أمان طبقة النقل (TLS) تعمل على تشفير حقل "إشارة اسم الخادم" (SNI) في رسالة تأكيد الاتصال من العميل. في Android 17 (المستوى 37 من واجهة برمجة التطبيقات) والإصدارات الأحدث، تكون ميزة ECH مفعّلة تلقائيًا. تساعد ميزة ECH في الحفاظ على خصوصية زيارات المستخدمين على الويب من خلال منع الجهات الوسيطة على الشبكة من الاطّلاع على أسماء المضيفين التي يتصل بها التطبيق.
لمطوّري التطبيقات
لاستخدام ميزة ECH في تطبيقك، اتّبِع الخطوات التالية:
- تحقَّق من توفّر دعم ميزة ECH في مكتبة الشبكات: تأكَّد من أنّك تستخدم إصدارًا من المكتبة يتيح ميزة ECH على Android. سيتوفّر الدعم قريبًا في OkHttp وHttpEngine.
- اضبط إعداد أمان الشبكات: تكون ميزة ECH مفعّلة تلقائيًا لجميع
النطاقات إذا كانت مكتبتك تتيحها. إذا كنت بحاجة إلى إيقاف ميزة ECH أو فرضها،
اضبط العنصر
domainEncryptionفي إعداد أمان الشبكات. - عدِّل مستوى حزمة تطوير البرامج (SDK) المستهدَفة: لا تتوفّر ميزة ECH إلا على Android 17 (المستوى 37 من واجهة برمجة التطبيقات) والإصدارات الأحدث.
لمطوّري المكتبات
إذا كنت تطوّر مكتبة شبكات HTTP مخصّصة أو توسّع مكتبة حالية، عليك تنفيذ دعم ميزة ECH من خلال التفاعل مع واجهات برمجة التطبيقات الخاصة بالمنصّة.
التحقّق من سياسة تشفير النطاق
قبل طلب إعدادات ميزة ECH أو بدء الاتصالات، تحقَّق من سياسة
تشفير النطاق الخاصة بالتطبيق من خلال استدعاء
NetworkSecurityPolicy.getDomainEncryptionMode.
استنادًا إلى الوضع الذي يتم عرضه، تعامَل مع ميزة ECH على النحو التالي:
DOMAIN_ENCRYPTION_MODE_DISABLEDوDOMAIN_ENCRYPTION_MODE_UNKNOWN: لا تجلب إعدادات ميزة ECH أو تحاول استخدامها.DOMAIN_ENCRYPTION_MODE_ENABLEDوDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: افرض استخدام ميزة ECH. استرجِع إعدادات ميزة ECH واستخدِمها إذا كان الخادم يتيحها. إذا كان الخادم لا يتيح ميزة ECH، فعِّل ميزة ECH GREASE.
استرجاع إعدادات ميزة ECH
للاتصال باستخدام ميزة ECH، عليك حلّ سجلّ نظام أسماء النطاقات (DNS) الخاص ببروتوكول HTTPS للخادم الذي يحتوي على إعدادات ميزة ECH. عندما تستخدم التطبيقات نظام أسماء النطاقات (DNS) الخاص بالنظام، يمكن استرجاع هذه البيانات باستخدام إحدى الطريقتَين التاليتَين:
الطريقة 1: استخدام واجهة برمجة التطبيقات DnsResolver.query عالية المستوى
إذا كانت مكتبتك لا تتطلّب آليات مخصّصة لحلّ نظام أسماء النطاقات (DNS)، يمكنك استخدام
واجهة برمجة التطبيقات DnsResolver.query عالية المستوى الخاصة بالمنصّة. تُجري واجهة برمجة التطبيقات هذه طلبات بحث متوازية
عن سجلّات 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)، قد تفضّل حلّ عناوين IP باستخدام واجهات برمجة التطبيقات العادية أثناء جلب سجلّ HTTPS بشكلٍ منفصل:
- يمكنك حلّ سجلّات A وAAAA باستخدام
InetAddress.getAllByNameللشبكة التلقائية أوNetwork.getAllByName. - يمكنك استرجاع سجلّ HTTPS الأولي بالتوازي باستخدام
DnsResolver.rawQuery. حدِّدDnsResolver.TYPE_HTTPSكنوع طلب البحث.
مسؤولية المطوّر والحالات القصوى
إذا اخترت الطريقة 2، تتحمّل مكتبتك مسؤوليات إضافية وعليك مراعاة الحالات القصوى.
- تحليل سجلّ نظام أسماء النطاقات (DNS): عليك تحليل حمولة البايت الأولية لردّ نظام أسماء النطاقات (DNS)
من
rawQueryلاستخراجEchConfigList. - التعامل مع حالات عدم تطابق السجلّ: عليك التعامل مع حالات عدم التطابق بين طلبات البحث عن سجلّات A وAAAA وHTTPS.
- حالات التنافس: عليك مزامنة نتائج عمليات البحث المتوازية عن نظام أسماء النطاقات (DNS) . إذا تم حلّ أحد طلبات البحث قبل الآخر أو إذا انتهت مهلة طلب البحث عن سجلّ HTTPS، عليك الرجوع إلى الإعدادات المناسبة (على سبيل المثال، من خلال محاولة إجراء اتصال عادي باستخدام بروتوكول أمان طبقة النقل (TLS) بدون ميزة ECH إذا تعذّر طلب البحث عن سجلّ HTTPS، أو باستخدام ميزة ECH GREASE إذا كانت مفعّلة بموجب السياسة).
ضبط بروتوكول أمان طبقة النقل (TLS)
بعد أن تسترجع المكتبة قائمة إعدادات ميزة ECH
(EchConfigList) من HttpsRecord، مرِّر هذه القائمة باستخدام واجهات برمجة التطبيقات المساعدة SSLSockets أو SSLEngines قبل بدء تأكيد اتصال بروتوكول أمان طبقة النقل (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، ولا سيما سبب ضرورة المصادقة على الاسم العلني.