Обнаружение активного VPN в приложении Mac в песочнице
Публичного API для этого вопроса нет, а трюк с именем интерфейса — лишь эвристика. Шесть проверок, чтобы решить, на что ваше приложение действительно должно полагаться.
Нужен один экран, который ведёт себя иначе, когда туннель активен, а вызова, который сообщит вам об этом, нет. Это ограничение выпуска: в macOS ни один публичный фреймворк не сообщает, подключён ли произвольный VPN, а в iOS состояние принадлежит приложению, установившему конфигурацию. Проверки ниже ведут от этого предела к тому, что можно выпустить, и любая из них может прекратить поиск раньше.
Проверка 1: можно ли вообще ответить на этот вопрос?
Песочница решает вопрос раньше любого API. Процесс третьей стороны не может перечислить расширение другого поставщика и не может прочитать конфигурацию этого расширения, поэтому нет поддерживаемого объекта, у которого можно спросить.
Каждый обход упирается в ту же стену. Списки интерфейсов, scutil --nc list и прямое чтение системной конфигурации либо требуют прав, которые песочница не даёт, либо зависят от поведения, которое Apple никогда не обещала. Если плану нужно что-то из этого, проблема в плане. Функция, построенная на недокументированной детали, — это тикет в поддержку с датой поставки.
Проверка 2: приложение само управляет туннелем?
Если да, у вас есть поддерживаемый ответ для собственной конфигурации. Загрузите настройки, обработайте ошибку, затем прочитайте состояние и подпишитесь на изменения вместо опроса:
vpnManager.loadFromPreferences { error in
// handle a load failure before reading status
}
NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: nil,
queue: nil
) { note in
let status = (note.object as? NEVPNConnection)?.status
}Будьте точны в том, что это возвращает: состояние установленного вами туннеля. Пользователь с другим клиентом будет показан как отключённый, и это не ошибка в вашем коде. Решите, какой из этих двух вопросов действительно задаёт ваша функция, прежде чем строить на ответе.
Проверка 3: что эвристика на самом деле вам скажет?
Широко скопированный подход запрашивает у системного словаря прокси его записи с областью действия и читает ключи, которые случайно оказываются именами интерфейсов:
let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as NSDictionary?
let scoped = settings?["__SCOPED__"] as? [String: Any]
let names = scoped?.keys ?? []Ключ, начинающийся с tap, tun, ppp, ipsec или utun, означает, что присутствует интерфейс с областью действия туннеля. Сопоставление полного имени сразу терпит неудачу, потому что iOS 12 поставлялась с utun1 и utun2, а некоторые профили выглядят как ipsec4, ppp0 или utun0. Сравнивайте префиксы.
Тем не менее имя не определяет владельца. Системные функции и расширения других поставщиков тоже создают интерфейсы utun, и такой может существовать, не неся ничего из вашего трафика. Читайте это как подсказку, что рядом есть что-то в форме туннеля, и не более. Ложное срабатывание здесь дёшево, если вы ограничиваете одну функцию, и дорого, если ограничиваете экран входа.
Проверка 4: можно ли вместо этого проверить пункт назначения?
Если приложению нужно только знать, доступна ли внутренняя служба, проверьте эту службу. Короткая проба с тайм-аутом отвечает на вопрос, от которого функция действительно зависит, и продолжает работать, когда пользователь меняет клиент или туннель меняет форму.
Доступность — это наблюдение о вашем пути. Имя интерфейса — это вывод о чём-то смежном с вашим путём, поэтому эвристика порождает споры в отчётах об ошибках, а проба порождает доказательства. Если проба не проходит, пока пользователь настаивает, что VPN включён, у вас теперь есть конкретный путь для отладки: разрешение имён, маршрут или разрешение службы.
Проверка 5: может ли платформа принудительно обеспечить нужное?
Когда требование — гарантия, что ничто не вырвется из туннеля, профиль и есть место для этого, а не детектор. Включение includeAllNetworks в конфигурации полного туннеля на iOS 14 или новее меняет поведение платформы вместо того, чтобы просить приложение это вывести. Флаг документирован в справке NEVPNProtocol.
Это другой вид ответа: принуждение вместо наблюдения. Он к тому же применим только к профилям, которые вы контролируете, поэтому не помогает потребительскому приложению, наблюдающему за чужим туннелем.
Проверка 6: сделайте отказ безопасным и наблюдаемым
Отключайте только ту возможность, которой нужен туннель, и оставляйте остальное приложение работоспособным. Детектор, который прячет весь интерфейс всякий раз, когда не уверен, создаёт простой для каждого, чей туннель использует имя, которого вы никогда не предполагали. Вернитесь вместо этого к консервативному поведению: локальные функции продолжают работать, и отключается только тот удалённый вызов, которому действительно нужен туннель. Этот выбор — продуктное решение, и он заслуживает быть записанным рядом с эвристикой.
Записывайте то, что наблюдали: имена интерфейсов, версию платформы и результат одной пробы. Это превращает следующий отчёт из спора в проверку. Затем предпочитайте поддерживаемое объявление, когда оно у вас есть, потому что управляемая конфигурация может явно нести намерение и убрать большую часть причины вообще проверять имена.
Если проверки с 1 по 5 оставили вас без поддерживаемого ответа, выпустите пробу и честно обозначьте результат. Документированный сигнал наилучшего усилия — лучшее продуктное решение, чем частный API, который ломается в следующем выпуске.