Ваше приложение продолжает работать, пока туннель умирает. Wi-Fi всё ещё включён, интернет всё ещё отвечает, а частный API, от которого зависит приложение, стал недоступен. Событие .NET, к которому здесь обычно прибегают, NetworkAvailabilityChanged, вообще не описывает эту ситуацию. Оно сообщает об изменении совокупного состояния машины, и это состояние может оставаться истинным при отключении и повторном подключении VPN. Наблюдатель должен следить за клиентом, владеющим туннелем, а не за сводкой сетевого стека.

Доступность — это агрегат, а не ваш туннель

NetworkAvailabilityChanged срабатывает, когда меняется совокупный ответ машины о доступности, и этот ответ учитывает любой активный интерфейс, кроме loopback или туннеля.

Обрыв VPN при включённом Wi-Fi не меняет ничего из того, что оно измеряет, поэтому событие может молчать именно во время сбоя, который вас волнует.

Поток состояния принадлежит клиенту, владеющему туннелем

Определите, какой компонент владеет соединением: GUI или служба OpenVPN, OpenVPN Connect либо процесс, который запустило и контролирует ваше приложение. Это разные владельцы с разными поддерживаемыми интерфейсами, и наблюдатель должен использовать тот, что соответствует развёртыванию. Для процесса OpenVPN, с которым вам разрешено интегрироваться, интерфейс управления сообщает текущее состояние, историю и уведомления в реальном времени. Анализируйте документированные структурированные сообщения вместо поиска слова connected в локализованной подписи GUI.

Канал управления — привилегированная поверхность управления, поэтому не открывайте новый слушатель и не спорьте с GUI за канал, который он уже держит.

Защиты поколений останавливают устаревшие результаты

Каждое наблюдение относится к конкретной попытке подключения. Увеличивайте счётчик поколения, когда начинается новая попытка, когда теряется канал и когда клиент сообщает о выходе.

Рассмотрим проверку, начатую во время соединения A. Туннель падает, начинается соединение B, а затем приходит запоздалый успех HTTP от A. Без защиты этот старый результат делает экран зелёным, пока B ещё проходит аутентификацию. Отмена сокращает лишнюю работу, а проверка поколения не даёт устаревшему завершению стать текущим состоянием:

var generation = _generation;
var reachable = await ProbeAsync(token);
if (generation != _generation) return;
Publish(reachable);

Примените то же правило вокруг отправки в UI, потому что маршалированное обновление может прийти после того, как поколение ушло вперёд.

Инициализация и переподключение — один жизненный цикл

Рассматривайте первое подключение, переподключение и потерю канала как один жизненный цикл с одним набором правил:

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

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

Сообщайте свидетельства, а не логическое значение

Возвращайте именованное состояние сеанса и готовность ресурса как отдельные значения, каждое с возрастом. Потребитель, которому нужно что-то решить, увидит, что последняя успешная проверка имеет возраст сорок секунд, а это иная информация, чем свежая.

Поиск публичного IP не является доказательством того, что частный API использовал требуемый маршрут, поэтому не подавайте его как таковое.

Держите принуждение вне наблюдателя

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

Тогда вопрос об UI становится простым: покажите то, что известно, вместе с его возрастом, и говорите «неизвестно», когда это так.

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