Android VPN: מתאר VpnService מול התקן tun של root
אפליקציות אנדרואיד אינן יכולות ליצור tun0 בעצמן: מה establish() מחזיר, איפה Binder עוצר ו-JNI מתחיל, ומתי התקן שנוצר על ידי root מנצח.
שני מסלולים יכולים להציב ממשק IP מול אפליקציית אנדרואיד, ושניהם מסתיימים באותו סוג של מתאר קובץ עם בעלים שונים מאוד. האחד עובר דרך VpnService והבונה שלו, והשני דרך מעטפת root ו-/dev/net/tun. כל קריאה במתאר הזה היא מנות יוצאות וכל כתיבה היא מנות נכנסות. מה שמפריד בין המסלולים הוא מי מחזיק במדיניות, מי שורד שדרוג מערכת הפעלה, וכמה מטבלת הניתוב אתה רשאי לגעת.
בחר במסלול הפלטפורמה אלא אם כן אתה יכול לנקוב בדבר הספציפי שהיא מסרבת לעשות. הסירוב הזה הוא בדרך כלל TAP, לכידה מחוץ למערך האפליקציות שלך, או כלל ניתוב שהמסגרת לא חושפת לעולם.
במסלול הפלטפורמה אתה מצהיר על כוונה במקום פקודות. אתה מספק לפחות כתובת אחת, את המסלולים הרצויים, שרתי DNS ורשימת אפליקציות אופציונלית, ואז שירות המערכת של אנדרואיד מבצע את ההגדרה המורשית תחת זהות משלו. לעולם אינך לומד את שם הממשק מראש, ולעולם אינך מוציא את ה-ioctl שיוצר אותו, וזו כל הנקודה.
שתי דרכים לקבל ממשק IP
מסלול ה-root הוא המתכון הוותיק. אתה פותח את /dev/net/tun, קורא ל-TUNSETIFF עם IFF_TUN, מעלה את הממשק עם ip link, ומוסיף כתובות ומסלולים ביד. כל שלב הוא שלך לעשות נכון, כולל אלה שהמסגרת הייתה מטפלת בהם בשקט. מסלול אחד חסר מייצר למשתמש את אותו דיווח כמו שרת מת.
ניפוי השגיאות שונה בדיוק כמו ההגדרה. במסלול ה-root הממשק קיים או שה-ioctl שלך החזיר שגיאה שאתה יכול לקרוא ישירות. במסלול הפלטפורמה הממשק קיים רק לאחר ש-establish() מצליח, ולכן כשל מגיע כחריגה או כמתאר null ולא כצומת התקן שאפשר לחטט בו.
מה establish() באמת מחזיר
ל-Builder.establish() יש שלוש תוצאות והן דורשות שלוש תגובות שונות. מתאר פירושו שהממשק מוגדר ושהוא שלך לשימוש, null פירושו שהבקשה לא עברה, וחריגה שנזרקת פירושה ששירות המערכת דחה אותה. איחוד null והצלחה לערך boolean אחד הוא הדרך הנפוצה ביותר שבה אפליקציה מדווחת על מנהרה עובדת שלעולם אינה נושאת מנות. מדריך Builder מפרט את דרישת הכתובת, וכדאי לקרוא אותו לפני שאתה מאשים את השרת.
את הכיוונים קובע המתאר, לא הפרוטוקול שלך. קריאות נותנות לך מנות יוצאות שכבר נבחרו ל-VPN, וכתיבות מזריקות מנות נכנסות חזרה לממשק. שכבת התעבורה שלך היא שמצפינה, מאמתת וממסגרת את המנות האלה, כי הפלטפורמה עוצרת בגבול המנה.
Binder הוא צעד אחד, JNI הוא ציר אחר
בקשת בונה עוברת אל המערכת דרך Binder, ואיפשהו מתחת לזה הפלטפורמה קוראת לקוד מקורי כדי להגדיר את ההתקן. אלה שני מנגנונים שונים שנערמים בתוך קריאה אחת. Binder הוא תקשורת בין-תהליכית בין האפליקציה שלך לשירות מערכת, בעוד JNI הוא הגשר מ-Java לקוד מקורי בתוך תהליך. קריאת עץ AOSP של אנדרואיד 9 הופכת את המחסנית למילולית, וכדאי לזכור ששמות מחלקות פנימיים ונתיבי קבצים זזים בין מהדורות.
מה שלא זז הוא החוזה שאתה רשאי להסתמך עליו: הבונה, המתאר שהוא מחזיר ומחזור החיים של השירות סביבו. שום אפליקציה לא צריכה לקרוא לעסקת שירות פנימית או ליצור מחדש צומת התקן כדי לקבל ממשק. אם הקוד שלך תלוי בשם מחלקה של AOSP, הוא יישבר במהדורה שלא בדקת.
TUN נושא IP, TAP נושא Ethernet
TUN הוא ממשק IP. TAP הוא ממשק Ethernet, וההבדל הזה קובע אם גישור אפשרי בכלל. להתקן TUN אין כתובת MAC ואין כותרת Ethernet להעביר, ולכן לקוח TUN לא יכול להצטרף לגשר שכבה 2, איך שלא ייכתבו המסלולים.
VpnService נותן לך את גרסת ה-IP, שהיא מה ש-VPN ברמת האפליקציה צריך ומה שהוא יכול לתמוך בו בלי מחסנית Ethernet גולמית. אם העיצוב שלך דורש גישור, מסלול הפלטפורמה לא יביא אותך לשם, ומסלול ה-root יעשה זאת רק אם הליבה והרשאות ההתקן ישתפו פעולה.
איפה מסלול ה-root עדיין מנצח
שלוש משימות דוחפות אנשים ממסלול הפלטפורמה. גישור שכבה 2 צריך TAP. לכידת תעבורה מאפליקציות מחוץ לטווח ה-VPN שלך צריכה נראות שהמסגרת לא מעניקה. ניתוב מדיניות שהבונה לא חושף לעולם, כמו טבלה נפרדת ליעד אחד, צריך כללים שאתה מתקין בעצמך.
העלויות מגיעות מאוחר יותר: bootloader לא נעול או תמונת root, מדיניות SELinux שזזה מתחתיך, והתקן שמפסיק להתנהג כראוי אחרי OTA. מתכון עם root גם לא יכול להיות מופץ דרך הפצת אפליקציות רגילה, ולכן רוב הצוותים שומרים אותו כמסלול אבחון פנימי ולא כמוצר.
בחירה בין השניים
התחל מהממשק שאתה צריך, לא מהקוד שיש לך. אם התשובה היא התקן TUN עם מסלולים מוגבלים לאפליקציה, מסלול הפלטפורמה קצר יותר ושורד שדרוגים. אם התשובה כוללת מסגרות Ethernet, לכידה של אפליקציות אחרות או כללי ניתוב שהבונה דוחה, אתה בשטח root וכדאי לתקצב את זה כעלות תפעולית.
בדיקה שמכריעה את רוב הוויכוחים: בנה קודם את מסלול הפלטפורמה, רשום את תוצאת establish() בנפרד ממצב הסשן המרוחק, ואמת מנה אחת בכל כיוון. רק אז החלט אם היכולת החסרה מצדיקה לעזוב את המסלול הנתמך.