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

מיתוס: אם tun0 קיים, VPN פעיל

אפליקציות VPN לאנדרואיד הן מימושים של VpnService שמעבירים למערכת מתאר קובץ tun. שם הממשק מגיע ממי שיצר אותו: OpenVPN מייצר בדרך כלל tun0, PPTP ו-L2TP מייצרים ppp0, WireGuard קורא לממשק שלו על שם המינה, ומינה IPsec עשויה שלא להציג שם מקובל כלל. לכן קריאת /proc/net/dev או מניית אובייקטי NetworkInterface מחפשת מוסכמה, לא עובדה.

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

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

מיתוס: ping מוצלח פירושו שהמינה מעבירה תעבורה

ping למארח פנימי מצליח בכל פעם שקיימת דרך למארח הזה. עם מינה מפוצלת, המארח עשוי להיות נגיש דרך Wi-Fi בעוד המינה פעילה ועושה משהו אחר לגמרי. עם VPN לפי אפליקציה, האפליקציה שמבצעת את ה-ping עשויה להיות מוחרגת מהמינה, כך שהדהוד ICMP יוצא מחוצה לה וחוזר, מה שמוכיח את ההיפך ממה שהבדיקה מניחה.

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

מה הפלטפורמה מדווחת בפועל

מ-API 21 והלאה, ConnectivityManager.getAllNetworks() מחזיר כל רשת שהמערכת מכירה, ו-getNetworkCapabilities(network) מדווח מה הרשת הזו מצהירה. רשת VPN נושאת TRANSPORT_VPN. מעבר על כל הרשתות עונה על שאלה אחת: האם יש כרגע VPN פעיל במכשיר הזה?

שאלה שנייה וצרה יותר דורשת קריאה אחרת. getActiveNetwork() מחזיר את הרשת שהתעבורה של האפליקציה תשתמש בה עכשיו, ובדיקת אותה יכולת עליה עונה: האם התעבורה שלי תעבור דרך VPN? השתיים יכולות לחלוק, והמחלוקת מאלפת. VPN שפעיל אך אינו נוכחי מצביע על ניתוב לפי אפליקציה או על תצורת מינה מפוצלת, לא על מינה חסרה.

Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
    && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

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

NetworkInfo היה אותו מיתוס עם API

ConnectivityManager.TYPE_VPN עם NetworkInfo הוא הדור השני של הבדיקה הזו, והוא מיושן מסיבה. בגרסאות מודרניות האובייקט המוחזר עבור VPN יכול להיות null בעוד המינה מחוברת, כך שקוד שמתייחס ל-null כ«אין VPN» מדווח בשקט שלילי שגוי במכשירים שבהם התשובה חשובה ביותר.

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

הגיבו לקריאות חוזרות במקום לסקור

רשמו NetworkRequest שדורש TRANSPORT_VPN ומסיר NET_CAPABILITY_NOT_VPN, ואז קראו ל-registerNetworkCallback עם הבקשה הזו. אז onAvailable ו-onLost אומרים לכם מתי ההצהרה משתנה, וזה האירוע שבאמת מעניין אתכם. בטלו את רישום הקריאה החוזרת כשהרכיב נעצר, אחרת האפליקציה מחזיקה הפניה למסך מת והקריאה החוזרת ממשיכה לירות.

NetworkRequest req = new NetworkRequest.Builder()
    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
    .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
    .build();
cm.registerNetworkCallback(req, callback);

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

מה התשובה עדיין לא יכולה לספר לכם

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

לכן תעדו מה הבדיקה ראתה, לא רק את הערך הבוליאני. תעדו את מזהה הרשת, את התעבורות המוצהרות ואם הרשת הזו הייתה פעילה. דוח שאומר «לא זוהה VPN» חסר תועלת, ואילו דוח שאומר שרשת הצהירה TRANSPORT_VPN אך getActiveNetwork() החזיר Wi-Fi מצביע ישירות על ניתוב או על רשימת החרגות, וזה תיקון שמישהו יכול לעבוד עליו.

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