שירות ה-VPN נכשל ב-EXEC: השוו unit, קובץ בינארי ו-sudoers
כשל בשלב EXEC פירושו ש-systemd לא הצליח להפעיל את הקובץ הבינארי. השוו את הנתיב ב-unit, את הנתיב המותקן ואת ההרשאה ב-sudoers לפני שמשנים משהו.
אתמול המנהרה עבדה, והבוקר ה-unit מת עם Failed at step EXEC spawning /usr/local/bin/vpnclient ביומן. פרטי הגישה, המסלולים והחשבון לא נכנסים למשפט הזה אף פעם. systemd תרגם את ExecStart לנתיב, ביקש מהקרנל להריץ אותו, והקרנל סירב, לרוב מפני שאין דבר בנתיב הזה או שהקובץ אינו ניתן להרצה. התייחסו לתקלה כאל בעיית קובץ ו-argv עד שהפקודה המפוענחת תוכיח אחרת.
מדוע EXEC נכשל לפני שהאימות מתחיל
systemd אינו מריץ את ExecStart דרך מעטפת. הוא מבצע fork, בונה וקטור ארגומנטים מהשורה וקורא ל-execve על האיבר הראשון. אם הקריאה נכשלת, השירות לא מגיע אף פעם לקוד של לקוח ה-VPN עצמו, ולכן אין עריכה של פרופיל או בדיקת פרטי גישה שיכולה לעזור.
קוד המצב מפריד בין המשפחה הזו לקריסות רגילות. unit שיוצא עם status=203/EXEC מעולם לא התחיל תהליך; unit שמתחיל ואז יוצא עם status=1 אולי נכשל באימות, ב-DNS או בניתוב. קראו את הקוד לפני יומן הלקוח, כי במקרה הראשון יומן הלקוח יהיה ריק.
הנתיב ביומן הוא זה ש-systemd השתמש בו, לא בהכרח זה שערכתם. קובצי drop-in ופלט המחולל יכולים לדרוס את קובץ הספק, ו-daemon-reload מיושן משאיר את השורה הישנה בזיכרון. לכן האבחנה הראשונה היא לשאול את systemd מה הוא חושב שהפקודה, במקום לפתוח את הקובץ שאתם זוכרים ששיניתם.
שלושה מקומות שבהם נכתב נתיב הקובץ הבינארי
לקוח VPN שהותקן מחבילת הפצה, מסקריפט ספק ומקובץ tarball של מהדורה יכול לנחות בשלושה קידומות שונים. ה-unit, כלל ההרשאה והמתקין רשמו כל אחד אחד מהקידומות האלה, והתקלה מופיעה כשניים מהשלושה אינם תואמים.
| Record | Common value | How to read it |
|---|---|---|
| Unit or drop-in | /usr/local/bin/vpnclient | systemctl cat vpnclient.service |
| Installed binary | /usr/bin/vpnclient | command -v vpnclient, then ls -l the result |
| Sudoers grant | /opt/vpnclient/bin/vpnclient | sudo grep -r vpnclient /etc/sudoers.d/ |
אי-התאמה ממשית נראית כך: סקריפט הספק מתקין אל /usr/local/bin, ה-tarball נפרס תחת /opt, וה-unit עדיין מציין את הנתיב הישן /usr/bin מחבילה קודמת. רק אחד מהקבצים האלה קיים, ולעיתים רחוקות זה הקובץ שב-ExecStart.
החליטו איזה נתיב הוא הקנוני לפני שתשנו אחד מהקבצים. אם חבילת ההפצה היא הבעלים של הקובץ הבינארי, נתיב החבילה הוא הקנוני וה-unit צריך ללכת אחריו. אם סקריפט הספק הוא הבעלים, אותו כלל חל על קידומת הספק. העתקת קובץ בינארי בין קידומות יוצרת שני עותקים שנפרדים בשדרוג הבא.
כשפקודה במרכאות הופכת לשם קובץ
מצב כשל שני מסתתר מאחורי אותה שורה ביומן. מישהו כותב את זה ל-unit ומצפה שהלקוח יתחבר בזמן האתחול:
ExecStart=/usr/bin/"vpnclient connect office"systemd מפצל את הערך למילים, ולכן המילה הראשונה הופכת ל-/usr/bin/vpnclient connect office, כשהמרכאות מוסרות כתחביר קיבוץ. אחר כך הקרנל מחפש קובץ ששמו מכיל רווחים, וקובץ כזה אינו קיים. השגיאה זהה לזו של קובץ בינארי חסר, אף שהקובץ מותקן.
הצורה הנתמכת היא קובץ הרצה אמיתי שאחריו ארגומנטים נפרדים, לדוגמה ExecStart=/usr/bin/vpnclient connect office. אם זרימת העבודה באמת זקוקה לצינור או לשרשרת, שימו אותה בסקריפט וקראו לסקריפט, מפני של-ExecStart אין &&, אין צינורות ואין הרחבת glob.
תקנו את ה-unit, לא את מערכת הקבצים
התיקון המפתה הוא קישור סמלי מהנתיב הצפוי אל הקובץ הבינארי האמיתי. הוא עובד עד שהחבילה מתעדכנת והקישור מצביע על קובץ שהוסר, או עד שתווית SELinux אינה מתאימה עוד ליעד. עדיף לערוך את ה-unit דרך drop-in, כדי שקובץ הספק יישאר ללא שינוי והשדרוגים יישארו צפויים.
כלל ההרשאה חייב להתאים לאותו נתיב ולאותן תת-פקודות. רשומת sudoers עבור /usr/local/bin/vpnclient אינה מאשרת את /usr/bin/vpnclient, ורשומה שמתירה כל ארגומנט מאפשרת לכל תהליך מקומי להגדיר מחדש או לעצור את המנהרה. תנו את שני הפעלים שה-unit זקוק להם והשאירו את הפירוק מחוצה לו אם עצירת השירות כבר סוגרת את הממשק.
כאן ההשוואה משתלמת: ה-unit, מסד נתוני החבילות ו-sudoers אמורים כולם לציין נתיב אחד. כשהם עושים זאת, שגיאת ה-EXEC נעלמת בלי אתחול ובלי עותק של הקובץ הבינארי במקום שלישי.
הוכיחו את הפקודה המפוענחת לפני האתחול
sudo systemctl daemon-reload
sudo systemctl cat vpnclient.service
systemctl show -p ExecStart vpnclient.service
systemd-analyze verify /etc/systemd/system/vpnclient.service
sudo systemctl restart vpnclient.service
systemctl status vpnclient.service --no-pagersystemctl show הוא הפלט המכריע, מפני שהוא מדפיס את ה-argv ש-systemd באמת פיענח, כולל כל drop-in. systemd-analyze verify תופס נתיב חסר או לא ניתן להרצה לפני שתאתחלו, ופקודת המצב לאחר האתחול אומרת אם התהליך קיים כעת.
אם ה-unit מתחיל ואז נכשל, עזבתם את משפחת EXEC ונכנסתם לטיפול השגיאות של הלקוח עצמו. חפשו ביומן את ההודעה של הלקוח, לא כשל execve נוסף. שמרו את ה-diff של ה-unit ואת ה-diff של sudoers יחד ברשומת השינוי, כדי שהשדרוג הבא לא יחזיר בשקט את הנתיב המפוצל.