iPhone в VPN? Что на самом деле может увидеть приложение iOS
В iOS SDK нет единого флага VPN. Приложение может наблюдать только собственную конфигурацию, уведомление о состоянии и ограниченный снимок прокси, не более.
Спросите приложение iOS, находится ли телефон в VPN, и честный ответ таков: оно не может это знать в том смысле, который вы имеете в виду. SDK не предоставляет флаг «устройство в туннеле», а приватные интерфейсы, которые могли бы к этому приблизиться, закрыты для публикуемых приложений. Вы можете наблюдать собственную конфигурацию приложения, связанное с ней состояние и одну-две косвенные подсказки из сетевого стека. Каждая отвечает на свой вопрос, и большинство сломанных проверок возникает из-за того, что одну из них принимают за весь ответ.
Миф о единственном флаге VPN
Считается, что где-то в SDK есть свойство, сообщающее, находится ли устройство в туннеле, подобно тому как флаг доступности сообщает, отвечает ли хост. Такого публичного свойства не существует. И даже если бы оно было, «туннель существует» и «мой трафик использует его» — разные утверждения, и для большинства приложений важно только второе.
Именно этот разрыв — причина, по которой библиотеки, рекламирующие детектор VPN в одну строку, обычно читают что-то хрупкое, вроде имени интерфейса или ключа прокси, и подают это как приговор.
Загрузите настройки, прежде чем доверять nil
Общий экземпляр менеджера существует сразу после запроса, но его свойство протокола остаётся nil, пока не завершится обратный вызов загрузки. Поэтому nil протокола означает «ещё не загружено» как минимум так же часто, как «не установлено». Выполняйте чтение внутри блока завершения и только потом решайте: существующий объект протокола с включённым флагом активации — так выглядит «установлено».
Второй сигнал находится на объекте соединения: недопустимое состояние, о котором система сообщает, когда у этого приложения нет установленной конфигурации. Недопустимое состояние вместе с nil протоколом после завершённой загрузки — это настоящее отсутствие, а не гонка по времени.
Изменения соединения приходят как уведомления
Обработчики, переданные в load и save, сообщают, что произошло с конфигурацией, а не что делает туннель. Ожидание, что один из них сработает при подключении, — это гонка, которая никогда не разрешится. Вместо этого следите за уведомлением об изменении состояния и читайте текущее значение объекта соединения, когда оно придёт.
Как только конфигурация установлена, состояние соединения проходит короткую последовательность: отключено, подключение, подключено, повторное подтверждение, отключение. «Подключено» описывает туннель, а не сервер за ним, поэтому это не доказательство того, что трафик идёт. On-demand может поднимать туннель и тогда, когда приложение приостановлено, и уведомление приходит раньше, чем у вас появится что обновлять.
Ограниченный снимок прокси — это подсказка, а не приговор
CFNetwork может передать приложению текущие системные настройки прокси, и внутри этого результата есть ограниченный словарь с ключом по имени интерфейса. Имя, начинающееся с tun, utun, ppp, ipsec или tap, — подсказка о том, что туннель есть. Это остаётся снимком того, что видит это приложение, а не утверждением о том, куда идут пакеты.
Провайдеры расходятся в именовании, поэтому отсутствующий префикс ничего не доказывает, а присутствующий интерфейс может существовать, не неся трафика. Доступность ещё слабее: успешный запрос к частному адресу показывает, что путь работал для этого запроса в тот момент, а не то, что установлен VPN.
Симулятор одалживает Mac, а другие приложения остаются невидимыми
У симулятора нет собственного сетевого стека; каждое соединение, которое он устанавливает, — это соединение Mac. Поэтому VPN, работающий на хост-машине Mac, уже действует для симулируемого приложения, а настройки самого симулятора ничего не показывают. Если туннель поднялся после запуска симулятора, перезапустить его обычно быстрее, чем отлаживать приложение. Хосту на нестандартном порту нужен этот порт в URL или локальная переадресация вроде socat TCP-LISTEN:12345,fork TCP:host:12345.
Публичные API не будут перечислять профили VPN, принадлежащие другим приложениям или профилям конфигурации. Для одного известного приложения переносимая уловка — проверка схемы URL через canOpenURL, а начиная с iOS 9 запрашиваемая схема должна быть объявлена в вашем Info.plist; если её пропустить, вызов вернёт false. OpenVPN Connect отвечает на openvpn://, что делает его хорошим примером и плохим общим детектором.
Объедините три наблюдения и скажите, что они значат
На практике сложите три результата: конфигурацию, которая загрузилась и включена, состояние соединения, которое не является недопустимым, и запрос к адресу, доступному только через туннель. Каждое по отдельности вводит в заблуждение; вместе они говорят, что приложение может знать, а не что предполагает пользователь.
Запишите результат как три строки в диагностике, а не как один boolean. Когда пользователь говорит, что VPN выключен, а ваше приложение не согласно, три строки показывают, какое наблюдение разошлось, и это разница между обращением в поддержку, по которому можно действовать, и таким, что заканчивается словами «у меня работает».