You tap the VPN row in Settings, see a profile someone else installed, and start looking for the call that adds one. There is no such call. An app can open a tunnel of its own through VpnService, but the entries in that list belong to the platform and to whichever client created them. Knowing exactly where the line sits saves you a week of chasing hidden classes.

Start with what the Settings screen shows

The VPN list is a registry of profiles that the system or a vendor client installed: PPTP, L2TP/IPsec, IKEv2, and whatever rows an OEM app or an MDM pushed. Each row carries a server address, credentials and a protocol implementation that lives outside your process. When the user taps Connect there, the platform client brings up the tunnel, not your code. That is why the row survives a reboot and why uninstalling your app never removes it.

From the user's side the list looks like an API surface, a place where adding a VPN should be one intent away. It is not. The only public entry points that touch it are navigation intents, and everything that writes to it belongs to the platform or to a privileged component.

Practical consequence: if a support script tells users to add the profile in Settings, your app is not part of that flow at all.

What VpnService actually hands you

VpnService and VpnService.Builder describe one tunnel that your app owns. You add addresses, routes, DNS servers and an MTU, you scope traffic with addAllowedApplication() or addDisallowedApplication(), and then establish() returns a ParcelFileDescriptor for a TUN interface. From that point the app reads IP packets and writes replies, which means the protocol is yours to implement or embed: OpenVPN, WireGuard, a custom transport.

Nothing in that sequence speaks PPTP, L2TP/IPsec or IKEv2. Builder has no switch that says use the system L2TP client, and there is no callback that lets you drive a profile the platform already owns. That is the real boundary, and it is a design boundary rather than a permissions gap.

So an app can absolutely be a VPN client on Android. It just cannot be a manager for the clients that ship with the phone.

The consent dialog is not decoration

Because your tunnel can capture other apps' traffic, the system makes the user approve it explicitly. prepare() returns an Intent you must launch, the approval is tied to your package, and while the tunnel is up the status bar shows a persistent indicator. A second VPN coming up deactivates the first, since Android runs one active VPN at a time.

That single-active rule trips people up in testing. You bring up your tunnel while a corporate client is connected and conclude your code is broken, when the platform simply switched away from the other client. Approvals can be revoked later, and prepare() can hand back null on devices where the consent activity is missing, so establish() still has to handle failure.

Hidden APIs and settings intents

Older answers point at com.android.internal classes and AIDL interfaces that the built-in client uses. They are not in the SDK, they change between releases without notice, and app stores treat that access as a risk. Rooting the device does not make them stable either, because root and hidden-API access are unrelated problems.

The supported navigation aid is Settings.ACTION_VPN_SETTINGS, whose value is android.net.vpn.SETTINGS. Resolve it before launching, because some OEM builds do not ship that activity, and fall back to written steps when resolution fails. It takes the user to the screen and configures nothing.

Device-owner and MDM paths are the exception people usually mean. DevicePolicyManager can designate an always-on VPN package, and managed configurations can preload settings for a client that supports them, but that is still not the public SDK authoring a PPTP profile.

Walk the decision from the user's side

If your product needs to carry traffic, plan for VpnService plus a real protocol stack, and budget time for the consent flow and the one-VPN rule. If the goal is to connect to an existing corporate profile, your job is guidance: deep-link to the settings screen, document the fields, and let the platform client do the tunneling. If the app only needs to reach a private endpoint, it does not need a tunnel of its own at all.

Write that decision down before you write code, because the three paths have almost nothing in common after the first sprint.

A last check before you start: does the requirement say our app connects, or the device connects? The first is VpnService, the second is a profile you cannot create. Getting that sentence right is cheaper than any refactor.

Winndannde jowitiiɗe heen