ExecStart — не shell: починка вспомогательного юнита VPN
ExecStart запускает программу с аргументами, а не конвейер shell. Используйте явный интерпретатор или настоящий скрипт, затем проверьте юнит и прочитайте журнал.
Скопируйте рабочую командную строку в ExecStart, перезагрузите, и юнит падает или молча завершается, потому что аргументы считаются именами файлов. В systemd ничего не сломано. ExecStart — это список аргументов, а не shell, поэтому конвейеры, перенаправление и условия теряют смысл в тот момент, когда покидают ваш терминал. Решите, где должен быть shell, и напишите юнит, который точно говорит, что нужно выполнить.
Почему конвейер в ExecStart ничего не делает
Когда команда вводится в терминале, shell сначала разбирает её. Он строит конвейер, открывает файлы для перенаправления и раскрывает переменные ещё до запуска любой программы. systemd ничего этого не делает: он делит ExecStart по своим правилам и выполняет получившуюся программу с этими аргументами. Поэтому > приходит как буквальный аргумент, а | передаётся программе вместо соединения двух процессов. Юнит ведёт себя не неправильно; синтаксис означал другое.
Это сделано намеренно, а не по небрежности, потому что предсказуемая обработка аргументов и делает юниты проверяемыми. Это удивляет только тех, кто предполагает наличие shell.
Напишите юнит, который точно говорит, что запускается
Начните с небольшого юнита, назначение которого видно в журнале, чтобы проблемы разбора оставались отдельно от проблем туннеля. Этот только сообщает версию двоичного файла.
[Unit]
Description=Report the installed OpenVPN version
[Service]
Type=oneshot
ExecStart=/usr/sbin/openvpn --version
StandardOutput=journal
StandardError=journalПуть — это заполнитель, поэтому подставьте двоичный файл, который действительно установлен. Type=oneshot подходит для команды, работающей до завершения, а настройки журнала помещают её вывод туда, где вы можете прочитать его без терминала. Раздела установки намеренно нет, поэтому юнит вызывается вручную, а не включается при загрузке. Проверьте файл перед запуском, потому что опечатка в имени директивы проявляется иначе, чем опечатка в пути.
systemd-analyze verify /etc/systemd/system/openvpn-version.service
systemctl start openvpn-version.serviceДержите логику shell в скрипте
Когда помощнику действительно нужна условная логика, поместите эту логику в скрипт и вызовите его явно. Скрипту нужны строка интерпретатора, право на выполнение и коды выхода, которые служба может сообщить. Запуск /bin/sh -c с командой в кавычках действительно работает, но прячет правила цитирования внутри юнита и усложняет проверку. Именованный скрипт можно проверить вручную и позже заменить, не правя юнит. Не полагайтесь на профиль shell интерактивного пользователя, потому что служба стартует со своим окружением.
Задайте WorkingDirectory, когда важны относительные пути, и передавайте окружение через поддерживаемые директивы юнита, а не экспортируйте его в терминале. Секреты принадлежат защищённым файлам или учётным данным, а не спискам аргументов, которые журнал может записать.
Проверьте юнит, который действительно существует
Файлы drop-in меняют поведение, не трогая файл, который вы помните отредактированным. Прочитайте, что загрузил менеджер и что сообщил последний запуск.
systemctl cat vpn-helper.service
journalctl -u vpn-helper.service -n 20 --no-pagerДамп юнита включает файлы drop-in в порядке загрузки, и обычно именно оттуда берётся неожиданный ExecStart. После любого изменения перезагрузите менеджер и убедитесь, что новое определение — это то, что печатает systemctl cat. Журнал показывает точную командную строку и статус выхода, что отделяет проблему аргументов от отсутствующего двоичного файла или ошибки прав. Затем решите, относится ли помощник к существующему жизненному циклу VPN или должен остаться ручной диагностикой, потому что второй фоновый процесс туннеля создаёт конкурирующего владельца.
Юнит, который по запросу печатает правильный вывод, — хорошая первая веха. Это ещё не туннель, и он не должен делать вид, что это так.