Как обнаружить активный VPN-туннель из кода на любой платформе
Существует ли туннель, идёт ли этот адресат через него или отвечает дальний конец? Это три разные проверки и три разных сигнала.
«Включён ли VPN?» — на самом деле три вопроса в одном предложении. Какой из них нужен вашему коду — это первое решение, и именно его пропускает большинство реализаций. Есть ли на этом устройстве туннель, уходит ли трафик к этому адресату через него или дальний конец отвечает той личностью, которую вы ожидаете. Выберите свойство, затем сигнал, который его измеряет, и в конце запишите в журнале, какое именно вы измерили.
Три свойства, три сигнала
Сжатые до одной строки каждое, три вопроса имеют три разных ответа. Таблица ниже — это само решение, а не его краткое изложение.
| Property | Signal that answers it | What it still leaves open |
|---|---|---|
| Existence | The platform's own connection state: RAS on Windows, NetworkManager on D-Bus on Linux, scutil on macOS | Tunnels of a type the platform does not manage, which includes most third-party clients |
| Path | A route lookup for the one destination, or a scoped proxy or interface key that names the tunnel | Whether the far end is the peer you trust |
| Identity | An authenticated response from the far end, or a source-address policy applied on the server | Which local interface carried the packets |
Прочитайте последнюю строку медленно. Отвечающий частный хост доказывает, что что-то ответило, а не то, что ответ пришёл через туннель, и он ничего не говорит о том, кто на другом конце. Если нужное свойство — это путь, измеряйте путь.
Существование: собственное состояние платформы
Windows отвечает на это для родных профилей через Remote Access Service API. RasEnumConnections перечисляет, что подключено, RasGetConnectStatus сообщает состояние одной записи, а RasGetLinkStatistics добавляет скорость, счётчики байтов и возраст соединения. Ограничения и есть суть: OpenVPN, WireGuard и клиенты производителей никогда не регистрируют соединение RAS, поэтому исправный туннель такого типа показывает пустой список, и наивный код сообщает, что он выключен.
Рабочие столы Linux предоставляют то же свойство через NetworkManager на D-Bus, где PropertiesChanged на объекте VPN-соединения превращает проверку в событие, а свойство State сообщает, когда соединение достигает состояния подключено. Из оболочки nmcli connection show --active читает ту же информацию, и именно это нужно при отладке. macOS хранит этот список в платформе System Configuration, а scutil — его представление из командной строки. Все три описывают соединение, которым управляет платформа, и все три молчат о туннеле, которым она не управляет.
Путь: куда на самом деле идёт пакет
Сопоставление имён — это кратчайший путь, к которому все прибегают первым: перечислите интерфейсы, проверьте строку на tun, tap, ppp или ipsec и считайте совпадение доказательством. Это эвристика, и набор интерфейсов может выглядеть одинаково независимо от того, идёт ли через него что-нибудь. Старые сборки iOS отвечали так, а более новые перестали, потому что видимые интерфейсы больше не различали эти два состояния.
То, что заменило это там, — системный словарь прокси. Пока VPN подключён, вызов настроек прокси возвращает ключи, ограниченные интерфейсами, связанными с туннелем, а когда ничего не подключено, словарь возвращается пустым. Это более слабое утверждение, чем кажется, потому что оно фиксирует системную веру в существование туннеля, а не то, какой адресат им пользуется.
У Android есть собственная версия этой ловушки: широковещательное сообщение vpn.connectivity в одних сборках доходит до приёмников, объявленных в манифесте, а в других — до приёмников, зарегистрированных во время выполнения. Относитесь к нему как к наилучшему усилию и держите за ним прямую проверку.
Когда важен именно адресат, спрашивайте таблицу маршрутизации, а не список адаптеров. Эвристики шлюза по умолчанию ломаются на машинах с несколькими сетями и на туннелях, которые устанавливают собственный шлюз.
Идентичность: проверьте дальний конец
Цикл, который каждые несколько секунд отправляет ICMP-эхо на дальний конец, — самая дешёвая проверка такого рода, и она отказывает в полезную сторону. Направьте его на адрес, который существует только внутри частной сети, потому что публичный отвечает через любой работающий вышестоящий канал и ничего не доказывает. Там, где ICMP фильтруется, короткое TCP-соединение с портом службы делает ту же работу.
Проверка на стороне сервера располагает ровно одним сигналом: адресом источника в соединении. Ведение инвентаря диапазонов — это работа по сопровождению, и она не может отделить туннель от любой другой сети, которая случайно использует тот же блок.
Грубая политика может с этим жить. Идентичность не может, поэтому аутентифицируйте клиента, а не полагайтесь на то, откуда, как кажется, приходят его пакеты.
Записывайте категорию, а не вердикт
Держите категории сбоев в журнале раздельно: интерфейса нет вовсе, интерфейс есть, но адресат недостижим, адресат достижим, но аутентификация отклонена. Это три разных владельца, и каждый открывает своё расследование.
Сведение их в одно «VPN упал» отправляет отчёт команде, которая не может это исправить. Категория — полезный результат; вердикт — это то, что делает его неверным.