Relaunch the app, ask the manager for connection status, and it reports disconnected while the system menu plainly shows a live tunnel. Or a save call returns an error that explains nothing, or a shared secret is declared missing seconds after it was assigned. Three symptoms, one mechanism: the manager is a view onto a system store, and a view reads as empty until somebody loads it.

Before debugging any of that, settle which API you are supposed to use. A system IPsec or IKEv2 tunnel fits the built-in manager; a protocol the system does not implement needs a tunnel provider with your own packet handling, and no combination of fields changes that.

The manager is a view onto a store

Create or fetch the manager, ask it to load its preferences, and let the completion handler finish before reading status or changing protocol settings. Enumerate saved configurations with the all-variants call when that is the task, rather than reusing a single instance for two purposes. Before the load lands, status is typically invalid, and mapping invalid onto “off” is precisely the bug that makes an app look wrong only after a relaunch.

Writing follows the same order. A save attempted on a manager that never loaded anything can fail without a useful message, because there is no stored identity for the change to apply to. Load, modify, save, then read status again. The object the system connects is the saved profile, not the one you were editing in memory.

Status changes arrive only while you are looking

Once preferences are loaded, status moves through disconnected, connecting, connected, reasserting and disconnecting. Initialise the interface from one read, then listen for the change notification instead of polling on a timer. Polling works in the foreground and lies in the background, which is how apps end up with a spinner that never resolves.

Notifications arrive only while the app is in the foreground. Anything that maintains state in the background will look stale after a relaunch, so refresh on launch and again when the app returns to the foreground. If a provider owns the tunnel, drive it through the matching session object; the plain connection property does not model a provider's lifecycle.

Secrets are references, not strings

A reported missing shared secret rarely means nobody assigned one. The protocol object wants a reference: one for the user password and one for the IPsec pre-shared key. Passing nil, or passing a reference derived from the wrong keychain entry, yields the same complaint as an empty slot. The error names the symptom and hides the cause, which is why people re-enter the same secret several times.

Write the secret as a generic password item first, then read back a persistent reference to it. A persistent reference survives relaunch, unlike a pointer held in memory, and it is the representation the profile serialises. Pick an accessibility class that still permits access once the device has been unlocked, or a tunnel that must reconnect on a locked screen will fail authentication with a perfectly valid secret.

The extension has its own keychain view

A network extension is a separate process, so the app's default keychain access does not reach it. Give both targets the same keychain access group in their entitlements, store the item once inside that group, and let each process read it. Without the group, the extension finds nothing and reports a credential failure the app cannot reproduce.

If the provider is configured through its provider configuration object, do not also embed the secret inside the app. Two copies drift apart, and the drift presents as bad credentials rather than as duplication. A widget or a second process reporting another state has the same explanation: each needs the group, and each has to load preferences before it reads.

Match the API to the protocol

The built-in manager carries IPsec and IKEv2 protocol objects. It does not turn into a generic SSL VPN client through additional fields, and a mismatched profile can save cleanly and still never connect. Decide which category you are in before investigating status, because a status bug and an unsupported protocol look identical from the outside.

For the next relaunch, the checklist is short: load preferences, read status, keep change subscriptions in the foreground, rebuild references from the shared group, and confirm the protocol object matches the API you implemented. Running that list takes a minute; guessing at it takes an afternoon.

Makomi ya ndenge moko