Почему ваше приложение для Android не может добавить системный VPN-профиль
VpnService создаёт туннель, которым владеет ваше приложение; он не может добавлять или изменять записи PPTP, L2TP или IPsec телефона. Пройдите границу и выберите верный путь.
Вы нажимаете строку VPN в Настройках, видите профиль, установленный кем-то другим, и начинаете искать вызов, который его добавляет. Такого вызова нет. Приложение может открыть собственный туннель через VpnService, но записи в этом списке принадлежат платформе и тому клиенту, который их создал. Точное знание того, где проходит граница, экономит неделю погони за скрытыми классами.
Начните с того, что показывает экран настроек
Список VPN — это реестр профилей, установленных системой или клиентом производителя: PPTP, L2TP/IPsec, IKEv2 и строки, которые втолкнуло приложение OEM или MDM. Каждая строка несёт адрес сервера, учётные данные и реализацию протокола, живущую вне вашего процесса. Когда пользователь нажимает там «Подключиться», туннель поднимает клиент платформы, а не ваш код. Поэтому строка переживает перезагрузку и поэтому удаление вашего приложения никогда её не убирает.
Со стороны пользователя список выглядит как поверхность API, место, где добавление VPN должно быть на расстоянии одного интента. Это не так. Единственные публичные точки входа, которые его касаются, — интенты навигации, а всё, что в него пишет, принадлежит платформе или привилегированному компоненту.
Практическое следствие: если текст поддержки говорит пользователям добавить профиль в Настройках, ваше приложение вообще не участвует в этом сценарии.
Что на самом деле даёт VpnService
VpnService и VpnService.Builder описывают один туннель, которым владеет ваше приложение. Вы добавляете адреса, маршруты, DNS-серверы и MTU, ограничиваете трафик через addAllowedApplication() или addDisallowedApplication(), а затем establish() возвращает ParcelFileDescriptor для интерфейса TUN. С этого момента приложение читает IP-пакеты и пишет ответы, то есть протокол — ваш: реализовать его или встроить: OpenVPN, WireGuard, собственный транспорт.
Ничто в этой последовательности не говорит на PPTP, L2TP/IPsec или IKEv2. В Builder нет переключателя, который сказал бы использовать системный клиент L2TP, и нет обратного вызова, позволяющего управлять профилем, которым платформа уже владеет. Это и есть настоящая граница, и это граница проектирования, а не пробел в разрешениях.
Так что приложение вполне может быть VPN-клиентом на Android. Просто оно не может быть менеджером для клиентов, которые поставляются с телефоном.
Диалог согласия — не украшение
Поскольку ваш туннель может перехватывать трафик других приложений, система требует, чтобы пользователь явно его одобрил. prepare() возвращает Intent, который вы обязаны запустить, одобрение привязано к вашему пакету, а пока туннель активен, в строке состояния горит постоянный индикатор. Второй VPN при поднятии деактивирует первый, ведь Android держит один активный VPN одновременно.
Это правило одного активного сбивает людей при тестировании. Вы поднимаете свой туннель, пока подключён корпоративный клиент, и делаете вывод, что ваш код сломан, а платформа просто переключилась с другого клиента. Одобрения можно отозвать позже, и prepare() может вернуть null на устройствах, где нет активности согласия, поэтому establish() всё равно должен обрабатывать сбой.
Скрытые API и интенты настроек
Старые ответы указывают на классы com.android.internal и интерфейсы AIDL, которые использует встроенный клиент. Их нет в SDK, они меняются между выпусками без предупреждения, а магазины приложений считают такой доступ риском. Получение root тоже не делает их стабильными, потому что root и доступ к скрытым API — несвязанные проблемы.
Поддерживаемая навигационная подсказка — Settings.ACTION_VPN_SETTINGS, чьё значение android.net.vpn.SETTINGS. Разрешите её перед запуском, потому что некоторые сборки OEM не поставляют эту активность, и переходите к письменным шагам, если разрешение не удалось. Она ведёт пользователя к экрану и ничего не настраивает.
Пути владельца устройства и MDM — то исключение, которое обычно имеют в виду. DevicePolicyManager может назначить пакет постоянного VPN, а управляемые конфигурации могут предварительно загрузить настройки для клиента, который их поддерживает, но это всё ещё не публичный SDK, пишущий профиль PPTP.
Пройдите решение со стороны пользователя
Если ваш продукт должен переносить трафик, планируйте VpnService плюс настоящий стек протоколов и закладывайте время на сценарий согласия и правило одного VPN. Если цель — подключиться к существующему корпоративному профилю, ваша работа — это навигация: глубокая ссылка на экран настроек, документирование полей и предоставление туннелирования клиенту платформы. Если приложению нужно только добраться до частной конечной точки, собственный туннель ему вообще не нужен.
Запишите это решение до того, как писать код, потому что три пути после первого спринта почти ничего не имеют общего.
Последняя проверка перед стартом: в требовании сказано, что подключается наше приложение или что подключается устройство? Первое — это VpnService, второе — профиль, который вы не можете создать. Правильно сформулировать эту фразу дешевле любого рефакторинга.