আপনার Android VpnService টানেল কেন ট্রাফিক রাউটিং বন্ধ করে দেয়
প্রোডাকশনে ভেঙে পড়া VpnService-এর চারটি বিশ্বাস: একবারে একটি টানেল, রুটের পরিসর, অ্যাপভিত্তিক তালিকা এবং বন্ধ করার ক্রম, প্রতিটির জন্য একটি যাচাই সহ।
Android VpnService সম্পর্কে চারটি ধারণা একবার আমার সপ্তাহের বেশিরভাগ সময় নষ্ট করেছিল, আর তার একটিও ওয়্যার প্রোটোকল নিয়ে ছিল না। অ্যাপটি বলছিল সংযুক্ত, স্ট্যাটাস বারে চাবির আইকন বসে ছিল, কিন্তু ট্রাফিক তখনও সাধারণ রুট দিয়েই বেরোচ্ছিল। আমি ধরে নিয়েই ছিলাম যে টানেলটি আমি না থামানো পর্যন্ত আমারই, যে রুটগুলো আমি যোগ করেছি সেগুলোই আমি পাচ্ছি, এবং stopSelf ডাকলে ইন্টারফেসটিও সাথে নেমে যাবে। সার্ভিসটি ঠিক ডকুমেন্টেশনের মতোই আচরণ করছিল। ভাঙা অংশটা ছিল আমার মানসিক মডেল।
মিথ 1: টানেল আপনি না থামানো পর্যন্ত টিকে থাকে
Android একবারে একটি VPN ইন্টারফেস ধরে রাখে। দ্বিতীয়টি স্থাপন করুন আর প্ল্যাটফর্ম সেটিকে সারিতে রাখে না, বরং আপনারটি নিষ্ক্রিয় করে আপনার সার্ভিসে onRevoke() ডাকে। আপনার কোড যদি কেবল বোতাম চাপলে সব গুছিয়ে ফেলে, তাহলে একটি দখল পিছনে একটি ডেসক্রিপ্টর আর একটি রিডার থ্রেড রেখে যায়, এবং পরের শুরুটি দুটোই উত্তরাধিকারে পায়।
VpnService রেফারেন্স স্পষ্টভাবে বলে যে prepare() null ফেরায় যখন অ্যাপের কাছে ইতিমধ্যে সম্মতি আছে, আর Intent ফেরায় যখন ব্যবহারকারীকে এখনও অনুমোদন করতে হয়। সেই অনুমোদন কেবল তখনই আসে যদি আপনি ইনটেন্টটি startActivityForResult() দিয়ে পৌঁছে দেন। এই হস্তান্তর বাদ দিলে establish() null ফেরায়, যা অনুমতি সমস্যা নয় বরং নীরব ব্যর্থতা হিসেবে পড়া হয়।
Always-on VPN এমন একজন মালিক যোগ করে যাকে আপনি নিয়ন্ত্রণ করেন না। সিস্টেম বুটের পরে বা ক্র্যাশের পরে টানেলটি ফিরিয়ে আনতে পারে, এবং ব্যবহারকারী নিজে হাতে চালু করা VPN অ্যাপকে এটি আগেই সরিয়ে দেয়। onRevoke() এমনভাবে তৈরি করুন যাতে এটি আপনার সাধারণ বন্ধের মতো একই সম্পদ বন্ধ করে, এবং একে কোণার কেস নয় বরং প্রধান থামার পথ হিসেবে গণ্য করুন। ব্যবহারকারীরা VPN অ্যাপ বদলায়; আপনার কোডকে তা টিকে থাকতে হবে।
মিথ 2: addRoute ঠিক যা তালিকাভুক্ত করেছেন তাই ধরে
রুটগুলো ঠিক করে কোন প্যাকেট আপনার ডেসক্রিপ্টরে পৌঁছায়, আর পরিসরের ভুল সেই ক্ষ্যাপানো পরিস্থিতি তৈরি করে যেখানে কিছু ট্রাফিক টানেলের ভিতর দিয়ে যায় আর কিছু যায় না। addRoute("0.0.0.0", 0) সমস্ত IPv4 নেয়, মানে এখন প্রতিটি প্যাকেট আপনার কাছে আসে এবং তা ফরোয়ার্ড বা ড্রপ করতে হয়। একক হোস্টের জন্য /32 প্রায় কিছুই নেয় না, আর বাকি সবকিছু এমনভাবে চলে যেন আপনার VPN কখনও ইনস্টলই হয়নি।
পরিবারগুলো আলাদাভাবে গণনা করা হয়। কেবল IPv4-এর রুট IPv6-কে অন্তর্নিহিত নেটওয়ার্কে রেখে দেয়, তাই দ্বি-স্ট্যাক হোস্ট টানেলের ভিতরে পুরোপুরি সংযুক্ত দেখানোর সময়ও বাইরে নাম সমাধান ও সংযোগ করতে পারে।
DNS একই যুক্তি অনুসরণ করে। addDnsServer() দিয়ে যোগ করা সার্ভারগুলো সেই অ্যাপগুলোর কাছে যায় যারা টানেল রিজলভার ব্যবহার করে, অথচ নিজস্ব এনক্রিপ্টেড DNS এন্ডপয়েন্ট বা হোস্টনামে সেট করা Private DNS থাকা অ্যাপ সেগুলোকে সম্পূর্ণ উপেক্ষা করে। যখন এক অ্যাপের জন্য নাম ব্যর্থ হয় আর অন্যের জন্য কাজ করে, তখন একটি রুটেও হাত দেওয়ার আগে রিজলভার পথটি দেখুন।
মিথ 3: অ্যাপভিত্তিক নিয়ম হলো এমন একটি সুইচ যা আপনি দুই দিকেই ঘোরাতে পারেন
addDisallowedApplication() এবং addAllowedApplication() দুটি মোড, দুটি ফিল্টার নয়। বিল্ডার প্রতি টানেলে একটি মডেল গ্রহণ করে, তাই যে অ্যাপ তিনটি প্যাকেজ ছাড়া সবকিছু চায় সে নিষিদ্ধ তালিকা ব্যবহার করে এবং অনুমোদিত তালিকা খালি রাখে। দুটি পারস্পরিক একচেটিয়া, আর মানুষ এটি রানটাইমে জানতে পারে যখন সেটিংস স্ক্রিন ইতিমধ্যে দুই টগলই দেখিয়ে দিয়েছে।
প্যাকেজ দৃশ্যমানতাও কামড়ায়। Android 11 এবং তার পরে আপনি কেবল সেই প্যাকেজগুলোর নাম দিতে পারেন যেগুলো আপনি দেখতে পান, তাই যে কোয়েরি আপনার অ্যাপের চালানোর অনুমতি নেই তা থেকে গড়া বাদ তালিকা নীরবে এন্ট্রি মিস করবে। তালিকাটি বিশ্বাস করার আগে প্যাকেজ ম্যানেজার আসলে কী ফেরায় তার সাথে মেলান।
ওয়ার্ক প্রোফাইল শ্রোতাদের আবার ভাগ করে। প্রোফাইলের ভিতরে চালু করা টানেল সেই প্রোফাইল ও তার অ্যাপগুলো ঢাকে, আর ব্যক্তিগত দিক নিজের সংযোগ ধরে রাখে। এটি একবার নেওয়া একটি পরিসর সংক্রান্ত সিদ্ধান্ত, আর এর জায়গা সাপোর্ট টিকিটে নয়, ইন্টারফেসে।
মিথ 4: stopSelf টানেল শেষ করে
না, করে না। ইন্টারফেসটি establish() যে ডেসক্রিপ্টর ফেরায় তার ভিতরে বাস করে, আর সেই ডেসক্রিপ্টর খোলা থাকলেও সার্ভিস অবজেক্টটি মরে যেতে পারে। প্রথমে ParcelFileDescriptor বন্ধ করুন, রেফারেন্সটি ছেড়ে দিন, রিডার ও রাইটার থ্রেডগুলো জোড়া দিন বা বাধা দিন, তারপর stopForeground(true) এবং stopSelf() সেই ক্রমে ডাকুন।
ভুলে যাওয়া ডেসক্রিপ্টরই সাধারণত কারণ যে একটি সার্ভিস নিজের থামানোর অনুরোধের পরেও টিকে আছে বলে মনে হয়, আর এটি সেই পুরোনো রুট ব্যাখ্যা করে যা তাকে যোগ করা অ্যাপের চেয়েও বেশি বাঁচে।
উল্টো ভুলটিও সমান সাধারণ: রিডার থ্রেড পড়ার মাঝখানে থাকতে থাকতেই ডেসক্রিপ্টর বন্ধ করা, তারপর সেই থ্রেডকে এমন একটি এক্সেপশন সামলাতে ছেড়ে দেওয়া যার জন্য কেউ কিছু লেখেনি। শাটডাউন ক্রমটি এক জায়গায় রাখুন এবং ব্যবহারকারীর কাজ ও onRevoke() দুটোই সেটিতে ডাকুক।
টানেল মৃত মনে হলে কী দেখবেন
টানেল ওঠার আগে দীর্ঘজীবী সকেটগুলো protect() দিয়ে সুরক্ষিত করুন। এটি ছাড়া টানেলের নিজস্ব পরিবহন আবার টানেলের ভিতরে রুট হয়ে যায় এবং সংযোগটি এমনভাবে আটকে যায় যা দেখতে হুবহু সার্ভার ত্রুটির মতো। কেবল দূরবর্তী নেটওয়ার্কের পিছনে থাকা ঠিকানার বিরুদ্ধে পরীক্ষা করুন, ক্যাশে বা অন্তর্নিহিত পথ থেকে উত্তর দেওয়া কোনো পাবলিক সাইটের বিরুদ্ধে নয়।
establish()-এর ফল, রুটগুলো, বাদ দেওয়া প্যাকেজ এবং বন্ধ করার কারণ লগ করুন। ওই চারটি লাইন প্যাকেট ক্যাপচার ছাড়াই সম্মতি সমস্যা, পরিসর সমস্যা আর জীবনচক্র সমস্যাকে আলাদা করে।