ייחוס VPN באנדרואיד: getConnectionOwnerUid מול /proc/net
אנדרואיד 10 מסתיר את /proc/net מאפליקציות. VPN עדיין מקבל מנות, אבל הייחוס עובר ל-getConnectionOwnerUid, עם כללי tuple והרשאות משלו.
באנדרואיד 10 ואילך, אפליקציה שפותחת /proc/net/tcp מקבלת סירוב גישה במקום טבלת sockets. ממשק ה-VPN לא מתרגש: מנות ממשיכות להגיע, כי המנהרה היא החלטת ניתוב, לא הרשאת קובץ. מה שהשתנה הוא התשובה לשאלה צרה יותר: ״איזו אפליקציה מחזיקה בחיבור הזה?״ התחליף הוא ConnectivityManager.getConnectionOwnerUid, השאילתה הנתמכת לכל חיבור, והבחירה בין ההרגל הישן ל-API החדש תלויה באיזו משלוש המשימות באמת יש לכם: קבלת מנות, ייחוס זרימה או סיכום שימוש.
שתי דרכים לענות על שאלת הבעלים
לפני API 29, תהליך מורשה או root יכול היה לקרוא את טבלאות ה-sockets של הקרנל, להתאים את הרביעייה וללמוד את ה-UID המחזיק. זה נתן מלאי ברמת המכשיר בקובץ אחד. מ-API 29 תצוגת מערכת הקבצים מוגבלת, והמסלול הנתמך הוא שאילתה לכל חיבור עם הפרוטוקול, הכתובת המקומית והכתובת המרוחקת.
| Question | Legacy socket tables | getConnectionOwnerUid |
|---|---|---|
| Who owns this flow? | Scan and match the tuple | Ask once per tuple |
| Device-wide inventory? | Yes, when readable | No, one flow at a time |
| Historical usage? | No | No; that is NetworkStatsManager |
| Works without root? | No on modern releases | Yes, for authorized callers |
ההשוואה אינה ״ישן מול חדש״. אלו שלוש משימות שונות שקודם סופקו על ידי קובץ קריא אחד וכעת מסופקות על ידי שלושה ממשקים שונים עם שלושה חוזים שונים.
הכתובת המקומית היא הקצה של האפליקציה
חתימת המתודה מקבלת פרוטוקול, כתובת socket מקומית וכתובת socket מרוחקת. ״מקומי״ פירושו הצד של האפליקציה בחיבור, לא הקצה שבמקרה הוא מקור המנה ברגע הבדיקה. למנה שנעה מהשרת המרוחק חזרה לאפליקציה יש מקור ויעד הפוכים ביחס להגדרה הזו.
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
int uid = cm.getConnectionOwnerUid(
OsConstants.IPPROTO_TCP, appLocalAddress, appRemoteAddress);טעות הכיוון הזו היא הסיבה הנפוצה ביותר לכך ששאילתה תקינה מחזירה תשובה שגויה, או כלום. תעדו את שני הקצוות לפני הקריאה ואמתו את הזוג מול החיבור שהאפליקציה באמת פתחה. גם משפחת הכתובות חשובה: כתובת ממופה IPv4 וכתובת IPv6 מקורית הן tuples שונות, וכך גם זרימות TCP ו-UDP באותם פורטים.
הרשאה ותוצאות לא ידועות הן חלק מהחוזה
השאילתה מוגבלת לקוראים מורשים, כולל אפליקציית ה-VPN הפעילה עבור המשתמש הרלוונטי. אפליקציית ניטור לא קשורה לא יכולה להניח שהיא תמנה חיבורים של פרופיל אחר, וקריאה לא מורשית עשויה לזרוק חריגת אבטחה במקום להחזיר שגיאה ידידותית. טפלו בשתי התוצאות במפורש.
בעלים חסר אינו שם חבילה. ייתכן שהזרימה נסגרה בין המנה לשאילתה, שה-tuple חלקי, או שפשוט לא מותר לקורא לדעת. שמירת התשובה המוצלחת הראשונה ושימוש חוזר בה מאוחר יותר מייחסת חיבור חדש לאפליקציה ישנה, וזה גרוע מדיווח על לא ידוע.
כשהייחוס נכשל, לתעבורה עדיין צריך החלטה: לחסום אותה היכן שהמדיניות דורשת, לאפשר רק היקף שאתם מבינים, או לחשוף את הפער. גיבוי כזה שייך לעיצוב, לא להערה.
חשבונאות היא שאלה אחרת
NetworkStatsManager מדווח על שימוש לפי כללי הגישה שלו. הוא יכול לומר כמה UID העביר בתקופה; הוא לא יכול לומר מי הבעלים של מנה חיה שרירותית. יכולות רשת מתארות מאפיינים של רשת, לא ספריית sockets. ערבוב השלושה מייצר לוחות שנראים עקביים ואינם.
במקרה הפשוט של החלטה אילו אפליקציות משתמשות במנהרה, הרשימות הנתמכות של אפליקציות מותרות ואסורות באנדרואיד כבר מבטאות את המדיניות בלי לגרד דבר. שמרו ייחוס לכל זרימה למקרים שבאמת זקוקים לו, ותקצבו מימוש מתוחזק שמטפל בכיוון, בהרשאות ובמצבים לא ידועים יחד.
בחרו את הכלי לפי המשימה, ואז בדקו את הקצוות
אם הדרישה היא בחירת מנהרה, השתמשו ברשימת הפלטפורמה. אם זה ייחוס חי, השתמשו בשאילתת הבעלים והתייחסו לכל tuple ללא תשובה כלא ידוע. אם אלו סכומים היסטוריים, השתמשו ב-stats API. אחר כך בדקו את שלושת הקצוות ששוברים מימושים נאיביים: מנה בכיוון ההפוך, זרימה שנסגרת לפני השאילתה, וקורא שאינו ה-VPN הפעיל.
שום דבר מזה לא דורש השבתת SELinux, root לטלפון או הפיכת טבלאות הקרנל לקריאות לכולם. האילוץ הוא העיקר: הפלטפורמה העבירה את הייחוס מאחורי חוזה כדי שהתשובה תהיה זמינה לרכיב שזקוק לה באופן לגיטימי, ולאף אחד אחר.