এনক্রিপ্টেড ক্লায়েন্ট হ্যালো (ECH) গ্রহণ করা

এনক্রিপ্টেড ক্লায়েন্ট হ্যালো (ECH) হলো একটি TLS এক্সটেনশন যা ক্লায়েন্টের হ্যান্ডশেক মেসেজে থাকা সার্ভার নেম ইন্ডিকেশন (SNI) ফিল্ডকে এনক্রিপ্ট করে। অ্যান্ড্রয়েড ১৭ (এপিআই লেভেল ৩৭) এবং এর পরবর্তী সংস্করণগুলোতে ECH ডিফল্টভাবে সমর্থিত। কোনো অ্যাপ যেসব হোস্টনেমের সাথে সংযুক্ত হয়, নেটওয়ার্ক মধ্যস্থতাকারীদের তা দেখতে বাধা দেওয়ার মাধ্যমে ECH ব্যবহারকারীদের ওয়েব ট্র্যাফিককে ব্যক্তিগত রাখতে সাহায্য করে।

অ্যাপ ডেভেলপারদের জন্য

আপনার অ্যাপ্লিকেশনে ECH গ্রহণ করতে:

  1. আপনার নেটওয়ার্কিং লাইব্রেরিতে ECH সাপোর্ট আছে কিনা তা পরীক্ষা করুন : নিশ্চিত করুন যে আপনি এমন একটি লাইব্রেরি সংস্করণ ব্যবহার করছেন যা অ্যান্ড্রয়েডে ECH সাপোর্ট করে। OkHttp এবং HttpEngine-এ শীঘ্রই এই সাপোর্ট আসছে।
  2. নেটওয়ার্ক নিরাপত্তা কনফিগারেশন : ডিফল্টরূপে, আপনার লাইব্রেরি সমর্থন করলে সমস্ত ডোমেনের জন্য ECH সক্রিয় থাকে। যদি আপনার ECH নিষ্ক্রিয় বা প্রয়োগ করার প্রয়োজন হয়, তাহলে আপনার নেটওয়ার্ক নিরাপত্তা কনফিগারেশনে domainEncryption এলিমেন্টটি কনফিগার করুন।
  3. টার্গেট 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 ব্যবহার করে, তখন এই ডেটা দুটি পদ্ধতির যেকোনো একটি ব্যবহার করে পুনরুদ্ধার করা যেতে পারে:

পদ্ধতি ১: উচ্চ-স্তরের DnsResolver.query API ব্যবহার করে

যদি আপনার লাইব্রেরির জন্য কাস্টম ডিএনএস রেজোলিউশন পদ্ধতির প্রয়োজন না হয়, তাহলে আপনি প্ল্যাটফর্মের উচ্চ-স্তরের DnsResolver.query API ব্যবহার করতে পারেন। এই API-টি A/AAAA/HTTPS রেকর্ডগুলোর জন্য সমান্তরালভাবে কোয়েরি চালায় এবং ফলাফলগুলোকে একত্রিত করে একটি HttpsEndpoint তৈরি করে।

কোটলিন

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 */ }
    })

জাভা

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 */ }
    });

পদ্ধতি ২: getAllByName এবং DnsResolver.rawQuery ব্যবহার করে

যেসব লাইব্রেরি তাদের নিজস্ব সকেট সংযোগ এবং ডিএনএস রেজোলিউশন পাইপলাইন পরিচালনা করে, তাদের ক্ষেত্রে আপনি স্ট্যান্ডার্ড এপিআই ব্যবহার করে আইপি অ্যাড্রেস রেজোলিউশন করতে এবং এইচটিটিপিএস রেকর্ড আলাদাভাবে সংগ্রহ করতে পছন্দ করতে পারেন:

  1. ডিফল্ট নেটওয়ার্কের জন্য InetAddress.getAllByName অথবা Network.getAllByName ব্যবহার করে A/AAAA রেকর্ডগুলো সমাধান করুন।
  2. DnsResolver.rawQuery ব্যবহার করে সমান্তরালভাবে মূল HTTPS রেকর্ডটি পুনরুদ্ধার করুন। কোয়েরির ধরন হিসেবে DnsResolver.TYPE_HTTPS উল্লেখ করুন।
