Может ли скрипт-хук OpenVPN использовать туннель, который он только что построил?
OpenVPN выполняет хуки внутри собственного запуска, поэтому скрипту, которому нужен туннель, приходится ждать туннель, который ждёт его. Отделите работу и выбирайте хук по событию.
Дойдёт ли скрипт-хук до другой стороны туннеля, зависит от трёх условий: к какому событию он привязан, находится ли OpenVPN всё ещё внутри своей последовательности запуска, когда скрипт выполняется, и отделена ли работа от этой последовательности. Большинство скриптов спотыкаются на третьем. Добавление паузы трактует проблему жизненного цикла как проблему времени, и это работает ровно настолько часто, чтобы вводить в заблуждение.
Сначала решите, какое это событие, затем — механизм отложенной работы, затем — место, где отложенная работа выполняется. Эти три решения независимы, и их смешение — то, из-за чего хук в две строки превращается в целый день.
Почему хук ждёт туннель, который ждёт его
OpenVPN вызывает свои хуки синхронно, внутри того же процесса, который перемещает пакеты. up срабатывает, когда устройство tun или tap уже существует, но до добавления любого маршрута, а route-up — после маршрутов; ни одно из этих мгновений не обещает, что противоположная сторона ответит. Пока скрипт любого из хуков ещё выполняется, демон не пересылает, поэтому команда, которая ждёт, что трафик пройдёт через туннель, ждёт туннель, который ждёт эту команду. Именно это круговое ожидание объясняет, почему ручной запуск проходит, а хук получает тайм-аут.
Очевидная заплатка, сон на несколько секунд, иногда срабатывает потому, что дальняя сторона поднялась сама, а не потому, что сон что-то исправил. Помощник, который опрашивает готовность, — другой ответ: хук возвращается, пересылка начинается, и помощник получает нужное, когда оно действительно есть.
Выбирайте хук по событию, а не по удобству
Привяжите up к работе, которой нужно только устройство, route-up — к работе, которой нужны маршруты, а client-connect и client-disconnect оставьте для задач по каждому клиенту на стороне сервера. Очистка при отключении должна выдерживать двукратный запуск, потому что переподключения через постоянный интерфейс не следуют той же последовательности, что полный перезапуск. На сервере client-connect срабатывает, когда клиент проходит аутентификацию, а не когда стартует демон; это стоит подтвердить до переноса логики туда. Если работа относится к жизненному циклу службы, а не к одному соединению, юнит systemd — лучшее место, чем любой хук.
Команды хука демон выполняет напрямую, без оболочки между ними, поэтому ifconfig_remote в строке up приходит как буквальный текст. Оберните команду в явную оболочку или держите логику в скрипте, который читает окружение. ipchange — это одна директива, которая добавляет собственные аргументы: она передаёт команде адрес и порт, поэтому скрипт, написанный ни под что, видит два неожиданных.
Три способа отделить работу
Первый вариант — отправить помощник в фон изнутри хука: запустите его через setsid или systemd-run --scope, закройте унаследованные дескрипторы и сразу выйдите с нулём. Второй — позволить хуку записать небольшой файл состояния, а таймеру systemd или path-юниту действовать по нему; так хук остаётся тривиальным, а политика повторов попадает туда, где есть журналы. Третий — полностью отказаться от хука и перенести работу в юнит, упорядоченный после службы VPN; это правильный ответ, когда работа вовсе не привязана к соединению. Все три делят одно правило: хук должен вернуться раньше, чем ему понадобится туннель.
Что бы вы ни выбрали, сделайте помощник идемпотентным и наблюдаемым. Два перекрывающихся вызова не должны выполнять одно и то же необратимое действие дважды, а помощник, который падает, обязан оставить что-то, что человек сможет прочитать.
Отделяйте отказы песочницы от отказов жизненного цикла
Когда скрипт выполняется из root-приглашения и падает как служба, причина обычно в песочнице, а не в сети. Ограничивающий ProtectHome, доступный только для чтения ProtectSystem или рабочий каталог, которого не существует могут сделать скрипт или его данные недоступными, а на хосте SELinux файлу нужен ещё и контекст, разрешающий выполнение. Прочитайте юнит через systemctl cat openvpn-server@server и журнал через journalctl -u openvpn-server@server -n 80, затем ищите ошибку прав или контекста, а не тайм-аут соединения. Отключение песочницы должно быть последним шагом, а не первым.
Отказ жизненного цикла выглядит иначе: хук стартует, помощник работает, а сокета просто ещё нет. Запишите строку с отметкой времени из самого хука, отключитесь и подключитесь снова, прочитайте события по порядку. Когда помощнику действительно нужен туннель, пусть он опрашивает готовность, а не спит фиксированное время.
Три поля делают следующий отчёт полезным: какой хук работал, какой учётной записью пользовался и где остановился. Без них каждое последующее расследование начинается заново.