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

שלוש החלטות, לא אחת

הסיווג עונה מי פטור. הניתוב עונה לאן החבילה המסומנת הולכת במקום. בחירת המקור עונה מה יראה הצד המרוחק ולאן התשובה צריכה לחזור.

אלה נכשלים באופן בלתי תלוי. חבילה יכולה להיות מסווגת, מנותבת מחדש לממשק הפיזי, ובכל זאת לשאת כתובת פרטית שאף נתב במעלה הזרם לא יכול להחזיר, מה שיוצר פסק זמן שנראה בדיוק כמו חסימת חומת אש. שום דבר בשלב הסיווג לא מזהיר אותך מזה. שום דבר גם בשלב הניתוב.

גם הסדר חשוב. הסיווג קורה כשהחבילה נוצרת, הניתוב קורה אחר כך, והתרגום קורה בנקודת היציאה.

אם אינך יכול לנקוב איזה משלושת השלבים שבור, אינך מבצע דיבוג, אתה מסדר מחדש כללים ומקווה.

התאם cgroup שיהיה קיים גם מחר

nftables יכול לבדוק את החברות של socket ב-cgroup v2, וזה באמת שימושי כי זה עוקב אחרי התהליך ולא אחרי היעד. המלכודת היא המזהה. אפליקציית שולחן עבודה שהופעלה מתפריט חיה לעתים קרובות ב-scope חולף ששמו משתנה בהפעלה הבאה.

nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup

העתקת שם scope מהפעלה אחת וקידודו באופן קשיח מייצרת כלל שעובד היום ומחר מפסיק בשקט להתאים. במקום זה הצב את האפליקציה בהקשר הרצה יציב ומכוון, כמו scope או unit ייעודי שאתה שולט בו, והתאם אליו. אז הכלל שורד אתחולים ועדכוני חבילות. הוא גם הופך למשהו שעמית יכול לחשוב עליו בלי היסטוריית ההפעלות שלך.

היה מדויק גם לגבי המנגנון שבו אתה משתמש. סיווג net_cls ב-cgroup v1 והתאמת socket ב-cgroup v2 הם דברים שונים, ושדה בעל שם דומה בתיעוד לא הופך אותם לברי החלפה.

שרשרת מסוג route היא שמפעילה את החיפוש השני

בתעבורה שנוצרת מקומית, סוג השרשרת קובע אם משהו קורה אחרי הצבת הסימון. שרשרת פלט מסוג filter יכולה לסמן חבילה והקרנל ימשיך להשתמש במסלול שכבר בחר. שרשרת מסוג route היא המנגנון שמבקש חיפוש חדש.

table inet bypass {
  chain mark {
    type route hook output priority mangle; policy accept;
    socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
  }
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200

לאחר מכן הסימון זקוק לכלל מדיניות ששולח אותו לטבלה, והטבלה הזו חייבת להיות שלמה: מסלול ברירת מחדל, כל יעד מקומי שהאפליקציה עדיין זקוקה לו, ושער במעלה הזרם שבאמת עונה.

בחר את ערך הסימון בכוונה. WireGuard משתמש ב-fwmark משלו לתעבורת התעבורה החיצונית, ונחיתה במקרה על אותו מספר עלולה להפריע לחריגת הניתוב של המנהרה עצמה בדרכים שכואב לאבחן.

כתובת המקור לא עוקבת אחרי המסלול

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

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

התיקון הצר הוא כלל תרגום מקור שמוגבל לסימון של אותה אפליקציה ולממשק היציאה האמיתי שלה, משולב ב-NAT הקיים במקום להחליף אותו:

table inet bypass {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    meta mark 0x1 oifname "wlan0" masquerade
  }
}

תרגם את החריגה ולא שום דבר אחר. כלל masquerade גורף מתקן את התסמין והורס את היכולת שלך לומר איזו תעבורה לקחה איזה נתיב. התאם לפי הסימון, ממשק היציאה האמיתי ואם אפשר הכתובת של האפליקציה עצמה. כללים רחבים קלים לכתיבה וכמעט בלתי אפשריים לביקורת אחר כך.

מה עוד בורח מהחריגה

פתרון שמות עשוי לבוא מתהליך resolver נפרד ב-cgroup אחר, כך שסיווג ה-socket של הדפדפן לא מסווג את השאילתות שלו. אם השאילתות של ה-resolver נשארות במנהרה בעוד החיבור יוצא מחוצה לה, תשובות split-horizon עלולות לא להתאים לנתיב. אי ההתאמה הזו מייצרת כשלים שנראים כמו באגים של DNS ואינם.

תהליכי בן יורשים את ה-cgroup ברוב ההגדרות, אבל כלי עזר שמעביר את עצמו ל-scope משלו לא. בדוק במקום להניח, במיוחד לכל דבר שמפעיל את עצמו מחדש.

לבסוף, החלט מה צריך לקרות כשהמנהרה למטה. חריגה שהופכת בשקט לעקיפה מלאה היא עמדת אבטחה שונה מזו שנכשלת סגור, ורק אחת מהן היא מה שהתכוונת אליו.

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