איך לשלול תת-רשת ממנהרת VPN של Android
VpnService לא יכול לשלול מסלול, לכן מוסיפים את המשלים: שמונה קידומות שנוצרו עבור /8, בתוספת בדיקות IPv6, DNS וחיפוש שתופסות את השאר.
ל-VpnService של Android אין מסלול שלילה, ואף כמות של קריאת API לא תייצר אחד. Builder.addRoute() תמיד רק מוסיף יעדים למנהרה, כך שהשארת מקטע LAN מקומי פירושה שהמקטע הזה לעולם לא נכנס לרשימת המסלולים. מה שמוסיפים במקום הוא המשלים של המקטע, מבוטא כקידומות רגילות. העבודה היא אריתמטיקה, והיא נכשלת בכמה מקומות צפויים.
עברו על הבדיקות שלמטה לפי הסדר. כל אחת מניחה שהקודמת החזיקה, לכן עצרו בכשל הראשון במקום לתקן תסמינים בהמשך.
התחילו ממה שהמנהרה טוענת
העברת 0.0.0.0/0 אל addRoute() אומרת ל-Android שכל יעד IPv4 שייך למנהרה. זו הסיבה שלקוחות רבים נראים חסרי יכולת לעשות מינהור מפוצל: הם תובעים את כל מרחב הכתובות ואז מנסים לחסר ממנו. שום דבר בבונה לא מחסיר. מסלול ספציפי יותר לרשת שרציתם להשאיר מקומית עושה את ההפך ממה שאנשים מצפים, כי הקידומת הארוכה ביותר שמתאימה מנצחת והרשת הזו נוחתת בתוך המנהרה.
רשימת המסלולים היא כל המדיניות. אין מאחוריה וטו לכל יעד, ומדריך Builder אינו מפרט שום שיטה שכזו.
צרו את המשלים מאריתמטיקת קידומות
המשלים של קידומת שנשללה הוא קבוצת האחים שלה בדרך לשורש עץ הכתובות. לכל אורך מ-1 ועד האורך הנשלל, קחו את הבלוק באותו אורך שמכיל את הקידומת שלכם, הפכו את הביט שמתחת לקידומת, והפיקו את התוצאה. זה נשמע מופשט עד שמריצים אותו.
שלילת 10.0.0.0/8 מפיקה שמונה מסלולים: 128.0.0.0/1, 64.0.0.0/2, 32.0.0.0/3, 16.0.0.0/4, 0.0.0.0/5, 12.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8. אף אחד מהם אינו מכיל את 10.0.0.0/8, וביחד הם מכסים את כל השאר.
שמונה שורות מייצרות את הרשימה הזו לכל קידומת:
import ipaddress
def complement(addr, length):
base = int(ipaddress.IPv4Address(addr))
out = []
for k in range(1, length + 1):
block = base & (0xFFFFFFFF << (32 - k))
sibling = block ^ (1 << (32 - k))
out.append(f"{ipaddress.IPv4Address(sibling)}/{k}")
return out
print(complement("10.0.0.0", 8))
מספר המסלולים שווה לאורך הקידומת, וזה המספר שצריך לעקוב אחריו. שלילה אחת של /21 עולה 21 מסלולים ונשארת קריאה. עשר שלילות יכולות לעלות כמה מאות, מה שבדרך כלל אומר שצריך לשנות את תכנון הרשת ולא את המחולל.
בדקו את משפחת הכתובות ואת היקף המנהרה
מסלולי IPv4 לא עושים דבר ל-IPv6. אם המנהרה תובעת כתובת IPv6, קידומת LAN של IPv6 זקוקה למשלים משלה, שנוצר באותה אריתמטיקה על 128 ביטים. סיבה נפוצה לכך שהשלילה עובדת על הנייר בעוד המכשיר המקומי נשאר בלתי נגיש היא זו: הגיעו למארח דרך IPv6, ורק IPv4 נחתך. בדקו את שתי המשפחות לפני שתסיקו שהמתמטיקה שגויה.
ההיקף הוא החצי השני של הבדיקה. Android עוקב אחר ניתוב לפי רשת ולפי אפליקציה, כך שאותה קידומת יכולה לעבור במנהרה עבור אפליקציה אחת ולהיות ישירה עבור אחרת, בהתאם לאופן שבו הוכרז ה-VPN.
הניחו את כללי ה-DNS והאפליקציות במקום שבו הם שייכים
שלילות מסלול לא משנות את הפותר. שם מארח מקומי עדיין יכול להיענות על ידי שרת ה-DNS של המנהרה בזמן שכתובתו מנותבת מקומית, וחוסר ההתאמה מופיע כשם שנפתר למשהו שה-LAN לא יכול להגיע אליו. קבעו את מדיניות הפותר במכוון, עם דומיין חיפוש או כלל DNS מפוצל. אל תניחו שהמסלולים החליטו זאת בשבילכם.
addDisallowedApplication() מוציא אפליקציה אחת מהמנהרה; הוא לא מוציא יעד עבור האפליקציות שנשארות בפנים. protect() מאפשר לסוקט שאתם פותחים לעקוף את המנהרה, וכך מנהרה נמנעת מלתפוס את התעבורה של עצמה. אף אחד מהם לא מחליף את המשלים.
אמתו בחיפוש, לא ברשימה שלכם
לאחר שהמנהרה עולה, שאלו את המכשיר לאן כתובת באמת הולכת במקום לקרוא שוב את הרשימה שבניתם. בבילד שמגיע עם iproute2, adb shell ip route get 10.1.2.3 מציין את הממשק שהקרנל בחר. הריצו את אותה פקודה עבור כתובת ציבורית ואמתו שהיא מציינת את המנהרה.
שתי בדיקות נוספות סוגרות את המעגל. פתרו תחילה שם מקומי, ואז הריצו adb shell ip route get על הכתובת שנפתרה ודרשו ממשק שאינו המנהרה, כי adb shell ping -c 1 host.lan מוצלח מוכיח נגישות ולא את הנתיב. חזרו על בדיקת משפחת הכתובות מעל IPv6 אם המנהרה תובעת אחת. בדקו ברמת ה-API שאתם משלחים, כי האופן שבו רשתות VPN נחשפות לטבלת הניתוב השתנה בין גרסאות. רשימת מסלולים שנראית נכונה על הנייר וחיפוש שבוחר את הממשק הלא נכון הם באגים שונים, ורק השני מגיע למשתמשים.
אם רשימת השלילות ממשיכה לגדול, הפסיקו לייצר מסלולים ומספרו מחדש את הרשת המרוחקת או לצמצם את המנהרה במקום. המחולל הוא עקיפה ל-API החסר, לא כלי תכנון.