لماذا يلتقط VpnService على Android بعض حركة المرور وليس كلها
تحدد المسارات وإدخالات DNS ونطاق كل تطبيق أي الحزم تصل إلى جهاز tun، ولماذا تختفي الأجهزة المحلية، وما يرفضه المُنشئ.
لا شيء في VpnService يعترض حركة المرور من تلقاء نفسه. إنه يطلب من النظام واجهة tun، ويسلّم تلك الواجهة مجموعة من العناوين والمسارات، ومنذ تلك اللحظة ينسخ النواة كل حزمة تطابق تلك المسارات إلى واصف ملف يستطيع التطبيق قراءته. لا يرى كود التطبيق أبدًا الحزم التي لم تحددها المسارات، لذا فإن نطاق النفق سؤال يتعلق بجدول التوجيه قبل أن يكون سؤالًا برمجيًا. لهذا السبب يمكن للتنفيذ نفسه للخدمة أن يبتلع كل تدفق على هاتف ولا يبتلع سوى شريحة على آخر، ولهذا فإن إضافة مسار تُصلح أحيانًا نفقًا بدا معطلًا. ما تحصل عليه هو ما أعلنته، ناقصًا كل ما رفضه المُنشئ بصمت.
المسارات تحدد أي الحزم تصل
Builder.addRoute(prefix, prefixLength) يثبّت بادئة في جدول توجيه شبكة VPN. يعامل النظام تلك الشبكة كافتراضية للتطبيقات داخل النطاق، لذا تُستشار إدخالاتها قبل المسار الافتراضي للشبكة الفعلية، وتكفي بادئة مطابقة لتحويل تدفق. لذلك يأخذ مسار 0.0.0.0/0 عناوين IPv4 بالكامل، بما في ذلك الطابعة وهدف البث وصفحة إدارة الراوتر.
يستخدم العملاء الذين يريدون تلك التغطية دون حذف الإدخال الافتراضي الفعلي مسارين أكثر تحديدًا هما 0.0.0.0/1 و 128.0.0.0/1 بدلًا من ذلك. مساران أكثر تحديدًا يتفوقان على المسار الافتراضي، ويغطيان معًا فضاء العناوين كله، ويبقى الإدخال الافتراضي للهاتف في الجدول للمآخذ التي ما زالت مرتبطة بالشبكة الفعلية.
تأتي العناوين وإدخالات DNS من المُنشئ نفسه وتجيب عن سؤال مختلف. يمنح addAddress الواجهة عنوانًا مع طول بادئة، ويثبّت addDnsServer مُحلِّلًا تستعلم عنه التطبيقات داخل النطاق عبر الواجهة. يمكن أن توجد العائلتان على واجهة واحدة، لذا فإن مجموعة مسارات IPv4 فقط بجوار عنوان IPv6 إعداد يُقرأ بشكل صحيح ومع ذلك يسرّب. إذا وجّهت عائلة لا يوجد للواجهة عنوان فيها، فلن يكون للنواة عنوان مصدر لتلك الحزم ولن تظهر أبدًا عند الواصف.
ما يعيده establish()
يعيد establish() قيمة ParcelFileDescriptor، أو null عندما لا يكون التطبيق مصرّحًا له. قيمة null حالة موافقة وليست خطأ شبكة: يجب أن يكون VpnService.prepare() قد أعاد null بالفعل، أو يجب أن يكون المستخدم قد وافق على حوار النظام قبل الاستدعاء. يمكن سحب الموافقة أثناء تشغيل الخدمة، فتُغلق الواجهة من تحتك دون أي استدعاء راجع إلى حلقة القراءة.
الواصف غير حاجز ما لم تقل خلاف ذلك، لذا تعود القراءة فورًا بلا بيانات بدلًا من تعليق الخيط. الحلقة المكتوبة لمأخذ حاجز تحرق نواة معالجة، والحلقة التي تعتبر القراءة الفارغة قاتلة تهدم النفق في ثانية خمول. Builder.setBlocking(true) يغيّر ذلك السلوك، والثمن خيط ينتظر داخل القراءة.
يُتحقق من وسائط المسار والعنوان كقيم LinkAddress، وتُرفض وجهات البث المتعدد مثل 224.0.0.0/4 رفضًا قاطعًا. واجهة tun من نقطة إلى نقطة لا مجال بث لها أيضًا، لذا يحتاج اكتشاف البث المتعدد عبرها إلى تصميم مختلف بدلًا من مسار إضافي. أما مرجع VpnService.Builder فهو القائمة التي يجب فحصها قبل أن تستنتج أن استدعاءً قد جُهل.
نطاق التطبيقات يضيّق المجموعة لا النفق
منذ API 21 يقبل المُنشئ addAllowedApplication، كما يقبل addDisallowedApplication، والقائمتان متعارضتان داخل المُنشئ نفسه. قائمة السماح تجعل النفق موجودًا لتلك الحزم بينما يحتفظ كل شيء آخر بالشبكة الفعلية؛ وقائمة المنع تلتقط كل شيء عدا الحزم المسمّاة، وهو ما يقدّمه العميل باسم الأنفاق المنفصلة. قبل API 21 لا توجد آلية منصة إطلاقًا، وجاء سلوك كل تطبيق من عملاء طرف ثالث يفلترون حسب UID داخل مُمرّرهم الخاص. يهم هذا التمييز عند التصحيح، لأن نفق تطبيق واحد بقائمة سماح من حزمة واحدة ينتج حركة تكاد تكون معدومة، وذلك سلوك صحيح لا مسار معطل.
أيًا كان النطاق، لا يجوز أن يمر المأخذ الذي يفتحه تطبيقك إلى خادم VPN عبر النفق الذي يبنيه. VpnService.protect(socket) يربط ذلك المأخذ بالشبكة الأساسية فتتجاوز حزمه جهاز tun. إن تركت الاستدعاء، ابتلعت الواجهة المصافحة، وهي الواجهة التي يُفترض أن تُقيم المصافحة.
VpnService.setUnderlyingNetworks يغلق الحلقة بإخبار النظام أي شبكة فعلية تحمل النفق. تلك القيمة تحدد كيف يبلّغ VPN عن الاتصال للتطبيقات الأخرى، والقيمة الفارغة أو القديمة سبب روتيني لنفق متصل يعتبره بقية الهاتف دون اتصال. حدّثها عندما تتغير الشبكة الأساسية، لا عند بدء التشغيل فقط.
المجاميع الاختبارية وMTU: حيث يفشل مسار صحيح
بعد وصول الحزم فعلًا، يحدد تفصيلان على مستوى الحزمة ما إذا كان أي شيء يعمل من طرف إلى طرف. الأول هو عنوان المصدر: مع عدة عناوين للواجهة قد تصدر النواة حزمًا تحمل أولها، لذا على المُمرّر الذي يعيد كتابة الترويسات تصحيح العنوان والمجموع الاختباري الذي يغطيه.
الثاني هو نطاق المجموع الاختباري. عندما يبني التطبيق حزم TCP أو UDP بنفسه، يغطي المجموع الاختباري ترويسة زائفة تحمل عنوان المصدر وعنوان الوجهة ورقم البروتوكول وطول النقل، وإغفال أي منها ينتج حزمًا تصل وتُسقط. يبدو ذلك الفشل كمهلة بعد رد لا يأتي أبدًا، وهذا يعيد معظم الناس إلى المسارات. setMtu يثبّت MTU الواجهة، وكل قراءة تعيد حزمة IP كاملة واحدة بالضبط، لذا فإن مخزنًا مكتوبًا لتيار بايتات سيُجزّئ تحليلك لا الحزمة.
جولة تحقق تفصل الطبقات
لتجد أين أخطأ النطاق، اختبر الطبقات بالترتيب. اسأل الجهاز عن المسارات التي تحملها شبكة VPN فعليًا، ثم تحقق مما إذا كان تدفق إلى عنوان معروف يظهر كقراءة على الواصف، ثم تحقق مما إذا كانت الحزمة التي يصدرها مُمرّرك سليمة التكوين. مشكلة المسار لا تُظهر شيئًا عند الواصف، بينما مشكلة المجموع الاختباري تُظهر حزمة تصل وتتلاشى.
إذا لم يصل شيء إلى الواصف، فمجموعة المسارات هي المشتبه به لا القارئ: تأكد من البوادئ، وتأكد من وجود عنوان في العائلة نفسها، وتأكد من أن التطبيق داخل النطاق الذي ضبطته. إذا وصلت الحزم إلى الواصف لكن لم يعُد شيء، فافحص protect() وقيمة الشبكة الأساسية قبل أن تلمس مسارًا مرة أخرى. إذا أبلغ الطرف البعيد عن حزم مشوهة، فالمجموع الاختباري وMTU هما موضع البحث. مسبار واحد لكل طبقة يوفر بعد ظهيرة من التخمين.