Запись об отключении приходит после того, как второе подключение под тем же именем уже началось, и обработчик, обновляющий самую новую строку для этого пользователя, закрывает не ту сессию. Ошибка не в сравнении отметок времени. Ошибка в допущении, что имя плюс целая секунда идентифицируют одну сессию. События хуков описывают подключения, а подключению нужен ключ, принадлежащий только ему.

Начните с того, что на самом деле означают переменные отключения. Только тогда выбор идентификатора имеет смысл.

Что на самом деле несёт хук отключения

В Community OpenVPN 2.6 справочник хуков time_unix определяет как момент, когда клиент подключился; он устанавливается до запуска хука подключения и не переопределяется, когда хук отключения срабатывает позже. time_duration даёт длительность этой сессии в секундах, поэтому конец — это примерно время подключения плюс длительность. Оба значения — целые секунды, и ни одно из них не является моментом, когда ваша база данных обработала событие. Именно разделение этих трёх времён позволяет корреляции держаться, а не просто выглядеть правильной во время тестирования.

Эта арифметика заслуживает записи, потому что это единственное время окончания, которое предлагает сам хук. Задержка обработки и расхождения часов между хостами по-прежнему остаются за её пределами.

Почему очевидная пара не уникальна

Сопоставление имени со временем подключения сужает поиск и всё равно не идентифицирует сессию. Клиент может переподключиться в ту же секунду, поэтому две сессии могут делить оба значения. Два экземпляра демона или замена сервера с той же конфигурацией могут породить эту пару из разных процессов. Адреса туннелей переиспользуются по той же причине, что делает их атрибутами сессии, а не её идентификаторами.

Режим отказа тихий. Позднее отключение первой сессии совпадает с самой новой строкой и завершает выделение, которое вторая сессия всё ещё использует, поэтому пользователь переживает случайную потерю доступа, которую не вызывал. Число строк, которое потом выглядит правильным, делает проблему труднее заметной.

Откуда берётся стабильная идентичность

Интерфейс управления назначает каждому клиенту идентификатор экземпляра клиента, и этот идентификатор ограничен экземпляром демона, который его выдал. Рабочий внешний ключ объединяет стабильный идентификатор сервера, эпоху, меняющуюся при запуске демона, и этот клиентский ID. Эпоха важна, потому что идентификаторы перезапускаются вместе с процессом, а голый ID процесса может позже быть переиспользован чем-то несвязанным.

Переменные окружения, заданные внутри дочернего скрипта подключения, не попадают в более поздний скрипт отключения родительского демона, потому что эти скрипты выполняются как отдельные процессы со своими окружениями. Поэтому генерация UUID во время подключения — половина решения. Сторона отключения должна уметь назвать тот же UUID, а значит, идентичность должна жить там, куда могут добраться оба события.

Сделайте завершение условным и идемпотентным

Завершайте по владельцу сессии, а не по пользователю. Обработчик, удаляющий любое выделение, которое сейчас принадлежит имени, в конце концов удалит не то, и единственная защита — проверить, что удаляемая запись и есть запись, которую описывает это событие.

Интерфейс управления принимает одного подключённого клиента управления за раз, поэтому интегрируйтесь с тем, кто уже владеет им, вместо подключения второго наблюдателя, конкурирующего за тот же сокет.

Обрабатывайте повторяющиеся события, не меняя результат дважды, и сохраняйте непарные события для сверки, а не выбрасывайте их. Обратный вызов отключения имеет документированные условия жизненного цикла и может никогда не прийти после сбоя или жёсткой остановки, поэтому дизайну нужен способ представить сессию, событие завершения которой отсутствует.

Проверьте неудобные случаи намеренно: два переподключения в течение одной секунды, два экземпляра демона, подключение, обработка которого не удалась, и перезапуск демона в середине сессии. Ключ, который выдерживает все четыре, заслуживает доверия.

Похожие статьи