Three different products answer to the name iOS VPN, and they do not share an API. Personal VPN rides the system's built-in IPsec and IKEv2 transports. A custom tunnel runs your own protocol inside a network extension. A configuration profile configures either one and can add on-demand behaviour. Choosing by the transport you must speak is faster than choosing by whichever tutorial compiled first.

Run the checks below in order, because each step assumes the previous one produced a valid configuration.

Step 1: choose by transport

If the server is IKEv2 or Cisco-style IPsec, NEVPNManager is enough and the system transports come for free. If the server speaks OpenVPN, WireGuard or anything else, NEVPNManager cannot help: it has no hook for a third-party protocol. That case needs NETunnelProviderManager with a packet tunnel provider that implements the protocol. A profile only distributes configuration, and it cannot add a transport the operating system lacks.

PPTP is not a target on current systems. Shared-secret IPsec still expects the group name in remoteIdentifier, a keychain item holding the secret, and extended authentication when the server asks for a username. Each missing piece surfaces as the same generic configuration error, so add them all before debugging anything else.

Step 2: respect the save, load, start order

NEVPNManager writes to preferences, reads them back, and only then starts. Fill the protocol and manager objects, call saveToPreferences, call loadFromPreferences, and call startVPNTunnel from the load completion handler. Starting before the load returns produces a configuration-invalid error, NEVPNErrorDomain code 1, on a configuration that is otherwise correct.

The shape is short enough to keep in one place:

manager.protocolConfiguration = proto
manager.isEnabled = true
manager.saveToPreferences { }
manager.loadFromPreferences { manager.connection.startVPNTunnel() }

startVPNTunnel returns when the request is accepted, not when the tunnel is up. Observe NEVPNStatusDidChange and read connection.status for the real state. An app that assumes success at the call site reports a connected VPN while the extension is still negotiating. A status of connecting is normal for a moment; a status that never leaves it is the failure worth logging.

Step 3: give the provider process what it needs

A custom transport runs in a separate process with its own bundle identifier and its own entitlement. Both the container app and the provider need the network-extension entitlement at signing time, and a Developer ID build outside the App Store still has to carry it in the provisioning profile. A missing entitlement looks like a protocol bug because the extension simply fails to start.

There is no public socket descriptor to borrow. NEPacketTunnelProvider hands you NEPacketTunnelFlow with read, write and cancel, and code that reaches for private key paths breaks between releases. The provider keeps running while the app is backgrounded, so the container app needs no background mode for that.

Step 4: on-demand is a rule set, not a switch

isOnDemandEnabled only arms the rules; NEOnDemandRuleConnect and its siblings decide when the tunnel comes up. An evaluate-connection rule with a probe URL is only as good as that URL. If the probe answers without the tunnel, the rule concludes the private network is already reachable and never connects, so point it at something that replies only from inside.

Always-on is a separate promise. It needs a supervised device and a managed profile, and an unsupervised user can remove a profile whenever they like. Do not ship a design that depends on always-on unless the fleet is supervised.

Step 5: prove the path, not the label

Scanning for tun, tap, ppp or ipsec interfaces proves that a VPN is configured, not that your request used it. NWPathMonitor with usesInterfaceType(.other) says a virtual interface is available, and a viability handler can test one host. Neither proves the exit path, so combine them with a request to a resource only the tunnel can reach. Interface names are a diagnostic aid, never a gate that application logic should branch on.

For a field check, compare the public address with and without the tunnel and fetch an internal-only page. A status call can report connected while the provider negotiates, so let the resource result decide and treat the status as supporting evidence.

Amanqephe ahlobene