ডেভেলপারের দায়িত্ব এবং প্রান্তিক পরিস্থিতি

যদি আপনি পদ্ধতি ২ বেছে নেন, তাহলে আপনার লাইব্রেরির অতিরিক্ত কিছু দায়িত্ব এবং বিশেষ পরিস্থিতি বিবেচনা করার প্রয়োজন হবে।

  • DNS রেকর্ড পার্সিং : EchConfigList বের করার জন্য আপনাকে অবশ্যই rawQuery থেকে প্রাপ্ত DNS রেসপন্সের র ডেটা বাইট পার্স করতে হবে।
  • রেকর্ড অমিল পরিচালনা : আপনাকে অবশ্যই A/AAAA এবং HTTPS কোয়েরিগুলির মধ্যেকার অসঙ্গতিগুলি সমাধান করতে হবে।
  • রেস কন্ডিশন (Race Conditions) : আপনাকে অবশ্যই সমান্তরাল ডিএনএস লুকআপের (DNS lookups) ফলাফল সিঙ্ক্রোনাইজ করতে হবে। যদি একটি কোয়েরি অন্যটির আগে সমাধান হয়ে যায় অথবা যদি এইচটিটিপিএস (HTTPS) কোয়েরির সময়সীমা শেষ হয়ে যায়, তবে আপনাকে যথাযথভাবে ফলব্যাক (fallback) করতে হবে (উদাহরণস্বরূপ, এইচটিটিপিএস কোয়েরি ব্যর্থ হলে ইসিএইচ (ECH) ছাড়া একটি স্ট্যান্ডার্ড টিএলএস (TLS) সংযোগের চেষ্টা করে, অথবা পলিসি দ্বারা সক্রিয় করা থাকলে ইসিএইচ গ্রীজ (ECH GREASE) ব্যবহার করে)।

TLS কনফিগার করুন

লাইব্রেরিটি HttpsRecord থেকে ECH কনফিগারেশন তালিকা ( EchConfigList ) সংগ্রহ করার পর, TLS হ্যান্ডশেক শুরু করার আগে SSLSockets অথবা SSLEngines ইউটিলিটি API ব্যবহার করে এই তালিকাটি প্রেরণ করুন।

কোটলিন

fun establishEchConnection(echConfigList: EchConfigList) {
    val socket = sslSocketFactory.createSocket(ipAddress, port) as SSLSocket
    SSLSockets.setEchConfigList(socket, echConfigList)
    socket.startHandshake()
}

জাভা

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 রিট্রাইগুলো হ্যান্ডেল করতে, এক্সেপশনটি ক্যাচ করুন এবং এই ধাপগুলো অনুসরণ করুন:

  1. ব্যতিক্রমটির ক্ষেত্রে EchConfigMismatchException.getPublicHostname কল করুন।
  2. আপনার HostnameVerifier ব্যবহার করে ফেরত আসা পাবলিক হোস্টনেমটি যাচাই করুন। যদি এটি null , তাহলে সংযোগটি বাতিল করুন।
  3. হোস্টনেম যাচাইকরণ সফল হলে, EchConfigMismatchException.getRetryConfigList ব্যবহার করে আপডেট করা কনফিগারেশনগুলি পরীক্ষা করুন।
  4. যদি হালনাগাদ করা কনফিগারেশন উপলব্ধ থাকে, তাহলে নতুন EchConfigList ব্যবহার করে সংযোগটি পুনরায় চেষ্টা করুন।

কোটলিন

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
    }
}

জাভা

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 দেখুন, বিশেষ করে পাবলিক নেমের জন্য কেন অথেন্টিকেট করা প্রয়োজন