Ваше приложение сообщает о разъединении, строка меню — о подключении, а трафик явно идёт. Этот разрыв не является гонкой, которую можно исправить повтором: приложение и туннель — отдельные процессы, а объект, который читает ваш код, — кэшированное представление конфигурации, которую хранит система. Поверхность Personal VPN от Apple — это API управления, а не путь данных, и большая часть её странного поведения следует из этого одного факта. Вот что на самом деле происходит при каждом вызове.

Объект менеджера — кэшированное представление, а не туннель

NETunnelProviderManager заполняется из хранилища настроек, которым владеет система, поэтому его свойства ничего не значат, пока не завершится loadFromPreferences. Если прочитать connection.status раньше, вы получите значение по умолчанию, и именно поэтому первое обновление статуса после перезапуска сообщает о разъединении, когда туннель работает. Сначала загрузите, затем прочитайте статус и конфигурацию протокола из того же обработчика завершения. Наблюдайте за последующими изменениями через NEVPNStatusDidChange вместо опроса, и учтите, что уведомление срабатывает и для подключений, запущенных On Demand, где вообще нет вызова приложения, который мог бы что-то вернуть. То же относится к настройкам протокола: они читаются из загруженной конфигурации, а не из того, что передало приложение.

Сам туннель живёт в процессе провайдера Network Extension, поэтому приложение может потерять собственное состояние, не потеряв соединение. Всё, что показывает интерфейс, нужно заново собрать из менеджера, а не вспоминать из прошлой сессии.

Порядок запуска: сохранить, загрузить, затем запустить

Соединению на основе провайдера нужны три шага по порядку. Запишите конфигурацию провайдера и вызовите saveToPreferences, загрузите менеджер, чтобы сохранённая конфигурация была той, что у вас в руках, затем запустите через NETunnelProviderSession.startVPNTunnel(). Пропустите сохранение — запуску нечего запускать; загрузите до сохранения — менеджер всё ещё отражает старую конфигурацию.

Логическое значение, которое возвращает startVPNTunnel, говорит только о том, что запрос принят. Отказы на уровне провайдера приходят позже как смена статуса на «разъединено», которую вы наблюдаете через уведомление NEVPNStatusDidChange, и обработчик уведомления обязан заново прочитать connection.status после loadFromPreferences, потому что кэшированное значение может быть ещё старым. Отказы на уровне запроса возвращаются сразу из формы вызова, бросающей исключение, или через выходной параметр ошибки startVPNTunnelAndReturnError:. Код, который считает возвращаемое значение результатом, сообщает о соединении, которое так и не установилось. Другой тихий отказ — идентификатор пакета провайдера, не совпадающий с установленным расширением: запрос отклоняется, и ничто в сообщении не указывает на идентификатор. Если идентификатор неверен, запрос обычно терпит неудачу сразу, до появления хоть одной строки журнала провайдера.

Повторная загрузка настроек после запуска — это то, что делает экраны статуса правильными. Менеджер не присылает вам новую конфигурацию, когда система её меняет.

Материал доверия приходит через профиль, никогда из кода приложения

У приложения нет поддерживаемого способа добавить корневой сертификат в хранилище доверия iOS, поэтому конфигурация Personal VPN может ссылаться только на материал, которому устройство уже доверяет: цепочку, созданную установленным профилем, управлением устройством или CA, которому система доверяет по другим причинам. Приложение, которое при первом запуске создаёт собственный CA и ожидает, что система его признает, провалит проверку внутри провайдера, где интерфейс приложения не может увидеть причину.

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

Счётчики берутся из интерфейса, а не из API VPN

Менеджер предоставляет статус и конфигурацию и ничего, что считало бы байты на туннель. Если нужны цифры использования, вызовите getifaddrs() и прочитайте if_data для нужного интерфейса: en0 для Wi-Fi, интерфейсы pdp_ip для сотовой связи и устройство utun для самого туннеля. Счётчики utun описывают внутренний трафик и включают накладные расходы протокола.

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

Очистка относится к stopVPNTunnel, а не к завершению

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

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

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