ההגנה של VpnService נכשלת כשיש כבר VPN אחר פעיל
אנדרואיד מאפשר ממשק VPN אחד לכל משתמש. הפעילו את בדיקות prepare, establish, protect והבעלות לפי הסדר, ואז בחרו ביטול או החרגה לפי אפליקציה.
האפליקציה שלכם קוראת ל-VpnService.prepare, מקבלת null, בונה מנהרה, והמתאר נראה תקין, אבל שום דבר לא מגיע לשרת. או ש-prepare מחזיר Intent, המשתמש מאשר אותו, ו-establish עדיין נכשל. שני התוצאות בדרך כלל אומרות אותו דבר: אפליקציה אחרת כבר מחזיקה בממשק ה-VPN של המשתמש הזה, וקריאות protect שלכם מסמנות sockets שהמנהרה שלכם לא יכולה להשתמש בהם. עברו על הבדיקות שלמטה לפי הסדר ועצרו בראשונה שנכשלת, כי שלב מאוחר לא יכול לתקן שלב קודם.
בדקו את מקום ה-VPN לפני שאתם נוגעים ב-protect
אנדרואיד נותן לכל משתמש בדיוק ממשק VPN פעיל אחד. זהו כלל של הפלטפורמה, לא הרשאה שהאפליקציה שלכם יכולה לבקש, ולכן שני לקוחות שרוצים שניהם להיות המנהרה לא יכולים להתקיים יחד.
VPN עם הפעלה תמידית שהוגדר עבור אפליקציה אחרת הוא המקרה הקשה ביותר, כי המערכת מחזיקה את האפליקציה הזו מחוברת ודוחה השתלטות. מסנן תעבורה כמו AdGuard, לקוח ארגוני או VPN צרכני יכול להחזיק את המקום בזמן שהמשתמש שכח שהוא מותקן. בקשו מהמשתמש לפתוח את ההגדרות ולקרוא את הרשומה שמופיעה תחת VPN; השם שם הוא הבעלים שאתם מתחרים בו. עד שהרשומה הזו תנותק או שהגדרת ההפעלה התמידית תימחק, ה-VpnService שלכם לא יכול להיות הממשק.
שלב 1: קראו מה prepare מחזיר
VpnService.prepare עונה על שאלה אחת: האם האפליקציה הזו רשאית להיות ה-VPN? הוא מחזיר null כשההסכמה כבר קיימת, ו-Intent כשהמשתמש עדיין צריך לאשר אותה, שאותו עליכם להפעיל עם startActivityForResult. התייחסו להחזרת null כאל הרשאה להמשיך, לא כהוכחה ש-establish יצליח.
Intent intent = VpnService.prepare(context);
if (intent != null) {
startActivityForResult(intent, REQUEST_VPN);
}אם מסלול ה-Intent חוזר והמשתמש סירב, prepare ימשיך להחזיר Intent בניסיון הבא. תעדו את קוד התוצאה במקום לנסות שוב בלופ, כי הסכמה שנדחתה היא החלטת משתמש, לא שגיאה חולפת.
שלב 2: אשרו שהממשק שייך לאפליקציה שלכם
אחרי establish, ה-ParcelFileDescriptor שאתם מקבלים הוא הוכחת הבעלות שלכם, וכדאי לבדוק אותו לפני כל עבודה על sockets. מתאר שהוא null או כבר סגור אומר שהממשק מעולם לא נוצר, גם אם לא הופיעה חריגה. השוו את שם הממשק מה-builder לזה שהמערכת מדווחת עבור ה-VPN הפעיל, בעזרת dumpsys במכשיר בדיקה. אם הם שונים, המנהרה שבניתם אינה המנהרה שנושאת תעבורה.
adb shell dumpsys connectivityאותו חוסר התאמה מסביר סמל מחובר עם תעבורה מתה. המערכת מציגה את המנהרה של האפליקציה האחרת, בעוד הקוד שלכם מחזיק מתאר של ממשק שהוא לא מחזיק בו.
שלב 3: קראו ל-protect לפני connect
ברגע שהממשק קיים, sockets שהאפליקציה שלכם פותחת מנותבים למנהרה כברירת מחדל. התעבורה החיצונית לא יכולה לפעול לפי הכלל הזה, אחרת החבילה העטופה תחזור לממשק, ולכן VpnService protect מסמן socket כדי לעקוף את ה-VPN ולהשתמש ברשת שמתחת. הקריאה מחזירה boolean, והתעלמות ממנה היא הדרך להגיע ל-socket שנראה בריא ולא נושא דבר.
קראו ל-protect לפני connect או bind, אף פעם לא אחרי. ההגנה חייבת להיות במקום לפני שה-socket מחובר או נקשר, כי socket מחובר כבר בחר את המסלול שלו ו-protect מאוחר לא מזיז אותו באופן אמין. אם protect מחזיר false, עצרו ובדקו שוב בעלות: הוא נכשל כשהממשק לא שייך לאפליקציה שלכם, וזה בדיוק המצב ש-VPN אחר משאיר. החזרת false היא סימן לחזור לשלב 1, לא לנסות שוב את אותו socket.
DatagramSocket socket = new DatagramSocket();
if (!vpnService.protect(socket)) {
return;
}
socket.connect(serverAddress, serverPort);שלב 4: קשרו תהליך כשה-socket לא נוח
יש תעבורות שהן לא socket יחיד: ספרייה עשויה לפתוח כמה, או שאולי תרצו שכל חיבור בתהליך עזר ישתמש באותה יציאה. ConnectivityManager.bindProcessToNetwork קושר את התהליך לרשת נבחרת, וה-setProcessDefaultNetwork הישן עשה את אותו דבר לפני שנחשב מיושן. בקשו את החיבור שמתחת עם NET_CAPABILITY_NOT_VPN כדי שהקשירה תבחר את הרשת שמתחת למנהרה שלכם ולא את המנהרה עצמה.
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();קשירת תהליך אינה VPN שני. היא בוחרת יציאה לאותו תהליך, מה שמועיל לתעבורה שלכם וחסר תועלת להרצת שתי מנהרות בבת אחת.
תקנו את המכשיר של המשתמש, לא את מסלול הקוד
אין API שמאפשר לשתי אפליקציות לחלוק את מקום ה-VPN, ולכן התיקון שייך למצב המכשיר. הציעו למשתמש שני מסלולים מוחשיים: נתקו או מחקו את ההפעלה התמידית של האפליקציה האחרת כדי ש-prepare שלכם יוכל להשתלט, או פתחו את ההגדרות לפי אפליקציה של אותה אפליקציה והחריגו את האפליקציה שלכם מהמנהרה שלה. ההחרגה משאירה את המסנן פועל לכל השאר ומאפשרת לאפליקציה שלכם להשתמש בחיבור הרגיל, וזה לעתים קרובות התוצאה שהמשתמש באמת רוצה.
כתבו את שני המסלולים בתיעוד התמיכה שלכם, כי משתמשים ממעטים לדעת איזו אפליקציה מחזיקה ב-VPN. בקשו מהם לשלוח את שם ה-VPN הפעיל מההגדרות ואת התוצאות של prepare, establish ו-protect מהלוגים שלכם, ותוכלו לומר תוך דקה איזה שלב נכשל. אחר כך אמתו עם תעבורה אמיתית: חיפוש שם, חיבור TCP ובקשה אחת דרך המנהרה, לא רק סמל מחובר. מתאר תקין ומחוון ירוק מוכיחים בעלות במקרה הטוב, ולעולם לא שהתעבורה שרדה את כללי הניתוב.
אם כל ארבע הבדיקות עוברות והתעבורה עדיין מתה, הבעלות כבר לא הבעיה שלכם. עברו לניתוב ולנגישות השרת, ובדקו את אותם אישורים ממכשיר שבו לא מותקנת אפליקציית VPN.