לא, אפליקציית VpnService לא יכולה להדליק את ה-VPN של הטלפון
האפליקציה מחזיקה במנהרה אחת ובמחזור החיים שלה, לא במתג ה-VPN של הטלפון. אפשר לבטל את ההסכמה, רק VPN אחד רץ בכל רגע, והמנות נותרו לכם לניתוח.
ההנחה השגויה היא שאפליקציית 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
הפעילו את המנהרה, אשרו את ההתראה ואת הממשק, ואז חברו את הלקוח האחר וצפו אם הקריאה החוזרת של הביטול רצה ואם המצב מתנקה. עצרו את האפליקציה בכפייה ובדקו אם משהו עדיין מדווח על מתאר חי.
חזרו על הרצף אחרי אתחול מחדש במצב תמיד פועל, כי המסלול הזה שונה מחיבור שהמשתמש יזם. מה שאתם מחפשים אינו סמל ירוק אלא הוכחה שהאפליקציה הפסיקה להשתמש במתאר שכבר לא היה שלה.