Реалізація VPN-підключення з мобільного додатка (Per-App VPN)
Корпоративний додаток звертається до внутрішнього API через публічний інтернет, тому що IT-відділ не готовий розгортати повноцінний device-level VPN на BYOD-пристроях сотрудників. Потрібен туннель лише для трафіку конкретного додатка—решта йде напрямки. Це й є Per-App VPN.
Дві принципіально різні архітектури
На Android Per-App VPN будується на VpnService з пакета android.net.VpnService. Додаток створює віртуальний TUN-інтерфейс та сам обробляє IP-пакети—або форвардить їх у корпоративний шлюз через WireGuard/OpenVPN, або використовує HTTP CONNECT proxy. Обмежити туннель конкретним додатком можна через VpnService.Builder.addAllowedApplication():
val builder = VpnService.Builder()
.addAddress("10.0.0.2", 32)
.addRoute("192.168.1.0", 24) // лише корпоративна підсіть
.addAllowedApplication("com.company.app") // лише наш пакет
.setSession("Corp VPN")
.setMtu(1400)
val vpnInterface = builder.establish()
Якщо замість addAllowedApplication використовувати addDisallowedApplication—туннель працює для всіх додатків крім указаних. Потрібно точно розуміти, який сценарій потребує замовник.
На iOS зовсім інакше. Apple не дає прямого доступу до TUN-інтерфейсу з звичайного додатка. Per-App VPN реалізується через Network Extension framework—конкретно NEAppProxyProvider (для proxy-based) або NETunnelProvider (для VPN-туннеля). Обидва варіанти потребують спеціального entitlement com.apple.developer.networking.networkextension, який видається через Apple Developer Portal та коштує додаткового ревью.
// Конфігурація через NEVPNManager
let manager = NEVPNManager.shared()
manager.loadFromPreferences { error in
let proto = NEVPNProtocolIKEv2()
proto.serverAddress = "vpn.corp.example.com"
proto.authenticationMethod = .certificate
proto.identityReference = certRef // з Keychain
proto.useExtendedAuthentication = false
manager.protocolConfiguration = proto
manager.isEnabled = true
manager.saveToPreferences { _ in
try? NEVPNManager.shared().connection.startVPNTunnel()
}
}
Різниця в тому, що NEVPNManager—це device-level VPN, керований через системні налаштування. Для настоящого per-app на iOS потрібен MDM профіль з PerAppVPN конфігурацією. Без MDM неможливо обмежити туннель одним додатком засобами iOS.
Де ломається чаще всього
Android: Always-On VPN та Doze Mode. Якщо потрібна постійна конекція, VpnService потрібно об'явити як foreground service. У Doze Mode система убиває фонові сервіси, та туннель рвітця без попередження. Рішення—PowerManager.WakeLock + JobScheduler для реконнекту, або перехід на WireGuard-based рішення, яке краще переживає засипання.
iOS: смерть розширення. NEAppProxyProvider працює в окремому Extension process з обмеженим часом життя. Якщо extension крашится—iOS не завжди його перезапускає негайно. Crashlytics у extension не працює за замовчуванням (немає main bundle), потрібно ініціалізувати SDK вручну з явним шляхом до plist.
Обхід корпоративного proxy на Android 10+. Почнення з API 29, додатки в режимі PRIVATE_DNS за замовчуванням не використовують системний proxy. Якщо корпоративна мережа маршрутизує через HTTP proxy—потрібно явно прописати Proxy.setDefaultSelector() або використовувати ProxySelector у OkHttp:
val client = OkHttpClient.Builder()
.proxySelector(CorpProxySelector(proxyHost, proxyPort))
.build()
Що реально потрібно з'ясувати до старту розробки
- WireGuard, OpenVPN або IKEv2 на стороні шлюза?
- BYOD або корпоративні пристрої з MDM?
- Потрібен ли split-tunneling (лише корпоративні хости через VPN)?
- Вимоги до certificate pinning на корпоративному шлюзі?
Відповіді на ці питання міняють архітектуру кардинально.
Етапи та терміни
Проектування + вибір протоколу → розробка VpnService/Network Extension → інтеграція з корпоративним шлюзом → тестування на реальних девайсах (включаючи режим авіа та Doze).
На Android з WireGuard-туннелем через готову бібліотеку wireguard-android—3–4 дні. Кастомний VpnService з проксіюванням—5–7 днів. iOS з NETunnelProvider та MDM-профілем—від 1 тижня з урахуванням часу на отримання entitlement від Apple.







