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

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

שני מפגשי תעבורה, נתיב פנימי אחד

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

outer: consumer --TLS--> hub
outer: exit     --TLS--> hub
inner: consumer --tun--> hub --internal--> exit --tun--> uplink

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

מה iroute באמת מתקין בתוך השרת

המרכז הוא ההתקן היחיד שמחליט איזה עמית מחזיק באיזה יעד, והוא עושה זאת בטבלת הניתוב שלו במרחב המשתמש, לא בטבלה של הליבה. רשומת client-config-dir עבור היציאה בשם exit-node יכולה לתבוע שם את הקידומת המוגדרת כברירת מחדל:

# /etc/openvpn/server/ccd/exit-node
iroute 0.0.0.0 0.0.0.0

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

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

# /etc/openvpn/server/ccd/consumer-01
push "redirect-gateway def1"

הליבה של היציאה מבצעת את ההעברה

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

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

תרגום המקור הוא מה שקונה את נתיב החזרה

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

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.90.0.0/24 -o enp1s0 -j MASQUERADE
ip -4 route get 10.90.0.9   # run on the exit: must point at the tun

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

היכן המרכז יכול לאכוף והיכן לא

עם client-to-client ונתיב הנתונים המסורתי במרחב המשתמש, המרכז מוסר חבילות פנימיות בין עמיתים בתוך תהליך השרת. הן לעולם אינן עוברות בשרשרת ה-FORWARD של ליבת המרכז, ולכן כלל בליבה שם אינו יכול להגביל מעבר בין עמיתים, כמה בזהירות שלא ייכתב. האכיפה חייבת לחיות במדיניות של השרת עצמו: מי רשאי להתחבר, איזה קובץ CCD הוא מקבל והאם הבעלות המוגדרת כברירת מחדל של היציאה ניתנת בכלל.

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

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

להיכשל בבטחה כשהיציאה נעלמת

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

# on the consumer, with the exit up and then down
ip -4 route get 1.1.1.1
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com

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

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