Вы устанавливаете профиль, туннель поднимается, и всё выглядит готовым. Потом телефон перезагружается, on-demand будит соединение без приложения на переднем плане, и туннель отказывается запускаться. Профиль на месте, пароль по-прежнему верен, и именно поэтому этот сбой отправляет людей искать не там. Network Extension не может прочитать ваш секрет из профиля: ему нужна ссылка на связку ключей, которую он сам сможет разрешить.

Разбор ниже прослеживает одну попытку подключения через эту передачу — от запроса, который строит ссылку, до перезагрузки, которая её проверяет.

Профиль хранит дескриптор, а не секрет

NEVPNProtocol раскрывает passwordReference и sharedSecretReference не просто так. Когда демон запускает туннель, он может работать не внутри вашего процесса и потому не может вызвать ваш код и попросить пароль. Он читает постоянную ссылку на связку ключей, разрешает её в своём контексте и сам достаёт элемент.

Вы строите эту ссылку, запрашивая её у связки ключей, — это другой запрос, нежели запрос данных:

let query: [String: Any] = [
  kSecClass as String: kSecClassGenericPassword,
  kSecAttrService as String: "vpn-credentials",
  kSecAttrAccount as String: "user",
  kSecReturnPersistentRef as String: true,
  kSecMatchLimit as String: kSecMatchLimitOne
]

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

Почему API отвечает -50

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

Обёрточные библиотеки к этому располагают. Помощник, возвращающий байты, не взаимозаменяем с тем, что возвращает ссылку, поэтому исправление обычно заключается в другом запросе, а не в другом пароле.

То же несоответствие объясняет профиль, который сохраняется без жалоб, а затем не подключается: сохранён блоб, а не дескриптор, и демону нечего разрешать.

Сохраняется нормально, падает при подключении

Сохранение задействует контекст вашего приложения. Подключение задействует контекст демона, и доступ у них не одинаков. Поэтому профиль может пройти все тесты в симуляторе и упасть при первом запуске on-demand.

Такова же форма симптома при перезагрузке: работает, пока приложение открыто, и умирает, когда система запускает туннель сама.

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

Доступность решает, сработает ли перезагрузка

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

Выбрать более строгий класс соблазнительно, и именно это ломает on-demand профиль на заблокированном телефоне.

После смены класса пересоздайте элемент, а не редактируйте атрибуты существующего, затем сохраните протокол заново со свежей ссылкой. Изменения атрибутов существующего элемента не всегда дают поведение, которое подразумевает документация, а пересозданный элемент снимает сомнения.

Ротация — там, где гниют сохранённые профили

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

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

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