למה מנהרת ה-VpnService שלכם באנדרואיד מפסיקה לנתב תעבורה
ארבע אמונות לגבי VpnService שנשברות בייצור: מנהרה אחת בכל פעם, היקף הנתיבים, רשימות לפי אפליקציה וסדר הכיבוי, עם בדיקה לכל אחת.
ארבע הנחות לגבי Android VpnService עלו לי פעם ברוב השבוע, ואף אחת מהן לא עסקה בפרוטוקול הרשת. האפליקציה אמרה מחובר, סמל המפתח ישב בשורת המצב, והתעבורה עדיין יצאה בנתיב הרגיל. המשכתי להניח שהמנהרה שלי עד שאעצור אותה, שהנתיבים שהוספתי הם הנתיבים שאקבל, ושקריאה ל-stopSelf תוריד את הממשק איתה. השירות התנהג בדיוק כמתועד. החלק השבור היה המודל המנטלי שלי.
מיתוס 1: המנהרה נשארת למעלה עד שתעצרו אותה
אנדרואיד מחזיק ממשק VPN אחד בכל פעם. הקימו ממשק שני והפלטפורמה לא תוריד אותו לתור: היא תשבית את שלכם ותקרא ל-onRevoke() בשירות שלכם. אם הקוד שלכם מפרק הכול רק בלחיצת כפתור, השתלטות משאירה מאחור מתאר ותהליך קריאה, וההפעלה הבאה יורשת את שניהם.
מסמכי VpnService אומרים במפורש ש-prepare() מחזיר null כשהאפליקציה כבר קיבלה הסכמה, ו-Intent כשהמשתמש עדיין צריך לאשר. ההסכמה הזו מגיעה רק אם תמסרו את הכוונה דרך startActivityForResult(). דלגו על המסירה ו-establish() מחזיר null, מה שנקרא ככשל שקט ולא כבעיית הרשאות.
Always-on VPN מוסיף בעלים שאינכם שולטים בו. המערכת יכולה להחזיר את המנהרה לאחר אתחול או לאחר קריסה, והיא מקדימה אפליקציית VPN שהמשתמש הפעיל ביד. בנו את onRevoke() כך שיסגור את אותם משאבים שהכיבוי הרגיל שלכם סוגר, והתייחסו אליו כאל נתיב עצירה ראשי ולא כמקרה קצה. משתמשים מחליפים אפליקציות VPN; הקוד שלכם חייב לשרוד את זה.
מיתוס 2: addRoute לוכד בדיוק את מה שרשמתם
הנתיבים קובעים אילו מנות מגיעות למתאר שלכם, וטעות בהיקף יוצרת את המקרה המעצבן שבו חלק מהתעבורה עוברת במנהרה וחלק לא. addRoute("0.0.0.0", 0) לוקח את כל ה-IPv4, כלומר כל מנה מגיעה כעת אליכם וצריך להעביר אותה הלאה או לזרוק אותה. מסכה של /32 עבור מארח בודד כמעט ולא לוקחת כלום, וכל השאר מתנהג כאילו ה-VPN שלכם מעולם לא הותקן.
המשפחות נספרות בנפרד. נתיב של IPv4 בלבד משאיר את IPv6 ברשת הבסיסית, כך שמארח דו-סטאק עדיין יכול לפתור ולהתחבר מחוץ למנהרה בעודו נראה מחובר לגמרי מבפנים.
DNS הולך באותו היגיון. שרתים שהוספו עם addDnsServer() מגיעים לאפליקציות שמשתמשות בפותר של המנהרה, בעוד אפליקציה עם נקודת קצה DNS מוצפנת משלה או Private DNS המוגדר לשם מארח מתעלמת מהם לגמרי. כששמות נכשלים לאפליקציה אחת ועובדים לאחרת, בדקו את מסלול הפותר לפני שאתם נוגעים ולו בנתיב אחד.
מיתוס 3: כללים לפי אפליקציה הם מתג שאפשר להפוך לכל כיוון
addDisallowedApplication() ו-addAllowedApplication() הם שני מצבים, לא שני מסננים. הבונה מקבל מודל אחד לכל מנהרה, ולכן אפליקציה שרוצה הכול חוץ משלוש חבילות משתמשת ברשימה האסורה ומשאירה את הרשימה המותרת ריקה. השניים סותרים זה את זה, ואנשים מגלים את זה בזמן ריצה אחרי שמסך ההגדרות כבר הציע את שני המתגים.
נראות חבילות נושכת גם כן. באנדרואיד 11 ואילך אפשר לנקוב רק בחבילות שאתם רואים, ולכן רשימת החרגות שנבנתה משאילתה שאפליקציה שלכם אינה רשאית להריץ תפספס רשומות בשקט. השוו את הרשימה למה שמנהל החבילות באמת מחזיר לפני שאתם סומכים עליה.
פרופילי עבודה מפצלים שוב את הקהל. מנהרה שהופעלה בתוך הפרופיל מכסה את הפרופיל ואת האפליקציות שלו, בעוד הצד האישי שומר על הקישוריות שלו. זו החלטת היקף שמתקבלת פעם אחת, ומקומה בממשק המשתמש ולא בכרטיס תמיכה.
מיתוס 4: stopSelf מסיים את המנהרה
לא. הממשק חי במתאר שמוחזר מ-establish(), ואובייקט השירות יכול למות בעוד המתאר נשאר פתוח. סגרו קודם את ה-ParcelFileDescriptor, שחררו את ההפניה, הצטרפו לתהליכי הקריאה והכתיבה או קטעו אותם, ואז קראו ל-stopForeground(true) ול-stopSelf() בסדר הזה.
מתאר שנשכח הוא הסיבה הרגילה לכך ששירות נראה כאילו שרד את בקשת העצירה של עצמו, והוא מסביר נתיב מיושן שחי יותר מהאפליקציה שהוסיפה אותו.
הטעות ההפוכה נפוצה באותה מידה: לסגור את המתאר כשתהליך קריאה נמצא באמצע קריאה, ואז להשאיר את התהליך הזה להתמודד עם חריגה שאיש לא כתב לה טיפול. החזיקו את רצף הכיבוי במקום אחד ותנו גם לפעולת המשתמש וגם ל-onRevoke() לקרוא לתוכו.
מה לבדוק כשהמנהרה נראית מתה
הגנו על שקעים ארוכי חיים עם protect() לפני שהמנהרה עולה. בלי זה, התעבורה של המנהרה עצמה מנותבת חזרה לתוך המנהרה והחיבור נתקע באופן שנראה בדיוק כמו תקלת שרת. בדקו מול כתובת שקיימת רק מאחורי הרשת המרוחקת, לא מול אתר ציבורי שעונה מהמטמון או מהמסלול הבסיסי.
תעדו את התוצאה של establish(), את הנתיבים, את החבילות המוחרגות ואת סיבת הכיבוי. ארבע השורות האלה מפרידות בין בעיית הסכמה לבעיית היקף ולבעיית מחזור חיים בלי לכידת מנות.