На название iOS VPN отвечают три разных продукта, и они не разделяют один API. Personal VPN использует встроенные в систему транспорты IPsec и IKEv2. Custom tunnel запускает ваш собственный протокол внутри network extension. Configuration profile настраивает любой из них и может добавить поведение on-demand. Выбор по транспорту, с которым нужно говорить, быстрее выбора по тому руководству, которое скомпилировалось первым.

Выполняйте проверки ниже по порядку, потому что каждый шаг предполагает, что предыдущий дал корректную конфигурацию.

Шаг 1: выбирайте по транспорту

Если сервер — IKEv2 или IPsec в стиле Cisco, NEVPNManager достаточно, и системные транспорты достаются бесплатно. Если сервер говорит OpenVPN, WireGuard или что-то ещё, NEVPNManager не поможет: у него нет крючка для стороннего протокола. Этот случай требует NETunnelProviderManager с packet tunnel provider, который реализует протокол. Профиль только распространяет конфигурацию и не может добавить транспорт, которого нет в операционной системе.

PPTP не является целью в текущих системах. IPsec с общим секретом по-прежнему ожидает имя группы в remoteIdentifier, элемент keychain с секретом и расширенную аутентификацию, когда сервер запрашивает имя пользователя. Каждая недостающая часть проявляется как одна и та же общая ошибка конфигурации, поэтому добавьте их все, прежде чем отлаживать что-то ещё.

Шаг 2: соблюдайте порядок сохранения, загрузки и запуска

NEVPNManager пишет в preferences, читает их обратно и только потом запускает. Заполните объекты протокола и manager, вызовите saveToPreferences, вызовите loadFromPreferences и вызовите startVPNTunnel из load completion handler. Запуск до возврата загрузки даёт ошибку configuration-invalid, NEVPNErrorDomain code 1, на конфигурации, которая в остальном верна.

Форма достаточно коротка, чтобы держать её в одном месте:

manager.protocolConfiguration = proto
manager.isEnabled = true
manager.saveToPreferences { }
manager.loadFromPreferences { manager.connection.startVPNTunnel() }

startVPNTunnel возвращается, когда запрос принят, а не когда туннель поднят. Наблюдайте NEVPNStatusDidChange и читайте connection.status для реального состояния. Приложение, которое предполагает успех в точке вызова, сообщает о подключённом VPN, пока extension ещё ведёт переговоры. Состояние connecting нормально на мгновение; состояние, из которого оно никогда не выходит, — это сбой, который стоит логировать.

Шаг 3: дайте процессу provider то, что ему нужно

Custom transport работает в отдельном процессе со своим bundle identifier и своим entitlement. И приложению container, и provider при подписи нужен entitlement network-extension, а сборка Developer ID вне App Store всё равно должна нести его в provisioning profile. Отсутствующий entitlement выглядит как ошибка протокола, потому что extension просто не запускается.

Нет публичного socket descriptor, который можно одолжить. NEPacketTunnelProvider даёт вам NEPacketTunnelFlow с read, write и cancel, а код, тянущийся к путям приватных ключей, ломается между выпусками. Provider продолжает работать, пока приложение в фоне, поэтому приложению container для этого не нужен фоновый режим.

Шаг 4: on-demand — это набор правил, а не переключатель

isOnDemandEnabled только взводит правила; NEOnDemandRuleConnect и его собратья решают, когда поднимается туннель. Правило evaluate-connection с probe URL стоит ровно столько, сколько стоит этот URL. Если probe отвечает без туннеля, правило заключает, что частная сеть уже доступна, и никогда не подключается, поэтому нацельте его на то, что отвечает только изнутри.

Always-on — отдельное обещание. Ему нужны контролируемое устройство и управляемый профиль, а неконтролируемый пользователь может удалить профиль когда захочет. Не выпускайте проект, зависящий от always-on, если парк не находится под контролем.

Шаг 5: докажите путь, а не ярлык

Сканирование интерфейсов tun, tap, ppp или ipsec доказывает, что VPN настроен, а не что ваш запрос им воспользовался. NWPathMonitor с usesInterfaceType(.other) сообщает, что виртуальный интерфейс доступен, а viability handler может проверить один хост. Ни то ни другое не доказывает путь выхода, поэтому сочетайте их с запросом к ресурсу, до которого дотянется только туннель. Имена интерфейсов — диагностическая подсказка, а не ворота, на которых должна ветвиться логика приложения.

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

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