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

ודאו מה ה-builder מסר לפלטפורמה

VpnService.Builder הוא רשימת הצהרות. establish() מחזיר ParcelFileDescriptor לממשק וירטואלי, והפלטפורמה שולחת ל-descriptor הזה רק תעבורה שתואמת לקידומות של addAddress ו-addRoute; addDnsServer אומר ללקוחות איזה resolver לשאול. יעד מחוץ לכל קידומת שהוספתם לא יגיע לאפליקציה שלכם לעולם, יהיה אשר יהיה מה שהממשק מבטיח. העובדה האחת הזו מסבירה את רוב הדיווחים על פיצול מנהרה שבור עוד לפני שינוי קוד.

תעדו ביומן את רשימות הכתובות, המסלולים וה-DNS שה-builder שלכם הרכיב, והשוו אותן ליעדים שנכשלים. קידומת /32 חסרה למארח ניהול נפוצה הרבה יותר ממנהרה שבורה. אם הרשימה נכונה, המשיכו; אם היא שגויה, שאר רשימת הבדיקה הוא מאמץ מבוזבז.

excludeRoute קיים רק מ-API 33

החרגה לפי יעד היא הבקשה שלרוב הופכת לשרשור תמיכה. builder.excludeRoute(IpPrefix) הגיע ב-API 33; לפני כן הקירוב היחיד הנתמך הוא להוסיף מסלולים לכל דבר מלבד הקידומת המוחרגת, כלומר לחשב בעצמכם את הקידומות המשלימות. שתי הקריאות אינן תמונת מראה: האחת מחסירה מקבוצת המסלולים של הממשק, השנייה בונה את הקבוצה מאפס, ומכשיר ישן יותר יזרוק או יתעלם בשקט מקריאה שאינו מזהה. הסתעפו לפי Build.VERSION.SDK_INT ובדקו את שני המסלולים בחומרה אמיתית.

כשאתם מחשבים השלמות, שימו לב ליישור הקידומות. החרגת /24 בתוך מסלול /16 דורשת את הקידומות המכסות שמתארות באמת את השאר, ולא /8 אחד פגום. תעדו את הרשימה הסופית וודאו שהמארח המוחרג עדיין נפתר דרך מסלול מחוץ למנהרה.

אי אפשר לבטא פרטי גישה של proxy

setHttpProxy(ProxyInfo) מפנה את הממשק ל-proxy של HTTP או SOCKS, וכתובת PAC יכולה לבחור ביניהם לכל יעד. לא ProxyInfo ולא תבנית PAC נושאים שם משתמש וסיסמה, ולכן proxy הדורש אימות נשאר מחוץ ל-API של הפלטפורמה. אפליקציות שזקוקות לכך מטמיעות בדרך כלל מחסנית מרחב משתמש, tun2socks או מנוע דומה, ועונות בעצמן לאתגר 407.

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

רשימות לפי אפליקציה ו-DNS הן גם הצהרות

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

DNS הולך באותה תבנית. addDnsServer מכריז על כתובת resolver; הוא לא מאפשר לאפליקציה שלכם לענות לשאילתות. אם אתם רוצים לפתור שמות בעצמכם, הריצו resolver ונתבו את כתובתו למנהרה. אפליקציות שמשתמשות ב-DoT או ב-DoH לשם מארח קבוע יכולות לעקוף את הכתובת שהכרזתם, ולכן התייחסו לפתרון שמות כמאמץ מיטבי אלא אם אתם מיירטים גם את נתיב השאילתה.

Manifest, always-on ומבחן ארבעת השלבים

הזכאות מוצהרת ב-manifest: BIND_VPN_SERVICE, מסנן ה-intent android.net.VpnService ושירות חזית אם המנהרה צריכה לשרוד ברקע. Always-on VPN דורש בנוסף onRevoke נקי, כי המערכת קוראת לו כשהמשתמש משנה הגדרות או שאפליקציה אחרת משתלטת.

הריצו את הבדיקות לפי הסדר ועצרו בכשל הראשון. ראשית, שפכו את רשימות הכתובות, המסלולים וה-DNS שנמסרו ל-builder. שנית, ודאו ש-establish() החזיר descriptor ושסוקט התעבורה מוגן. שלישית, בקשו יעד אחד בתוך קידומת כלולה ואחד בתוך קידומת מוחרגת. רביעית, בדקו איזה resolver באמת ענה בשאילת שם מארח שרק ה-resolver שלכם יכול לפתור.

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

הפנייה של VpnService.Builder מפרטת את כל משטח ה-builder. כל מה שצריך מעבר לכך הוא API אחר, אפליקציה אחרת או מחסנית מרחב המשתמש שלכם, וזו החלטת תכנון ולא תקלה בפיצול מנהרה.

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