root הוא לא מה שמאפשר לאפליקציית VPN באנדרואיד להעביר תעבורה, לחסום דליפות או לשרוד אתחול. אנשים מסכמים את כל זה כ״האפליקציה קיבלה הרשאת VPN״, וזה מסתיר שלושה מנגנונים נפרדים: הסכמה שהמשתמש נותן, ממשק חבילות שהמערכת מקימה, ומדיניות חסימה של הפלטפורמה שאינה של האפליקציה להגדיר. לבדוק עיצוב של VPN באנדרואיד פירושו לשאול על איזה מן השלושה טענה נשענת באמת, כי לכל אחד יש בעלים אחר ואופן כשל אחר.

ההסכמה מגיעה דרך פעילות מערכת

VpnService.prepare() לא מעניק דבר. הוא מחזיר intent אל פעילות ההסכמה של המערכת כשההסכמה חסרה, והאפליקציה מפעילה את הדיאלוג הזה כדי שהמשתמש יאשר את השירות. הפעלת התהליך של האפליקציה עצמה, הוספת הרשאה למניפסט, או הצגת מסך ״הפעל VPN״ מותאם אישית אינם יוצרים את ההרשאה.

ההרשאה גם מוגבלת בהיקף. היא שייכת לחבילה בפרופיל משתמש, ולכן העותק האישי ועותק העבודה הם החלטות נפרדות, ובעל מכשיר יכול להכין פרופיל מנוהל אחרת מפרופיל אישי. אם הדיאלוג לא מופיע לעולם ו-prepare() מחזיר null, ההסכמה כבר קיימת לאותה חבילה באותו פרופיל; זו אינה הוכחה שהאפליקציה מהימנה, אלא רק שההרשאה קיימת.

הממשק הוא בקשה שהמערכת מממשת

לאחר ההסכמה האפליקציה מתארת מה היא רוצה באמצעות VpnService.Builder: כתובות, מסלולים, שרתי DNS, MTU והיכן שנתמך, האפליקציות שיש לכלול או להחריג. establish() מבקש מהמערכת ליצור את ממשק המנהרה ולהפנות אליו את התעבורה המתאימה. המערכת היא הבעלים של החלטת הניתוב; האפליקציה מקבלת את מתאר הקובץ ואז חייבת לעשות משהו שימושי עם החבילות.

What you observeWho actually owns it
VPN consent dialogThe system, triggered by prepare()
Routes, DNS and app listsThe app requests them; the system applies them
One active VPN per profileThe platform; a new service replaces the old one
Always-on and block-without-VPNDevice policy enforced by the platform

הטבלה הזו היא הארכיטקטורה במקום אחד. האפליקציה משתתפת בטיפול בחבילות; היא אינה הבעלים של הממשק, של טבלת הניתוב או של מספר השירותים שרשאים לרוץ.

שקע המנהרה חייב להימלט מהמנהרה של עצמו

החיבור שנושא את התעבורה המוצפנת הוא בעצמו שקע. אם הוא מנותב חזרה אל הממשק שהאפליקציה רק יצרה, המנהרה תלויה במנהרה ונתקעת. VpnService.protect() קושר את השקע הזה לרשת הבסיסית כדי שיעקוף את הלכידה, ולכן לקוח נכון מגן על השקע החיצוני שלו לפני לחיצת היד הראשונה.

adb shell dumpsys connectivity | grep -i -A5 vpn
adb shell settings get global always_on_vpn_app
adb shell settings get global always_on_vpn_lockdown

הפקודות האלה מציגות את רשת ה-VPN המותקנת, את החבילה של הפעלה תמידית והאם הקיפאון מוגדר. אלה תצפיות לקריאה בלבד: אם הערכים לא תואמים למה שהעיצוב טוען, הטענה היא על הממשק של האפליקציה ולא על מצב המערכת.

החסימה עשויה להיות של הפלטפורמה

אנדרואיד מתעד מדיניות שחוסמת חיבורים שאינם משתמשים ב-VPN. האכיפה הזו היא חוזה של הפלטפורמה, לא אפליקציה שמנסה שוב ושוב להתחבר בלופ. המתג נמצא בהגדרות ה-VPN של המכשיר בשם חסימת חיבורים ללא VPN, ובמכשיר מנוהל הבעלים יכול להפעיל אותו עבור כל הפרופיל. ההבחנה חשובה כשאתם מעריכים טענה שיווקית: לקוח יכול להציג סמל מנעול ולהתחבר מחדש באגרסיביות בזמן שתעבורה ישירה עדיין אפשרית, או שאפליקציות מכוסות יכולות להישאר לא מקוונות מפני שהמערכת אוכפת את הקיפאון.

את סמל המנעול מציירת האפליקציה, ולכן הוא מוכיח רק שהאפליקציה מאמינה שהיא מחוברת. אם אפליקציה אחרת מאבדת גישה לרשת, הפלטפורמה מחליטה, ולכן הבדיקה השימושית היא לראות את האפליקציה ההיא נכשלת כשהמנהרה פעילה.

גם תגית ״תמיד פעיל״ מחנות האפליקציות אינה מחליפה את החוזה הזה. הפעלה תמידית וקיפאון הם הגדרות מכשיר, שלעתים קרובות מיושמות בידי בעל המכשיר, וההתנהגות שלהן לאפליקציות מוחרגות נקבעת בידי הפלטפורמה. קראו את ההגדרה, לא את התג.

ארבע שאלות לבדיקת עיצוב

ראשית, מי נתן הסכמה: איזו חבילה, איזה פרופיל, והאם זה היה המשתמש או מדיניות של מכשיר מנוהל? שנית, מה ביקשה האפליקציה: אילו מסלולים, אילו משפחות כתובות, אילו אפליקציות נכללו או הוחרגו? שלישית, מה קורה כשהמנהרה אינה זמינה: המערכת חוסמת, או שהתעבורה נופלת חזרה לרשת הבסיסית? רביעית, מה האפליקציה עושה עם החבילות שהיא מקבלת?

השאלה הרביעית היא זו שמודל ההרשאות לא יכול לענות עליה. הסכמה וממשק TUN מוכיחים שהאפליקציה יכולה לקבל תעבורה, לא שהיא מצפינה אותה, מאמתת עמית או מתאוששת נקי. אלה תכונות מימוש, ושם בדיקה ישרה מבזבזת את זמנה.

בקשת root מיותרת אינה הופכת ללגיטימית כשקוראים לתוצאה מתג הרג, ודיאלוג הסכמה אינו ביקורת אבטחה. החזיקו את שלושת האישורים בנפרד וכל טענה הופכת לניתנת לבדיקה.

מאמרים קשורים