--reconnect-timeout answers one narrow question: after the tunnel drops, how long does this client process keep trying before it gives up and exits. The OpenConnect manual page records that option with a documented default of 300 seconds for retries that follow a lost link or a peer that stopped answering, and the number is a deadline for one running client rather than proof the VPN stays reachable. It is also not a policy for keeping the process alive, which is where most of the confusion starts.

The confusion compounds once a second mechanism sits on top. A supervisor that restarts the client, a prober that pings the gateway, and the built-in retry window can all react to one failure, and the incident then yields three timelines nobody can reconcile.

What the reconnect window actually bounds

Picture the access network vanishing for a few seconds. The client sees the link drop, or stops hearing dead-peer replies, and starts re-establishing the session while holding what state it can. Retries continue until the gateway accepts again or the window runs out, and a window that runs out ends in a non-zero exit. That exit, rather than the timeout value, is where lifecycle ownership passes to whoever supervises the process.

The window says nothing about how often a probe runs, and it cannot extend authentication. If the gateway revoked the session, or the account now demands a fresh second factor, more waiting will not make the old session valid again. A retry lands only when the path returns, the gateway still recognises the session, and the credentials it asks for stay usable, so read the number as a deadline rather than a liveness interval.

Nor does the window govern what applications do while it runs. Depending on the platform and how far the failure got, the client may be reconnecting with routes still installed, or after they were pulled. That is why a request issued mid-outage tends to hang until it times out instead of failing fast.

Supervisor restarts and client retries are two layers

Let the running client spend its own window, and let the service manager react to the exit. A foreground client under systemd with a restart policy gives exactly that split: retries for a bounded period, then one restart decision. The result is a single timeline you can still reconstruct afterwards.

A backgrounded client plus an independent poller that starts another copy when it cannot see an interface is the pattern that goes wrong. Both processes then contend for the same device node, routes, and resolver settings. If you do want a daemon, put the option on the command line the client actually starts with:

sudo openconnect --background --pid-file=/run/openconnect/vpn.pid \
  --reconnect-timeout=180 \
  --user=<account> https://vpn.example.edu

Two details in that line carry weight. --background detaches the process, so the pid file becomes the only dependable handle on it, and the timeout is spent only while that same process lives; a supervisor that kills it leaves no budget to continue with.

Intentional disconnects need their own signal. A planned pause and a real outage look identical to a naive watcher, both of them reading as "not connected". Drive the pause through the supervisor's stop operation, or a marker the wrapper understands, so a deliberate halt does not trigger a restart nobody asked for.

Keeping the handoff from racing the retry loop

Unattended recovery depends on credentials that renew without a person present. A keyring that unlocks only inside an interactive desktop session, or a token that always needs a typed code, will strand the client at the authentication stage once the path is back. Settle that workflow before lengthening the window, because a longer window only grants the same blocked prompt more time to fail.

Server verification should not be relaxed to reduce prompts. The pin or trust anchor that validates the gateway is what makes an automatic reconnect safe, and disabling it to avoid a certificate question trades a visible interruption for an invisible one. Keep the verified identity and repair the prompt instead; a client willing to connect to any gateway is worse than one that stops and waits.

Interface presence is not a health signal either. A tunnel device can exist with a dead session behind it, and a launcher process can exist with no tunnel at all. Neither observation survives a real request. Health has to come from what the user needs, such as a request that traverses the tunnel to a known internal service.

What to verify during and after an outage

Run four events as separate tests: an interruption shorter than the window, an outage longer than it, an authentication failure, and an intentional stop. Each should end in a different, predictable state, and the boundary between the first two is the entire reason for picking a value.

Keep the checks outward-facing, since the process table proves very little on its own. Watch the pid hold steady through the short cut, then confirm it is gone and replaced once after the long one:

curl -sS --max-time 5 -o /dev/null -w '%{http_code}\n' https://intranet.example.internal/health
cat /run/openconnect/vpn.pid
ps -o pid,etime,cmd -p "$(cat /run/openconnect/vpn.pid)"

During the short interruption, confirm the client really retried rather than exiting early, and that routes and resolver entries came back without duplicates. For the longer outage, confirm a clean exit and exactly one restart followed. For the authentication failure, confirm the client does not spin against a gateway that will keep refusing it. Add a check that the pid returns to a single value rather than accumulating.

For the deliberate stop, the health URL should stay unreachable and the process should stay down. Then close the loop from the application side: the same request must fail while the tunnel is gone and must succeed after it returns, from the source address your policy expects. A green tunnel icon is not evidence, but a completed request is.

Ingingo zihuye