Two error codes account for most first attempts at a Network Extension VPN on iOS: NEVPNErrorDomain Code=4 and Code=5. Neither is a server complaint. One appears when the app writes settings into an object that was never read from disk, and the other when the write itself fails, most often because the profile is saved while it is still disabled. Both are fixed by changing the order of four calls, not by touching the IKEv2 parameters.

Load first, or the save has nothing to compare against

The manager holds a copy of the stored profile, and the object you edit is not that copy until you read it. Call loadFromPreferences first, edit inside its completion handler, then save. Skip the read and the framework sees a mismatch between what it has and what you are writing. In Swift the shape is the same for every step, and each step belongs in its own closure:

let mgr = NEVPNManager.shared()
mgr.loadFromPreferences { loadErr in
    guard loadErr == nil else { return }
    let proto = NEVPNProtocolIKEv2()
    proto.serverAddress = "vpn.example.net"
    proto.username = "alice"
    proto.useExtendedAuthentication = true
    mgr.protocolConfiguration = proto
    mgr.isEnabled = true
    mgr.saveToPreferences { saveErr in
        // do not start here; reload, then connect
    }
}

Starting the tunnel from inside the save closure is the same mistake in a different place. It runs before the profile has been re-read, so the start call sees the state you built rather than the state that was persisted. The save closure ends with a second loadFromPreferences, and only that completion handler calls startVPNTunnel.

The enabled flag is part of the save

A stored profile is inert until it is switched on. Set isEnabled = true in the same write, or the profile lands on disk disabled and startVPNTunnel refuses even though Settings shows a configuration that looks complete.

External edits are the other half of this. If the user changes something in Settings, or the device moves between networks, re-read before trusting the values you still hold in memory. The object does not observe those changes for you. A stale copy is how an app writes a profile that silently reverts the user edit. Two adjacent fields cause quiet trouble at the same moment: on-demand rules must be an array of rule objects rather than an empty placeholder, and disconnectOnSleep should reflect the behaviour you decided on rather than a copied default.

Only two protocols are available to third-party apps

Network Extension gives apps IPsec and IKEv2. L2TP and PPTP stay inside the system, so an app that needs them cannot have them, and the realistic answers are an IKEv2-capable gateway or a managed configuration profile.

Credentials follow the same pattern. Put the password in the Keychain and pass a persistent reference instead of the string. A nil reference at save time usually means the lookup ran before the item was written, not that the manager is broken.

Both of those are design decisions rather than workarounds. An app that keeps the password in its own preferences is choosing a path the framework discourages, and the failure it produces reads like a save error.

Rule out the entitlement before rewriting code

When loadFromPreferences never calls back at all, the likely cause is a missing Network Extension capability or a provisioning profile that does not carry it. That failure hangs instead of erroring, which is why working code gets rewritten for hours. Check the entitlement on the signed build you intend to ship, because a simulator run and a device run can differ and the target that matters is the device. Open the target signing settings and confirm the Network Extension capability is present for that exact build.

A provisioning profile that lacks the entitlement fails the same way on hardware, while the same build may load normally in the simulator. Test the signed archive on a real device before rewriting the call order.

Watch the status changes, not assumptions

Observe NEVPNStatusDidChangeNotification and log manager.connection.status after every step. A status that never leaves invalid means the profile was not stored or the entitlement is absent. A connection that reaches connecting and never connects points at the proposal or the credentials, which is a different investigation with different evidence.

Confirm the profile appears under Settings, General, VPN, and record the error from each closure. Those two facts separate an ordering bug from a permissions bug without guesswork.

Ingingo zihuye