Сбой client-connect в OpenVPN: гарантирована ли очистка при disconnect?
Отклонённый клиент не отменяет правило брандмауэра и не удаляет запись. Сравните контракт обратного вызова с откатом, который ваш hook должен выполнить сам.
Когда скрипт client-connect завершается с ненулевым кодом, OpenVPN отклоняет клиента, и администраторы обычно полагают, что соответствующий скрипт client-disconnect всё приведёт в порядок. Часто он вообще не запускается, потому что обработка disconnect зависит от того, что успел завершить этап connect. Рассматривать отклонение и откат как один механизм — значит превращать неудачный вход в утёкшее правило брандмауэра или осиротевшую запись. Это две отдельные обязанности, и только одна из них автоматическая. Прочитайте собственный путь выхода hook, прежде чем винить сервер в оставленном состоянии.
Ненулевой выход отклоняет клиента, но не отменяет вашу работу
Код выхода client-connect — это решение о допуске. Ноль означает допуск, любое другое значение — снос экземпляра до того, как он полностью установлен. Ничто в этом контракте не проверяет, что скрипт уже сделал во внешнем мире. Если скрипт добавил правило iptables, записал строку или увеличил счётчик, эти изменения переживают отклонение.
Та же асимметрия действует для плагинов, где для одного экземпляра могут выполняться несколько функций connect. Последующая ошибка не отменяет задним числом предыдущий успех, и у предыдущей функции нет способа узнать, что соседняя функция не сработала.
Отклонение автоматическое, очистка нет
Руководство OpenVPN 2.6 связывает обработку client-disconnect с успехом предыдущей обработки connect. Документированное исключение охватывает каскадные цепочки: если один шаг connect уже вернул успех, соответствующие шаги disconnect всё равно выполняются при сносе экземпляра, даже если последующий шаг connect завершился ошибкой. Это исключение узкое и зависит от числа настроенных обратных вызовов, поэтому проверка одного shell-скрипта ничего не говорит о цепочке.
Составьте карту реальной конфигурации до того, как проектировать вокруг неё: какой скрипт, какой плагин и в каком порядке они выполняются. Таблица ниже перечисляет, что оставляет после себя незавершённая настройка.
| What the hook did before failing | What the client sees | What stays behind |
|---|---|---|
| Created a firewall entry | A rejection, with no tunnel | A rule matching an address nobody owns |
| Wrote a state row | A rejection, with no tunnel | A record of a session that never started |
| Allocated a tunnel address | A rejection, with no tunnel | An address marked busy until something releases it |
Каждая строка заканчивается одинаково для клиента и иначе для хоста. Клиент не может определить, что именно произошло, поэтому hook должен откатить сделанное до возврата статуса ошибки.
Компенсируйте внутри скрипта, который сбоил
Проверьте всё, что можно, прежде чем касаться общего состояния: поля сертификата, доступность адреса, группу, к которой относится клиент. Самое дешёвое отклонение — то, что происходит до любого выделения. Как только настройка охватывает несколько внешних операций, ведите учёт выполненных и откатывайте их в обратном порядке перед возвратом статуса ошибки.
Конкретный случай: скрипт создаёт запись брандмауэра для каждого клиента, а затем не может записать строку состояния, которая её описывает. Сбойная ветвь должна удалить только что созданную запись и затем выйти с ненулевым кодом. Ожидание события disconnect, которое может никогда не быть запланировано, — это способ, которым правило переживает сеанс, для которого оно предназначалось.
Сделайте компенсацию идемпотентной и с владельцем
Очистка должна быть безопасной при двукратном запуске и точной в отношении того, что ей можно удалять. Позднее событие из предыдущей попытки не должно удалять выделение более нового сеанса только потому, что у обоих общее имя или переиспользованный адрес туннеля. Записывайте конкретный дескриптор или идентификатор операции вместе с ресурсом и удаляйте по этому ключу, а не по имени. Транзакции базы данных помогают только с собственными изменениями базы; правило брандмауэра за этой границей требует явного шага компенсации.
Предполагайте, что процесс может умереть между изменением и очисткой, и добавьте согласователь, который по расписанию сравнивает внешнее состояние со списком допущенных клиентов.
Проверьте три пути, которые действительно различаются
Отработайте три случая: отклонение до того, как что-либо выделено, отклонение после частичного выделения и обычный допущенный сеанс, который затем завершается. Эти три дают разные строки журнала и разные остатки. Проверяйте внешнее состояние после каждого из них, а не только сообщение об ошибке клиента, потому что отказ клиента — ожидаемый результат в двух случаях из трёх.
Записывайте этап hook, код выхода и то, что было выделено или освобождено, без учётных данных. Журнал hook, в котором назван созданный ресурс, — это то, что делает утёкшее правило находимым полгода спустя.