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

كائن المدير نسخة مخزّنة مؤقتاً، وليس النفق

NETunnelProviderManager يُملأ من مخزن تفضيلات يملكه النظام، لذا لا تعني خصائصه شيئاً حتى يكتمل loadFromPreferences. اقرأ connection.status قبل ذلك فتحصل على قيمة افتراضية، ولهذا يُبلّغ أول تحديث للحالة بعد إعادة التشغيل بأنه غير متصل بينما النفق يعمل. حمّل أولاً، ثم اقرأ الحالة وإعداد البروتوكول من معالج الإكمال نفسه. راقب التغييرات اللاحقة عبر NEVPNStatusDidChange بدلاً من الاستقصاء المتكرر، ولاحظ أن الإشعار يُطلق أيضاً للاتصالات التي يبدأها On Demand، حيث لا يوجد نداء من التطبيق ليعيد أي شيء. ينطبق الأمر نفسه على إعدادات البروتوكول: تُقرأ من الإعداد المحمّل، لا مما مرّره التطبيق.

النفق نفسه يعيش في عملية مزوّد Network Extension، لذا يمكن للتطبيق أن يفقد حالته دون أن يفقد الاتصال. كل ما تعرضه الواجهة يجب أن يُعاد بناؤه من المدير، لا أن يُتذكّر من الجلسة السابقة.

ترتيب البدء: احفظ، حمّل، ثم ابدأ

الاتصال القائم على المزوّد يحتاج ثلاث خطوات بالترتيب. اكتب إعداد المزوّد ونادِ saveToPreferences، ثم حمّل المدير ليصبح الإعداد المحفوظ هو ما تحمله، ثم ابدأ عبر NETunnelProviderSession.startVPNTunnel(). إن تخطيت الحفظ فليس لدى البدء ما يشغّله؛ وإن حمّلت قبل الحفظ فسيظل المدير يعكس الإعداد القديم.

startVPNTunnel يعيد قيمة منطقية تقول فقط إن الطلب قُبِل. أما أعطال مستوى المزوّد فتصل لاحقاً كتغيّر حالة إلى غير متصل، تراقبه عبر إشعار NEVPNStatusDidChange، وعلى معالج الإشعار أن يعيد قراءة connection.status بعد loadFromPreferences لأن القيمة المخزّنة قد تكون القديمة. أما أعطال مستوى الطلب فتعود فوراً من الصيغة الرامية للنداء، أو عبر معامل الخطأ الخارجي في startVPNTunnelAndReturnError:. الكود الذي يعتبر القيمة المرجعة هي النتيجة يُبلّغ عن اتصال لم يُنشأ قط. والعطل الصامت الآخر هو معرّف حزمة مزوّد لا يطابق الإضافة المثبّتة: يُرفض الطلب، ولا شيء في الرسالة يشير إلى المعرّف. وإذا كان المعرّف خاطئاً يفشل الطلب عادة فوراً، قبل ظهور أي سطر سجل من المزوّد.

إعادة تحميل التفضيلات بعد البدء هي ما يجعل شاشات الحالة صحيحة. فالمدير لا يدفع إليك إعداداً جديداً عندما يغيّره النظام.

مواد الثقة تأتي عبر الملف الشخصي، ولا تأتي من كود التطبيق أبداً

لا يملك التطبيق طريقة مدعومة لإضافة شهادة جذر إلى مخزن ثقة iOS، لذا لا يمكن لإعداد Personal VPN إلا أن يشير إلى مواد يثق بها الجهاز مسبقاً: سلسلة أنشأها ملف شخصي مثبّت، أو إدارة جهاز، أو جهة إصدار يثق بها النظام لأسباب أخرى. والتطبيق الذي يصدر جهة إصدار خاصة به عند أول تشغيل ويتوقع أن يعتمدها النظام سيفشل في التحقق داخل المزوّد، حيث لا تستطيع واجهة التطبيق رؤية السبب.

عامل هذا الحد كقيد تصميم حقيقي، وسلّم مرساة الثقة مع ملف التسجيل. وافصل مصادقة VPN عن قرار الثقة، لأنهما يفشلان في مكانين مختلفين ويتولى تصحيحهما أشخاص مختلفون.

العدادات تأتي من الواجهة، لا من واجهة VPN البرمجية

يعرض المدير الحالة والإعداد، ولا شيء يعدّ البايتات لكل نفق. إن احتجت أرقام الاستخدام فنادِ getifaddrs() واقرأ if_data للواجهة المعنية: en0 لشبكة Wi-Fi، وواجهات pdp_ip للشبكة الخلوية، وجهاز utun للنفق نفسه. وعدادات utun تصف حركة داخلية وتشمل حمولة البروتوكول.

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

التنظيف مكانه stopVPNTunnel، لا عند الإنهاء

applicationWillTerminate يمنحك ثوانٍ قليلة قبل أن يقتل النظام العملية، وهو ليس مكاناً موثوقاً لتفكيك اتصال. أرسل الإيقاف إلى stopVPNTunnel() أو تعامل معه في رد نداء الإيقاف لدى المزوّد، واحتفظ بمعرّف المدير أو الإعداد الذي بدأت به كي يستهدف الإيقاف ذلك الاتصال بدلاً من كل إعداد على الجهاز.

خزّن ذلك المعرّف بجانب الملف الشخصي الذي ينتمي إليه، لأن كائن المدير لا يسجّل أي إعداد اختاره المستخدم. وتذكّر أن الاتصال الذي يرفعه On Demand لا يقف خلفه أي نداء من التطبيق، لذا على شاشة الحالة أن تحمّل التفضيلات قبل أن ترسم أي شيء.

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