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

קראו את המסלול לפני שאתם משנים את התצורה

התחילו ברשת ש-Docker באמת בנה. בלוק ה-IPAM הוא תוכנית הכתובות, ותת-הרשת היא השורה שחשובה:

docker network inspect project_default --format '{{json .IPAM.Config}}'
# [{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]

Compose מקצה כברירת מחדל /16 לכל פרויקט חדש, כך ששני שירותים יכולים לשבת בטווח של 65,536 כתובות. הרוחב הזה הוא הסיבה לכך שהחפיפות נפוצות: /16 שנבחר באקראי גדול מספיק כדי לבלוע אתר שאליו מגיעים דרך המנהרה, ואיש לא שם לב עד שהמנהרה עולה.

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

ip route get 172.18.0.7
# 172.18.0.7 dev br-3f2a9c src 172.18.0.1

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

למה התנגשות /16 קלה לפגיעה

Docker שואב רשתות גשר מהמאגרים הפרטיים, ומאגר ברירת המחדל מגיע מהבלוק 172.16.0.0/12 בפרוסות של /16. רשתות ארגוניות משתמשות לעתים קרובות באותו מרחב פרטי, ו-VPN לגישה מרחוק דוחף את הקידומות האלה לכל לקוח שמתחבר. כששני הצדדים מאמינים שהם מחזיקים ב- 172.18.0.0/16, טבלת ניתוב אחת מחזיקה שתי טענות סותרות.

אורך הקידומת קובע את המנצח, לא הסדר שבו המסלולים מופיעים. מנהרה שדוחפת 10.0.0.0/8 אינה מנצחת מסלול גשר 172.18.0.0/16, כי /8 פחות ספציפי. מנהרה שדוחפת 172.18.0.0/16, או 172.18.5.0/24 בתוכו, כן מנצחת. לכן מחשב נייד אחד עובד ברשת המשרד ונכשל ב-Wi-Fi של בית מלון עם אותו VPN: הלקוח מקבל קידומות שונות.

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

השוו את שלושת התיקונים

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

RepairWho owns itWhat it costs
Renumber the container networkYouRecreates the network, restarts services, invalidates literal addresses in configs and scripts
Exclude your range from the tunnelNetwork teamChanges what every laptop sees, needs a change request and a client profile update
Reach the service through a published portYou and the clientOne port only, no direct container-to-container routing across the tunnel

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

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

נעצו טווח בכוונה

לפרויקט אחד, הצהירו על תת-הרשת בקובץ ה-compose כדי שהפרויקט יפסיק לשאוב מהמאגר המשותף:

networks:
  default:
    ipam:
      config:
        - subnet: 10.201.7.0/24

קחו את הטווח הזה ממרשם שהצוות שלכם מחזיק, לא מדוגמה שמצאתם בחיפוש. בדקו אותו מול הקידומות שה-VPN דוחף, רשתות ה-LAN הביתיות שמהן אנשים עובדים, ורשתות הקונטיינרים האחרות באותו מארח. אחר כך תכננו את היצירה מחדש, כי רשת קיימת שומרת על תת-הרשת הישנה שלה: docker compose down, ואז up -d. docker network rm מסרב כל עוד נקודות קצה מחוברות, וכל ipv4_address רשומה בקובץ חייבת ליפול בתוך הטווח החדש.

לרשתות ש-Docker יוצר בעצמו, הגדירו מאגר דמון ב- /etc/docker/daemon.json:

{
  "default-address-pools": [
    { "base": "10.201.0.0/16", "size": 24 },
    { "base": "10.202.0.0/16", "size": 24 }
  ]
}

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

גשר Linux ו-Docker Desktop נכשלים אחרת

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

Docker Desktop מציב קונטיינרים מאחורי VM עם NAT משלו. למארח אין שום מסלול לכתובת הקונטיינר; הוא מגיע לשירות דרך פורט מפורסם, שלרוב קשור אל 127.0.0.1. לכן לקוח VPN יכול לשבור את השירות בלי שום חפיפת תתי-רשתות, או על ידי התקנת מסלול לטווח הפנימי של ה-VM, או על ידי חסימת תעבורת loopback כשהמנהרה מחוברת. אם ip route get במארח מדווח שאין מסלול לכתובת הקונטיינר, זה צפוי ולא הבאג.

ההשלכה המעשית היא איפה אתם מסתכלים קודם. ב-Linux, התחילו בטבלת הניתוב. ב-Desktop, התחילו בפורט המפורסם ובכללי חומת האש של לקוח ה-VPN, ואז בדקו אם תת-הרשת הפנימית של ה-VM מתנגשת במשהו שהמנהרה דוחפת.

אמתו כשהמנהרה למעלה

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

  1. ip route get מהמארח, באמצעות הכתובת האמיתית של הקונטיינר.
  2. בקשה מקונטיינר אחר, כדי להוכיח שהשירות עצמו בריא.
  3. curl -sv http://127.0.0.1:8080/ מהלקוח כשהמנהרה למעלה, ואז traceroute -n לכתובת הקונטיינר ממכונה ב-LAN.
  4. sysctl net.ipv4.conf.all.rp_filter אם הבקשה מגיעה למארח אבל התשובה נעלמת.

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

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