איך לבדוק בקוד אם VPN מחובר ב-Android
ל-Android אין getter למצב חיבור VPN. שאלו על יכולות הרשת, עקבו אחרי שינויי transport, והפרידו בין הסכמת האפליקציה, מצב המכשיר והוכחה מהשרת.
אי אפשר לשאול את Android אם VPN מחובר. VpnService אינו חושף getter לחיבור, ואת סמל המפתח בשורת המצב מצייר המערכת, לא משהו שהאפליקציה שלכם יכולה לקרוא. מה שכן אפשר לעשות הוא לשאול את מחסנית הקישוריות שאלה צרה יותר: באיזו רשת משתמשת התעבורה של האפליקציה הזו, והאם הרשת הזו נושאת את transport ה-VPN. המדריך הזה עובר על הבדיקות בסדר שממנע חיוביים שגויים, מפקודת shell במכשיר ועד ה-callback שאתם משאירים פועל באפליקציה.
בדקו את המכשיר מ-shell לפני שנגעתם בקוד האפליקציה
קודם כל קחו קו בסיס. adb shell dumpsys connectivity מדפיס כל רשת שהמערכת מכירה, וכל בלוק מפרט את ה-transport שלו, כך שרשת VPN מופיעה עם VPN ברשימה הזו. קראו את בלוק רשת ברירת המחדל לפני כל דבר אחר, ואז חפשו רשתות אחרות שנושאות את אותו transport.
שני פלטים עונים על שאלות שונות. עצם קיומה של רשת VPN אינו זהה לשאלה אם האפליקציה שלכם משתמשת בה, ובמכשיר עם פרופיל עבודה או משתמש שני שניהם יכולים להיות נכונים בו-זמנית בזמן שהתהליך שלכם מדבר ישירות עם האינטרנט. שורת הרשת הפעילה היא זו שקובעת מה עושות הסוקטים שלכם.
בקשו את הרשת שהאפליקציה משתמשת בה
בקוד, בדיקת היכולות היא ארבע שורות. בקשו ACCESS_NETWORK_STATE ב-manifest, ואז השוו את יכולות הרשת הפעילה ל-transport ה-VPN:
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean vpnInUse = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
התשובה צרה בכוונה: האם רשת ברירת המחדל של האפליקציה היא VPN עכשיו. היא לא אומרת דבר על איזה VPN זה, אם הוא שלכם או אם המנהרה מגיעה לקצה הרחוק שלה. התייחסו ל-true כ״תעבורה יוצאת דרך מנהרה כלשהי״ והחזיקו את החשבונאות שלכם בנפרד. ערך בוליאני שקט שמשמעותו שלושה דברים בעת ובעונה אחת הוא הסיבה שמסך הגדרות בסופו של דבר מדווח שה-VPN כבוי בזמן שהמשתמש גולש דרכו.
כשבמקום זה אתם צריכים לדעת אם קיימת רשת VPN כלשהי, עברו באיטרציה על cm.getAllNetworks() ובדקו כל סט יכולות. שתי התשובות נפרדות במכשיר שמריץ VPN לפי אפליקציה שמכסה אפליקציות אחרות אבל לא את שלכם, ופער זה הוא המקור לרוב דוחות הבאגים שאומרים שהאפליקציה לא רואה VPN שברור שהוא דלוק.
עקבו אחרי שינויים במקום לסקור
סקירה בטיימר היא הצורה הלא נכונה לשאלה הזו, כי שינויי מצב מגיעים כאירועים. רשמו בקשה שמתאימה ל-transport ה-VPN ותנו ל-framework לומר לכם מתי רשת מתאימה מופיעה, נעלמת או משתנה:
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(request, callback);
רשת VPN לעולם אינה נושאת NET_CAPABILITY_NOT_VPN, ולכן ההסרה המפורשת מתעדת את הכוונה. בלעדיה הבקשה עדיין מתאימה, אבל האדם הבא שיקרא את הקוד צריך להבין זאת בעצמו.
הרישום שייך לתחום שזקוק לתצפית, בדרך כלל מסך או שירות חזית. בטלו את הרישום ב-onStop או ב-onDestroy, ואל תשאירו callback מחובר ל-activity שכבר איננה. אפליקציה בלי רכיב גלוי היא מארחת גרועה למתבונן קבוע, וה-callbacks ששכחתם הם אלה שקורסים אחרי שינוי תצורה.
הפרידו בין שלוש שאלות
רוב הבלבול בתחום הזה נובע מהתייחסות לשלוש בדיקות שונות כאל אחת. הן משתמשות ב-API שונות, וכל אחת יכולה להיות נכונה בזמן שהאחרות שגויות.
| Question | How you answer it | What a yes means |
|---|---|---|
| Did the user grant VPN consent to my app? | VpnService.prepare() returns null | Consent exists; the tunnel may still be down |
| Is the device sending default traffic through a VPN? | TRANSPORT_VPN on the active network | Something is tunneling, not necessarily your app |
| Did this request arrive through the VPN? | Source address seen by the server | Proof for that connection, nothing wider |
השורה השלישית היא זו שאנשים מנסים לענות עליה בצד הלקוח, ואין לענות עליה שם. תנו ללקוחות VPN מאגר כתובות משלהם וקראו את כתובת המקור בשרת; זו ההוכחה היחידה ששורדת כשמשתמש מחליף מנהרה באמצע הפעלה.
סדר אימות ששולל חיוביים שגויים
קוד דוגמה ישן שעדיין נמצא בתפוצה קורא ל-getNetworkInfo(ConnectivityManager.TYPE_VPN). הקריאה הזו מיושנת, ובכמה גרסאות היא החזירה null גם כשה-VPN היה מחובר, ולכן כל כך הרבה קטעים מועתקים נראים שבורים במכשיר שעובד. בדיקות יכולות נמנעות מהמלכודת ומתנהגות אותו דבר מ-Android 5.0 והלאה. אם אתם מתחזקים בסיס קוד שעדיין קורא לה, החליפו את הקריאה במקום לעטוף אותה ב-fallback, כי ה-fallback הוא מה שיצר את התשובה השגויה מלכתחילה.
קיצור דרך אחד ראוי לאזהרה. השוואה בין הכתובת הציבורית שמדווחת שירות חיצוני לכתובת היציאה של ה-VPN אכן מוכיחה משהו על הנתיב, אבל היא מוסיפה בקשת צד שלישי לכל בדיקה, תלויה בכך שהשירות יישאר זמין, ויכולה להישמר במטמון או להיחסם. החזיקו אותה כאבחון ידני, ולעולם לא כבדיקת המצב בתוך האפליקציה.
הריצו את הבדיקות בסדר הזה ועצרו בראשונה שאינה מתיישבת עם הדוח של המשתמש: רשימת transport ב-shell, יכולות הרשת הפעילה, אירועי callback בזמן שאתם מפעילים ומכבים את המנהרה, ואז כתובת המקור בצד השרת. אם כל הארבעה מתאימים והאפליקציה עדיין מתנהגת רע, הבאג נמצא במכונת המצבים שלכם ולא בתפיסת Android את הרשת.