الافتراض الخاطئ هو أن تطبيق VpnService يقلب مفتاحًا في النظام ومن ثم يصبح الهاتف محميًا. ما يحدث فعليًا أضيق من ذلك: يطلب التطبيق الموافقة، ويبني واجهة، ويقرأ الحزم ويكتبها عبر واصف ملف، ويعيد الواجهة عندما يتوقف. لا يمكن أن يبقى سوى نفق واحد كهذا حيًا في وقت واحد، ويمكن للنظام أن ينتزعه دون أن يسأل. معظم السلوك المربك الذي يلوم الناس البروتوكول عليه يأتي من نموذج الملكية هذا.

تعامل مع كل انتقال في دورة الحياة كجزء من التصميم لا كحالة استثنائية.

خرافة: التطبيق يشغّل VPN الهاتف

يبدأ التدفق بـ VpnService.prepare()، الذي يعيد Intent يجب على المستخدم قبوله، ثم يستمر بـ establish()، الذي قد يفشل بعد ذلك لأسباب لا يتحكم فيها التطبيق. إنشاء نفق ثانٍ يعطّل الواجهة الأولى ويستبدل إشعار النظام. لا يملك التطبيق في أي موضع من هذا التسلسل مفتاحًا على مستوى الجهاز؛ بل يملك الواصف الذي مُنح له والحزم التي تهبط عليه. حتى الإشعار الدائم يخص النظام لا تصميم التطبيق.

لأن الواجهة تُمنح لا تُنتزع، يمكن لأي عميل آخر أن يأخذ المكان في أي وقت. يعرف التطبيق ذلك عبر استدعاء راجع، لا عبر قيمة إرجاع.

إيقاف التطبيق قسرًا يظهر في لوحة الإشعارات للسبب نفسه: النظام يبلّغ عما منحه، لا عما اختار التطبيق عرضه.

خرافة: الموافقة تُمنح مرة واحدة

عندما يسلم النظام النفق لعميل آخر، يرسل معاملة سحب على الـ binder الذي تعيده الفئة الأساسية من onBind(). التطبيق الذي يستبدل ذلك الـ binder بواحد خاص به يجب أن يمرر معاملة السحب بنفسه، وإلا فلن يُستدعى VpnService.onRevoke() أبدًا وسيظل الخدمة تكتب في واصف لم يعد موجودًا. يمكن للمستخدم أيضًا سحب الموافقة، ويمكن للنظام إيقاف الخدمة أثناء إعادة تشغيل أو إعادة اتصال دائم التشغيل.

التعامل معها عمل ملموس لا سطر سجل: توقف عن قراءة الواصف، وأغلقه، وامسح علم الاتصال وأخبر المستخدم بالحالة التي هو فيها. ثم قرر ما إذا كان على التطبيق إعادة الإنشاء أو السؤال مرة أخرى أو الانتظار، لأن النظام قد يعيد الخدمة من تلقاء نفسه في إعداد دائم التشغيل.

التطبيق الذي يعامل السحب كاستثناء لا كحالة سيبقى يبلّغ بأنه متصل بينما تُسقط كل حزمة يكتبها، وهو أسوأ نمط فشل ممكن لأداة خصوصية.

خرافة: الواجهة تصفي حركة المرور عنك

ما يصل هو IP خام. الالتقاط أو التصفية يعني تحليل رؤوس IP و TCP و UDP و IPv6، وتتبع التدفقات وكتابة الردود عائدة إلى الواصف، وهو ما يجعل تطبيق الالتقاط مكدس شبكات صغيرًا لا مرفقًا نظاميًا. protect() موجود كي يتجاوز مقبس النقل الخاص بالتطبيق نفقَه بدلًا من التقاط نفسه، ويعيد قيمة منطقية تستحق الفحص على كل مقبس يُنشأ بعد أن يصبح النفق قائمًا.

تحدد قوائم التطبيقات المسموحة والممنوعة في الباني من يدخل حركة مروره إلى الواجهة. لا تمنح وصولًا إلى الحمولات أبعد من حزم IP التي تحملها الواجهة، ولا يوجد API يكشف نفق عميل آخر أو تسرد ملفات تعريف VPN في النظام. قدرات الشبكة يمكن أن تقول إن شبكة ما هي نقل VPN، لكن ليس لنفق من هي.

لهذا تحصل أسئلة الحالة عن التطبيقات الأخرى على إجابات متناقضة: المعلومة ببساطة غير معروضة لعمايتك.

ما تتطلبه دورة الحياة فعليًا

تتبع الحالات التي تتحكم فيها: جاهز، منشأ، مسحوب، موقوف. يجب أن تكون إعادة الاتصال تفكيكًا يتبعه إنشاء، بمالك واحد للواصف ومقبس النقل، لأن مكونين يتسابقان على الإنشاء سيتقاتلان على المكان الوحيد المتاح. بدء التشغيل في الخلفية مقيد على Android الحديث، لذا فإن بث الإقلاع ليس ضمانة؛ استخدم خدمة أمامية بإشعارها حيث يسمح مستوى API بذلك.

سجل الانتقال لا الحالة النهائية فقط، وأبقِ آلة الحالات صغيرة بما يكفي لاختبار كل مسار. الحالة التي لا يمكنك الوصول إليها في اختبار هي حالة ستلقاها في الميدان دون سجل.

اختبر دورة الحياة على جهاز

يمكن للمحاكيات تزييف الاتصال لكن ليس مسار الموافقة والسحب بشكل مقنع، لذا شغّل هذه على جهاز حقيقي مع تثبيت عميل VPN ثانٍ ليسرق المكان.

adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp

ابدأ النفق، وأكد الإشعار والواجهة، ثم وصل العميل الآخر وراقب هل يعمل استدعاء السحب الراجع وتُمسح الحالة. أوقف التطبيق قسرًا وتحقق مما إذا كان أي شيء لا يزال يبلّغ عن واصف حي.

أعد التسلسل بعد إعادة تشغيل دائم التشغيل، لأن ذلك المسار يختلف عن اتصال يبدأه المستخدم. ما تبحث عنه ليس أيقونة خضراء بل دليلًا على أن التطبيق توقف عن استخدام واصف لم يعد يملكه.

مقالات ذات صلة