VPN на Mac при загрузке: launchd следит за заданием, а не за туннелем
Проверенный lint plist ничего не доказывает ни насчёт учётных данных, ни насчёт готовности. Привяжите задание к его сеансу, держите клиент на переднем плане и проверяйте в этом контексте.
Задание запускается при входе, в журнале видно запуск клиента, но туннель так и не поднимается. Или поднимается и исчезает через несколько секунд. В обоих случаях список свойств, скорее всего, был в порядке. launchd дал два обещания, принимая задание: он будет следить за процессом, и этот процесс будет продолжать работать. VPN нужно и то и другое, а корректный XML-файл не даёт ни одного.
Корректный список свойств — ещё не работающий сервис
plutil -lint проверяет структуру. Он не может сказать, существует ли бинарный файл, поддерживает ли аргументы эта версия клиента и запускается ли задание в контексте, где доступны учётные данные. Это семантические вопросы, и они объясняют большинство сбоев, которые выглядят как проблемы launchd.
plutil -lint /Library/LaunchDaemons/com.example.vpn.plist
launchctl print system/com.example.vpn
Прочитайте вывод второй команды, а не предполагайте, что задание загружено. Он сообщает последний статус выхода и работает ли сервис, что позволяет до правки профиля отличить «так и не запустилось» от «запустилось и умерло».
Агенты наследуют сеанс пользователя, демоны — нет
Документация Apple по launchd разделяет агентов отдельных пользователей и системные демоны. Агент работает в контексте вошедшего пользователя, с его связкой ключей и окружением. Демон работает без этого сеанса, и перенос задания в системный каталог не воссоздаёт интерактивные учётные данные пользователя.
Именно это различие определяет архитектуру. Если подключение требует, чтобы человек вошёл или подтвердил запрос MFA, поддерживаемый вариант — задание в сеансе пользователя, запускаемое после входа. Если оно должно работать без вошедшего пользователя, клиенту нужна одобренная беспризорная учётная запись со своим процессом продления и отзыва. Выбор неверной строки даёт задание, которое чисто стартует и падает на первом шаге аутентификации.
Не маскируйте разницу паролем в массиве аргументов или отключением проверки сертификатов. И то и другое превращает требование аутентификации в постоянный секрет в читаемом файле, а второе означает, что туннель аутентифицирует всё, что отвечает.
Надзор работает только тогда, когда процесс остаётся на переднем плане
Клиент, который сам демонизируется, немедленно возвращает управление launchd, и наблюдатель начинает следить за уже завершившимся процессом. Туннель может быть поднят, но launchd считает, что задание не удалось, и его политика перезапуска сделает то, чего вы не предполагали.
Используйте документированный режим переднего плана или сервиса. Если у продукта есть собственная поддерживаемая функция запуска, она уже знает, как оставаться на переднем плане и как передать управление сетевому стеку; собственная задача — отдельный проект, а не её улучшение.
Дайте заданию уникальную метку, нужный исполняемый файл и осознанный массив аргументов. Массив — не командная строка оболочки, поэтому конвейеры и перенаправления там не работают, а кавычки ведут себя иначе. Команда, которая успешно выполняется в терминале администратора, — в лучшем случае предварительная проверка.
Учётные данные должны работать без запроса
Работа без сопровождения не устраняет проблему учётных данных, а переносит её. Кто-то должен владеть идентичностью, решать, сколько она живёт, и документировать, как её заменяют. Сертификат без контроля срока действия превращается в сбой с датой в календаре.
Держите секреты вне аргументов и вне стандартного вывода задания. Если клиент поддерживает чтение учётных данных из защищённого хранилища, используйте этот путь и проверьте, что контекст выполнения задания может до него добраться. Демон, работающий от root, обычно не может прочитать элемент связки ключей входа, и это как раз та деталь, которая проявляется только в системном контексте.
Определите, что значит готово, и проверяйте именно там
Успешный запуск доказывает, что процесс начался. Он не доказывает, что сеть поднята, что шлюз принял идентичность или что частное приложение доступно. Если что-то другое зависит от туннеля, ему нужен сигнал готовности, отражающий соединение, а не время запуска задания.
Повторы должны быть ограничены и должны отличать отсутствующую сеть от отклонённых учётных данных. Перезапуск при каждой ошибке не починит истёкший сертификат и даёт журнал, полный одинаковых попыток, который скрывает настоящую.
Примите задание в том контексте, в котором оно действительно будет работать: без сети при загрузке, при обычном входе, при пробуждении из сна и при контролируемой остановке. Затем запишите метку задания, версию клиента и ревизию профиля вместе, чтобы следующий человек унаследовал отслеживаемый сервис, а не файл, который когда-то работал.