Pressing Disconnect works, and ten seconds later the tunnel comes back anyway. That loop is rarely the VPN client misbehaving; it is a recovery script that only ever asked one question, "is it down?", and answered it by connecting. Once you separate the question the user answered from the question the system reports, the fix becomes a small amount of state rather than a longer sleep.

Two questions, not one

Should this profile be connected right now, and what does the system currently report? Those are different questions with different owners, and a recovery policy needs both before it acts. Intent is something your tool decides and remembers. Observed state is something the platform tells you.

IntentWhat you observeWhat to do
WantedDropped unexpectedlyAllow one supported recovery attempt
Paused by the userDisconnectedDo nothing until intent changes
WantedAlready connectingWait instead of starting a rival attempt
WantedAuthentication needs a personPrompt, then stop replaying credentials

Read the rows as a policy rather than a list of cases. The second row is the one that most scripts omit, and it is the only row that distinguishes a monitor from a machine that insists on being right.

Observed status is not a preference

Apple's Network Extension status model reports connecting, connected, disconnecting, disconnected and recovery states. All five describe the connection. None of them says "the user asked this profile to stay paused", because that is not a connection state at all. Clients that are not built on that framework expose their own supported controls and status calls, and they draw the same line in their own vocabulary.

The consequence is that a disconnected status can never be interpreted on its own. It means one of at least three things: the tunnel dropped, the user stopped it, or the system is between attempts. Only your own record of intent can tell those apart. Write that record down, keep it across restarts if it has to survive a reboot, and update it at the moment the user acts rather than inferring it later from a log line.

scutil --nc list
scutil --nc status "Office VPN"

Who else is allowed to start this profile

A pause promised by your helper is only as strong as the other owners of the same profile. An On Demand rule, a managed configuration or a second client can all request a connection, and your helper will look broken while it is behaving exactly as written.

Find every mechanism that can start the profile before you promise to honour a pause, and write down which one wins. Where a managed service deliberately withholds a personal pause option, that is an administrator decision about policy, not a gap for a script to fill by fighting management.

Make the retries boring

Bound the number of attempts and make each one responsive to something that changed: the network came back, the gateway answered, the credential prompt completed. An unavailable gateway, an expired identity and a rejected second factor need three different responses, and a tight loop gives all three the same one.

Watch what a fast retry actually produces on screen. Repeated dialogs that the user dismisses are worse than a single clear failure message, and they train people to click through prompts without reading them.

Pause and traffic policy are separate promises

A user-paused tunnel often leaves traffic blocked on purpose, which is the whole point of a kill switch style policy. That means Pause should promise exactly one thing: recovery stops. It should not silently change what the network permits.

Decide the traffic behaviour separately, document it next to the Pause control, and keep it consistent whether the tunnel is paused, failed or absent. Otherwise the user learns that pressing Disconnect does something unpredictable, and the next support ticket is about a network that "sometimes stops working" for no visible reason.

Akụkọ ndị metụtara ya