האם סקריפט hook של OpenVPN יכול להשתמש במנהרה שהוא בנה זה עתה?
OpenVPN מריץ hooks בתוך תהליך האתחול שלו, ולכן סקריפט שזקוק למנהרה ממתין למנהרה שממתינה לו. נתקו את העבודה ובחרו את ה-hook לפי האירוע.
האם סקריפט hook יכול להגיע לצד השני של המנהרה תלוי בשלושה תנאים: לאיזה אירוע הוא מחובר, אם OpenVPN עדיין בתוך רצף האתחול שלו כשהסקריפט רץ, ואם העבודה נותקה מאותו רצף. רוב הסקריפטים נכשלים בתנאי השלישי. הוספת שינה מתייחסת לבעיית מחזור חיים כאל בעיית תזמון, והיא עובדת בדיוק לעתים קרובות מספיק כדי להטעות.
החליטו קודם על האירוע, אחר כך על המנגנון לדחיית עבודה, ואחר כך על המקום שבו רצה העבודה הנדחית. שלוש ההחלטות האלה בלתי תלויות, וערבובן הוא הדרך שבה hook בן שתי שורות הופך לצהריים שלמים.
למה ה-hook ממתין למנהרה שממתינה לו
OpenVPN קורא ל-hooks שלו באופן סינכרוני, בתוך אותו תהליך שמעביר מנות. up נורה כשמתקן ה-tun או ה-tap קיים אבל לפני שמוסיפים מסלול כלשהו, ו-route-up נורה אחרי המסלולים; אף אחד מהרגעים האלה לא מבטיח שצד שני יענה. כל עוד סקריפט מאחד ה-hooks רץ, ה-daemon לא מעביר, ולכן פקודה שמצפה שתעבורה תעבור במנהרה ממתינה למנהרה שממתינה לפקודה. ההמתנה המעגלית הזו היא הסיבה שריצה ידנית עוברת וה-hook מגיע לפסק זמן.
התיקון הברור, שינה של כמה שניות, עובד לפעמים משום שהקצה המרוחק עלה מעצמו, לא משום שהשינה תיקנה משהו. עוזר שבודק מוכנות הוא תשובה אחרת: ה-hook חוזר, ההעברה מתחילה, והעוזר מקבל את מה שהוא צריך כשהוא באמת שם.
בחרו את ה-hook לפי אירוע, לא לפי נוחות
חברו up לעבודה שצריכה רק את המתקן, route-up לעבודה שצריכה מסלולים, ושמרו client-connect ו-client-disconnect למשימות לכל לקוח בצד השרת. הניקוי בעת ניתוק חייב לסבול ריצה כפולה, כי התחברויות מחדש דרך ממשק מתמשך אינן עוקבות אחרי אותו רצף כמו אתחול מלא. בשרת, client-connect נורה כשהלקוח מאמת את עצמו ולא כשהדמון מתחיל, וכדאי לאמת זאת לפני שמעבירים לשם לוגיקה. אם העבודה שייכת למחזור החיים של השירות ולא לחיבור בודד, יחידת systemd היא בית טוב יותר מכל hook.
פקודות hook מופעלות ישירות על ידי ה-daemon, בלי מעטפת באמצע, ולכן ifconfig_remote בשורת up מגיע כטקסט מילולי. עטפו את הפקודה במעטפת מפורשת, או השאירו את הלוגיקה בסקריפט שקורא את הסביבה. ipchange היא הנחיה אחת שמוסיפה ארגומנטים משלה: היא מוסרת את הכתובת והפורט לפקודה, ולכן סקריפט שנכתב עבור אף אחד רואה שניים בלתי צפויים.
שלוש דרכים לנתק את העבודה
האפשרות הראשונה היא לשלוח את העוזר לרקע מתוך ה-hook: הפעילו אותו עם setsid או systemd-run --scope, סגרו את המתארים בירושה, וצאו מיד עם אפס. השנייה היא לתת ל-hook לכתוב קובץ מצב קטן ולתת לטיימר systemd או ליחידת נתיב לפעול לפיו, מה שמשאיר את ה-hook טריוויאלי ושם את מדיניות הניסיון החוזר במקום שיש בו יומנים. השלישית היא לוותר על ה-hook לגמרי ולהעביר את העבודה ליחידה שמסודרת אחרי שירות ה-VPN, וזו התשובה הנכונה כשהעבודה אינה פר-חיבור כלל. שלושתן חולקות כלל אחד: ה-hook חייב לחזור לפני שהוא זקוק למנהרה.
מה שתבחרו, הפכו את העוזר לאידמפוטנטי ולניתן לתצפית. שתי קריאות חופפות לא אמורות להריץ פעמיים את אותה פעולה בלתי הפיכה, ועוזר שנכשל חייב להשאיר משהו שאדם יכול לקרוא.
הפרידו כשלי sandbox מכשלי מחזור חיים
כשסקריפט רץ מדפיסת root ונכשל כשירות, הסיבה היא בדרך כלל sandbox ולא הרשת. ProtectHome מגביל, ProtectSystem לקריאה בלבד, או תיקיית עבודה שאינה קיימת יכולים להשאיר את הסקריפט או את הנתונים שלו בלתי נגישים, ובמארח SELinux גם הקובץ צריך הקשר שמתיר הרצה. קראו את היחידה עם systemctl cat openvpn-server@server ואת היומן עם journalctl -u openvpn-server@server -n 80, ואז חפשו שגיאת הרשאה או הקשר במקום פסק זמן של חיבור. השבתת ה-sandbox צריכה להיות המהלך האחרון, לא הראשון.
כשל מחזור חיים נראה אחרת: ה-hook מתחיל, העוזר רץ, והשקע פשוט עוד לא שם. כתבו שורה עם חותמת זמן מתוך ה-hook עצמו, נתקו והתחברו מחדש, וקראו את האירועים לפי הסדר. כשהעוזר באמת זקוק למנהרה, הניחו לו לבדוק מוכנות במקום לישון פרק זמן קבוע.
שלושה שדות הופכים את הדוח הבא לשימושי: איזה hook רץ, באיזה חשבון השתמש, ואיפה עצר. בלעדיהם כל חקירה מאוחרת יותר מתחילה מהתחלה.