Cron restarts a perfectly healthy VPN every ten minutes, and the same job leaves a dead one running for a week. Both failures come from treating up as a single fact when it is at least four. A device can be administratively enabled, correctly routed, fully encrypted, and still carry nothing useful. Separate the observations first, and the check almost writes itself.

Link flags describe the device, not the path

The check everyone copies greps for a hardware address of all zeros. That trick separates a genuine interface from a placeholder in some drivers and proves nothing in the rest, because a tun device has no MAC to report in the first place. ip -brief link show tun0 gives flags instead: UP means an administrator enabled the device, and LOWER_UP means the driver considers the link usable. Read together, they tell you the device exists and is not administratively down. Neither of them tells you a packet has anywhere to go.

OpenVPN creates its tun device before routes are installed, so the flags can look perfect while nothing is routed. Treat them as a precondition, never as the answer.

Ask where a packet would go

Route lookup answers the question the flags cannot. ip route get 10.8.0.1 from 10.8.0.2 names the interface, the source address and the table the kernel would use, which is exactly what a split tunnel needs to be checked against. Without the source address you are asking something else, and with policy routing the answer can differ per source. This is the cheapest honest test, because it consults the same rules your traffic will.

For WireGuard, add the handshake. wg show prints a latest-handshake timestamp per peer, and a timestamp that stopped advancing means the peer is gone no matter what the interface flags say. A configured interface with no recent handshake is one of the most common false positives in home-grown monitoring.

Table 220 tells IPsec's story

strongSwan installs routes for negotiated selectors in table 220 when route installation is enabled. ip route list table 220 therefore separates negotiated from routed: an empty table next to a loaded connection means the IKE and CHILD SAs came up while no traffic is being directed into them. Check ip xfrm state alongside it, since that lists the installed security associations and their endpoints.

A trap policy is the case to watch for. It waits for interesting traffic instead of routing it, so the table stays empty until something triggers the tunnel.

Manager state and daemon state are different lists

NetworkManager lists only the connections it owns. nmcli -t -f NAME,TYPE connection show --active returns nothing for a tunnel started directly by wg-quick or by an OpenVPN daemon.

That distinction matters when a script has to decide which of two managers to query. Pick the one that actually owns the tunnel on this host and ignore the other's silence. If both exist, decide which is authoritative and disable the second, because two managers rewriting routes on one interface produce failures nobody can reproduce. A missing entry is information about the manager, not about the tunnel.

Gate the next step on the probe it needs

Polling a UDP port proves nothing about readiness, since UDP has no handshake and nc -u succeeds as soon as the packet leaves. Probe what the next step actually consumes: a TCP connect to the forwarded port, a request to a host inside the tunnel, a name that only resolves there. Bound the wait with a timeout and an attempt count, and fail loudly when it runs out.

Hooks beat polling when the client offers them. OpenVPN's --up and --down scripts run after the interface changes state, which removes the race entirely, but they run with the privileges the daemon kept. If OpenVPN dropped to an unprivileged account through user and group, the hook runs as that account. Restarting a system service then requires sudo to permit exactly that command. Hand the retry to the service manager rather than a cron loop: a bounded restart policy with a start limit surfaces a broken tunnel, while an unbounded loop hides it indefinitely.

Artaigilean co-cheangailte