"Is the VPN up?" is really three questions wearing one sentence. Which of them your code needs is the first decision, and it is the one most implementations skip. Does a tunnel exist on this device, does traffic to this destination leave through it, or does the far end answer with the identity you expect. Pick the property, then pick the signal that measures it, then say in the log which one you actually measured.

Three properties, three signals

Compressed to one line each, the three questions have three different answers. The table below is the decision, not a summary of it.

PropertySignal that answers itWhat it still leaves open
ExistenceThe platform's own connection state: RAS on Windows, NetworkManager on D-Bus on Linux, scutil on macOSTunnels of a type the platform does not manage, which includes most third-party clients
PathA route lookup for the one destination, or a scoped proxy or interface key that names the tunnelWhether the far end is the peer you trust
IdentityAn authenticated response from the far end, or a source-address policy applied on the serverWhich local interface carried the packets

Read the last row slowly. A private host that answers proves something answered, not that the answer came through the tunnel, and it says nothing about who is on the other end. If the property you need is the path, measure the path.

Existence: the platform's own state

Windows answers this for native profiles through the Remote Access Service API. RasEnumConnections lists what is connected, RasGetConnectStatus reports the state of one entry, and RasGetLinkStatistics adds speed, byte counters and connection age. The limits are the point: OpenVPN, WireGuard and vendor clients never register a RAS connection, so a healthy tunnel of that kind shows an empty list and naive code reports it as down.

Linux desktops expose the same property through NetworkManager on D-Bus, where PropertiesChanged on the VPN connection object turns the check into an event and the State property reports when the link reaches connected. From a shell, nmcli connection show --active reads the same information, which is what you want while debugging. macOS keeps that list in the System Configuration framework, with scutil as its command-line view. All three describe a connection the platform manages, and all three go quiet for a tunnel it does not.

Path: where the packet actually goes

Name matching is the shortcut everyone reaches for first: enumerate interfaces, test the string against tun, tap, ppp or ipsec, and treat a hit as proof. It is a heuristic, and the interface set can look the same whether or not anything is moving through it. Older iOS builds answered this way and later ones stopped, because the visible interfaces no longer distinguished the two states.

What replaced it there is the system proxy dictionary. While a VPN is connected the proxy settings call returns keys scoped to the tunnel-related interfaces, and with nothing connected the dictionary comes back empty. That is a weaker statement than it sounds, since it records a system belief that a tunnel exists rather than which destination uses it.

Android has its own version of the trap: the vpn.connectivity broadcast reaches receivers declared in the manifest on some builds and runtime-registered receivers on others. Treat it as best effort, and keep a direct probe behind it.

When the destination is what matters, ask the routing table instead of the adapter list. Default-gateway heuristics break on multi-homed machines and on tunnels that install a gateway of their own.

Identity: probe the far end

A loop that pings the far side every few seconds is the cheapest check of this kind, and it fails in a useful direction. Aim it at an address that exists only inside the private network, because a public one answers over any working uplink and proves nothing at all. Where ICMP is filtered, a short TCP connect to the service port does the same job.

A server-side check has exactly one signal available to it: the source address on the connection. Maintaining the range inventory is upkeep work, and it cannot separate a tunnel from any other network that happens to use the same block.

Coarse policy can live with that. Identity cannot, so authenticate the client rather than trusting where its packets seem to come from.

Log the category, not the verdict

Keep the failure categories apart in the log: no interface at all, interface present but the destination unreachable, destination reachable but authentication rejected. Those are three different owners, and each one opens a different investigation.

Collapsing them into one VPN down sends the report to a team that cannot fix it. The category is the useful output; the verdict is what makes it wrong.

ተዛማጅ ጽሑፎች