Implementing Per-App VPN in Mobile Apps
Corporate app accesses internal API over public internet because IT isn't ready for device-level VPN on BYOD employee phones. Need tunnel only for specific app traffic—rest goes direct. This is Per-App VPN.
Two Fundamentally Different Architectures
On Android Per-App VPN built on VpnService from android.net.VpnService. App creates virtual TUN interface and processes IP packets itself—either forwards to corporate gateway via WireGuard/OpenVPN or uses HTTP CONNECT proxy. Limit tunnel to specific app via VpnService.Builder.addAllowedApplication():
val builder = VpnService.Builder()
.addAddress("10.0.0.2", 32)
.addRoute("192.168.1.0", 24) // only corporate subnet
.addAllowedApplication("com.company.app") // only our package
.setSession("Corp VPN")
.setMtu(1400)
val vpnInterface = builder.establish()
Using addDisallowedApplication instead of addAllowedApplication tunnels all except listed. Must understand exactly what scenario customer needs.
On iOS completely different. Apple doesn't give direct TUN access to regular apps. Per-App VPN via Network Extension framework—specifically NEAppProxyProvider (proxy-based) or NETunnelProvider (VPN tunnel). Both require special entitlement com.apple.developer.networking.networkextension issued via Apple Developer Portal and costs additional review.
// Configure via NEVPNManager
let manager = NEVPNManager.shared()
manager.loadFromPreferences { error in
let proto = NEVPNProtocolIKEv2()
proto.serverAddress = "vpn.corp.example.com"
proto.authenticationMethod = .certificate
proto.identityReference = certRef // from Keychain
proto.useExtendedAuthentication = false
manager.protocolConfiguration = proto
manager.isEnabled = true
manager.saveToPreferences { _ in
try? NEVPNManager.shared().connection.startVPNTunnel()
}
}
Difference: NEVPNManager is device-level VPN managed via system settings. For true per-app on iOS need MDM profile with PerAppVPN config. Without MDM can't restrict tunnel to one app via iOS means.
Where It Most Often Breaks
Android: Always-On VPN and Doze Mode. If continuous connection required, VpnService must declare as foreground service. Doze Mode kills background services, tunnel breaks silently. Solution—PowerManager.WakeLock + JobScheduler for reconnect, or WireGuard-based solution handling sleep better.
iOS: extension death. NEAppProxyProvider runs in separate Extension process with limited lifetime. If extension crashes—iOS doesn't always restart immediately. Crashlytics in extension doesn't work by default (no main bundle), must init SDK manually with explicit plist path.
Bypass corporate proxy on Android 10+. From API 29, apps in PRIVATE_DNS mode don't use system proxy by default. If corporate network routes via HTTP proxy—must explicitly set Proxy.setDefaultSelector() or use ProxySelector in OkHttp:
val client = OkHttpClient.Builder()
.proxySelector(CorpProxySelector(proxyHost, proxyPort))
.build()
What Must Be Clarified Before Development
- WireGuard, OpenVPN or IKEv2 on gateway side?
- BYOD or corporate devices with MDM?
- Need split-tunneling (only corporate hosts via VPN)?
- Certificate pinning requirements on corporate gateway?
Answers fundamentally change architecture.
Implementation Stages and Timeline
Design + choose protocol → develop VpnService/Network Extension → integrate with corporate gateway → test on real devices (including airplane mode and Doze).
Android with WireGuard tunnel via ready library wireguard-android—3–4 days. Custom VpnService with proxying—5–7 days. iOS with NETunnelProvider and MDM profile—from 1 week including Apple entitlement.







