إسناد اتصالات VPN في Android: getConnectionOwnerUid مقابل /proc/net
يخفي Android 10 المسار /proc/net عن التطبيقات. لا يزال VPN يستقبل الحزم، لكن الإسناد ينتقل إلى getConnectionOwnerUid بقواعده الخاصة للـ tuple والأذونات.
في Android 10 وما بعده، يحصل التطبيق الذي يفتح /proc/net/tcp على رفض وصول بدلًا من جدول sockets. لا يهمّ ذلك واجهة VPN: تصل الحزم كما هي، لأن النفق قرار توجيه وليس إذن ملف. الذي تغيّر هو جواب سؤال أضيق: «أي تطبيق يملك هذا الاتصال؟» البديل هو ConnectivityManager.getConnectionOwnerUid، الاستعلام المدعوم لكل اتصال، والاختيار بين العادة القديمة والواجهة الجديدة يعتمد على أي من ثلاث مهام لديك فعلًا: استقبال الحزم، أو إسناد تدفّق، أو جمع الاستهلاك.
طريقتان للإجابة عن سؤال المالك
قبل API 29، كان بوسع عملية ذات امتياز أو root أن تقرأ جداول sockets في النواة، وتطابق الرباعية وتتعرّف على UID المالك. وكان ذلك يمنح جردًا على مستوى الجهاز في ملف واحد. ومنذ API 29 أصبحت رؤية نظام الملفات مقيّدة، والمسار المدعوم هو استعلام لكل اتصال بالبروتوكول والعنوان المحلي والعنوان البعيد.
| Question | Legacy socket tables | getConnectionOwnerUid |
|---|---|---|
| Who owns this flow? | Scan and match the tuple | Ask once per tuple |
| Device-wide inventory? | Yes, when readable | No, one flow at a time |
| Historical usage? | No | No; that is NetworkStatsManager |
| Works without root? | No on modern releases | Yes, for authorized callers |
المقارنة ليست «قديم مقابل جديد». إنها ثلاث مهام مختلفة كانت تُقضى بملف واحد قابل للقراءة، وتُقضى الآن بثلاث واجهات مختلفة وثلاثة عقود مختلفة.
العنوان المحلي هو طرف التطبيق
تأخذ دالة التوقيع بروتوكولًا وعنوان socket محليًا وعنوان socket بعيدًا. «المحلي» يعني جهة التطبيق في الاتصال، لا الطرف الذي يصادف أنه مصدر الحزمة لحطة الفحص. الحزمة القادمة من الخادم البعيد إلى التطبيق لها مصدرها ووجهتها معكوسان بالنسبة إلى هذا التعريف.
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
int uid = cm.getConnectionOwnerUid(
OsConstants.IPPROTO_TCP, appLocalAddress, appRemoteAddress);خطأ الاتجاه هذا هو السبب الأكثر شيوعًا لأن يعيد استعلام سليم جوابًا خاطئًا أو ألّا يعيد شيئًا. سجّل الطرفين قبل الاستدعاء وتحقّق من الزوج مقابل الاتصال الذي فتحه التطبيق فعلًا. عائلة العنوان مهمة أيضًا: العنوان الممثّل بـ IPv4 والعنوان الأصلي IPv6 زوجان مختلفان، وكذلك تدفّقات TCP و UDP على المنافذ نفسها.
التفويض والنتائج المجهولة جزء من العقد
الاستعلام مقصور على المستدعين المصرّح لهم، ومنهم تطبيق VPN النشط للمستخدم المعني. لا يمكن لتطبيق مراقبة لا علاقة له أن يفترض قدرته على تعداد اتصالات ملف تعريف آخر، والاستدعاء غير المصرّح به قد يرمي استثناءً أمنيًا بدل أن يعيد خطأً لطيفًا. تعامل مع النتيجتين صراحة.
غياب المالك ليس اسم حزمة. قد يكون التدفّق أُغلق بين الحزمة والاستعلام، أو يكون الزوج ناقصًا، أو قد لا يُسمح للمستدعي ببساطة أن يعرف. تخزين أول جواب ناجح وإعادة استخدامه لاحقًا ينسب اتصالًا جديدًا إلى تطبيق قديم، وهذا أسوأ من الإبلاغ عن المجهول.
حين يفشل الإسناد، تبقى الحركة بحاجة إلى قرار: احجبها حيث تفرض السياسة ذلك، أو اسمح بنطاق تفهمه فقط، أو أظهر الثغرة. هذا الاحتياط ينتمي إلى التصميم لا إلى تعليق.
المحاسبة سؤال مختلف
يعرض NetworkStatsManager الاستهلاك وفق قواعد وصوله الخاصة. يستطيع أن يقول كم نقل UID خلال فترة؛ ولا يستطيع أن يقول مالك حزمة حيّة عشوائية. تصف قدرات الشبكة خصائص شبكة، لا دليلًا لـ sockets. خلط الثلاثة ينتج لوحات تبدو متسقة وليست كذلك.
في الحالة البسيطة لتحديد التطبيقات التي تستخدم النفق، تعبّر قوائم التطبيقات المسموح بها والممنوعة المدعومة في Android عن السياسة دون كشط أي شيء. احتفظ بالإسناد لكل تدفّق للحالات التي تحتاجه فعلًا، وارصد ميزانية لتنفيذ مصان يعالج الاتجاه والأذونات والحالات المجهولة معًا.
اختر الأداة حسب المهمة ثم اختبر الحواف
إن كان الشرط اختيار النفق، فاستخدم قائمة المنصة. وإن كان الإسناد الحيّ، فاستخدم استعلام المالك واعتبر كل زوج لا جواب له مجهولًا. وإن كانت المجاميع التاريخية، فاستخدم stats API. ثم اختبر الحواف الثلاثة التي تكسر التنفيذات الساذجة: حزمة في الاتجاه المعاكس، وتدفّق يُغلق قبل أن تستعلم عنه، ومستدعٍ ليس هو VPN النشط.
لا شيء من هذا يتطلب تعطيل SELinux أو عمل root للهاتف أو جعل جداول النواة مقروءة عالميًا. القيد هو المقصود: نقلت المنصة الإسناد خلف عقد كي يكون الجواب متاحًا للمكوّن الذي يحتاجه شرعًا، ولا لسواه.