Служба 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 ловит отсутствующий или неисполняемый путь до перезапуска, а команда status после перезапуска сообщает, существует ли процесс теперь.
Если unit стартует и затем падает, вы покинули семейство EXEC и вошли в обработку ошибок самого клиента. Ищите в журнале сообщение клиента, а не очередной сбой execve. Храните diff unit и diff sudoers вместе в записи об изменении, чтобы следующее обновление не вернуло расколотый путь молча.