OpenVPN হেল্পার বাইনারি Android 10-এ ব্যর্থ: অনুমতি প্রত্যাখ্যাত
Android 10 অ্যাপটি নিজের স্টোরেজে লেখা একটি হেল্পার চালাতে অস্বীকার করে। logcat পড়ুন, হেল্পারটি নেটিভ লাইব্রেরি হিসেবে দিন এবং বান্ডল বিল্ড ঠিক করুন।
অ্যাপটি Android 9-এ কাজ করত এবং Android 10 আপডেটের পর ওই একই ফোনে মারা যায়। আপনার প্রোফাইল অক্ষত, সার্টিফিকেট একই, আর ক্র্যাশ কোনো প্রমাণীকরণ ট্রাফিকের আগেই আসে। logcat প্রথমে একটি নাল পয়েন্টার দেখায়, আর তার কয়েক লাইন ওপরে থাকে Cannot run program ...: error=13, Permission denied। ওই বার্তাটি একটি ফাইল চালানোর বিষয়ে, আপনার কনফিগারেশন পড়ার বিষয়ে নয়।
ত্রুটি 13 আসলে কী প্রত্যাখ্যান করছে
Android 10, API স্তর 29, অ্যাপগুলোকে তাদের নিজের ডেটা ডিরেক্টরিতে লেখা ফাইল চালানো থেকে বিরত করল। নিয়মটি লেখা-তারপর-চলনযোগ্য স্টোরেজকে লক্ষ্য করে, যা ঠিক একটি অ্যাসেট-কপি লঞ্চার তৈরি করে। ics-openvpn কোরের পুরোনো বিল্ড প্রথম চালনায় APK থেকে একটি openvpn এক্সিকিউটেবল আনপ্যাক করে এবং তা ProcessBuilder দিয়ে চালু করে। আনপ্যাক এখনও সফল হয়; প্রত্যাখ্যাত হয় এক্সিকিউটটি। API 29 লক্ষ্য করা অ্যাপ পুরোনো রিলিজসহ আসা ডিভাইসেও নিয়মটি উত্তরাধিকারে পায়, তাই কোনো আপডেট এমন বিল্ড ভাঙতে পারে যা কেউ ছোঁয়নি।
প্রত্যাখ্যানে একটি ফোল্ডার নামের প্রতি নির্দিষ্ট কিছু নেই। অ্যাপ তার ডেটা ডিরেক্টরির অধীনে তৈরি করা ফাইল পথ যাই হোক অ-চলনযোগ্য হিসেবে বিবেচিত হয়, তাই getFilesDir(), getCacheDir() এবং একটি কাস্টম উপ-ডিরেক্টরি — সবই একইভাবে ব্যর্থ হয়।
হেল্পার ক্র্যাশকে প্রোফাইল সমস্যা থেকে আলাদা করুন
উপর থেকে স্ক্রল না করে প্রসেস শুরুর আশপাশের লগ পড়ুন। যে সিগনেচারটি খুঁজছেন তা হল error=13 লাইনটি, আর তার ওপরের স্ট্যাক ট্রেস সাধারণত যে লঞ্চার মেথড ProcessBuilder ডেকেছিল তার নাম বলে:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"হেল্পার কখনও শুরু না হলে আপনি OpenVPN লগ আউটপুট একেবারেই দেখবেন না: কোনো সাইফার নেগোশিয়েশন নেই, কোনো পুশড রুট নেই, কোনো DNS কনফিগারেশন নেই। ওই লাইনগুলো যদি দেখা যায় আর ব্যর্থতা পরে আসে, তবে বাইনারি চলেছে এবং সমস্যা প্রোফাইল, সার্ভার বা রাউটিং স্তরে। ওই একটি পার্থক্যই .ovpn এডিট করতে যত সময় নষ্ট হতো তা অনেক বাঁচায়। এটি আরও বলে দেয় নিচের দুটি সমাধানের কোনটি আপনার সত্যিই দরকার।
আপনি যে ডিভাইসে পরীক্ষা করছেন তার API স্তর দেখুন, আপনার বিল্ড কনফিগেরটি নয়। API 28-এর একটি এমুলেটর ইমেজ কিছুই পুনরুৎপাদন করে না, কারণ নিষেধাজ্ঞাটি কেবল 29 থেকে ওপরে কাজ করে। একই সময়ে আপনার বিল্ড ফাইলের টার্গেট SDK দেখুন: এটি বাড়ানোই নিয়মটি টেনে আনে।
adb shell getprop ro.build.version.sdkবাইনারি অন্যত্র কপি করলে কেন কাজ হয় না
স্পষ্ট ওয়ার্কঅ্যারাউন্ড, অর্থাৎ হেল্পারটিকে অন্য একটি লেখনযোগ্য ডিরেক্টরিতে লেখা, মূলটি যে কারণে ব্যর্থ হয়েছিল সেই একই কারণে ব্যর্থ হয়। Android এখনও অ্যাপের নেটিভ লাইব্রেরি ডিরেক্টরিতে ইনস্টল করা শেয়ার্ড লাইব্রেরি চালানোর অনুমতি দেয়, কারণ সেগুলো রানটাইমে লেখা হয় না, ইনস্টলার প্যাকেজ করে। ওই ডিরেক্টরিই সেই একমাত্র জায়গা যেখানে লঞ্চারের দৃষ্টি থাকা উচিত। রানটাইমে আপনি নিজে যা আনপ্যাক করেন, যাই নাম দিন, তা প্রত্যাখ্যাত শ্রেণিতেই থাকে।
বাহ্যিক স্টোরেজ আরও খারাপ: এটি noexec মাউন্ট করা, আর সেখানে রাখা হেল্পার কোনো অ্যাপ চালাতে পারে না। অ্যাপ ডেটা মুছে ফেলাও পুরোনো আচরণ ফিরিয়ে আনে না, কারণ পরের চালনায় শুধু একই অ-চলনযোগ্য কপি আবার তৈরি হয়।
লঞ্চারকে প্যাকেজ করা লাইব্রেরির দিকে নির্দেশ করুন
হেল্পারটির নাম একটি শেয়ার্ড-লাইব্রেরি নামে বদলে আপনার সমর্থিত প্রতিটি ABI-এর জন্য jniLibs এর অধীনে দিন, তারপর লঞ্চার API 28 থেকে ইনস্টল করা লাইব্রেরি পথ ফেরত দিক:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}ওই পথে সত্যিকারের ফাইল থাকতে লাইব্রেরিটি ইনস্টল করার সময় আনপ্যাক হতে হবে, তাই android:extractNativeLibs="true" কে <application> এলিমেন্টে সেট করুন। কোনো ম্যানিফেস্ট মার্জার ভিন্ন মান নিয়ে এলে তার পাশে tools:replace="android:extractNativeLibs" যোগ করুন। অ্যাপ্লিকেশন এলিমেন্ট রেফারেন্স নথিভুক্ত করে অ্যাট্রিবিউটটি কী নিয়ন্ত্রণ করে।
হাতে লঞ্চার প্যাচ করার সময় ডুপ্লিকেট লোকাল ভেরিয়েবলের দিকে খেয়াল রাখুন। পুরোনো সোর্স একই মেথডে পরে nativeAPI ঘোষণা করে, আর দ্বিতীয় ঘোষণা রানটাইম নয়, কম্পাইল ত্রুটি, তাই বিল্ড দ্রুত ব্যর্থ হয় আর ভুলটি সহজে চোখে পড়ে। আপনি যে ফোল্ডার যোগ করেন তার সঙ্গে Gradle ব্লকের ABI তালিকা মিলিয়ে রাখুন।
রিলিজ আর্টিফ্যাক্টকে টেস্ট বিল্ডের সঙ্গে মিলিয়ে দিন
একটি ডিবাগ APK টানেল তুলতে পারে, অথচ একটি রিলিজ বান্ডল তখনও ক্র্যাশ করে, কারণ বান্ডল নেটিভ লাইব্রেরি ভিন্ন আকারে প্যাক করে এবং আর্কাইভ থেকে সংকুচিত করে দিতে পারে। যদি কেবল বান্ডল-উদ্ভূত বিল্ড ব্যর্থ হয়, এটি gradle.properties -এ যোগ করুন এবং আবার বিল্ড করুন:
android.bundle.enableUncompressedNativeLibs=falseতারপর নিশ্চিত করুন ইনস্টল করা প্যাকেজ আপনার প্রত্যাশিত ABI রিপোর্ট করে, এবং দুটোই দিলে 32-বিট ও 64-বিট ডিভাইসে যাচাইটি পুনরাবৃত্তি করুন। প্যাকেজ ম্যানেজার তার বাছাই করা ABI-এর নাম বলে, তাই যে ডিভাইস 32-বিটে ফিরে গেছে তা ক্র্যাশ রিপোর্টে নয়, এখানে ওঠে:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiএকটি ABI-এর জন্য অনুপস্থিত লাইব্রেরি কেবল ওই ডিভাইসেই মূল অনুমতি ত্রুটিটি পুনরুৎপাদন করে, তাই একটি সফল পরীক্ষামূলক ফোন প্রমাণ নয়। ম্যানিফেস্ট ফ্ল্যাগ, লাইব্রেরির নাম ও বান্ডল সেটিং একসঙ্গে দিন; একটি বদলে বাকিগুলো ভুলে গেলে আপনি আবার একই ক্র্যাশে ফিরে যান। স্থায়ী সমাধান হল প্যাচ চিরকাল বহন না করে একটি রক্ষণাবেক্ষিত কোর রিলিজে যাওয়া।