Pinging 192.0.2.10 tells you that one address answered an ICMP echo, and that is all it tells you. On a network with no tunnel it can still succeed, because the address may sit on the local subnet; with a healthy tunnel it can fail, because the server drops ICMP while serving HTTPS. The check your script needs is a platform status query plus a route test for the destination the application uses. Treat those as two separate fields, never as one boolean.

What an echo reply actually proves

ICMP is a reachability protocol with no notion of interfaces or tunnels. When a reply arrives, the kernel reports that a host responded; it does not report the route that carried the request or the one that carried the answer. Filtering makes the signal noisier still, since a firewall that blocks echo requests says nothing about the ports behind it. And a split-tunnel profile can exclude the tested address on purpose while every other destination works.

What you actually want to know is whether a specific service is usable over the path the app takes. Pick the host and port the app calls, then require an answer at the application layer.

Reachability and tunnel state are different facts, and either one can be true while the other is false. Keeping them apart tells you who owns the failure. Collapsing them into one check produces a ticket nobody can route.

Ask Android for the transport, not for a ping

Android answers the status question directly through ConnectivityManager. The active network's capabilities carry a VPN transport flag, set when traffic is routed through a VPN interface. The call is local, cheap, and independent of whether any remote host responds.

This Java version returns false rather than throwing when there is no active network. Because it reads the active network, it reflects the profile the user chose in Settings.

ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
NetworkCapabilities caps = cm.getNetworkCapabilities(cm.getActiveNetwork());
boolean tunnel = caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);

For change notifications, register a NetworkCallback instead of polling on a timer. Do not confuse that with VpnService.prepare, which only reports whether the user granted permission for a VPN, not whether packets are flowing. Revocation arrives through VpnService.onRevoke, yet forgetting a profile in Settings produces no broadcast at all.

iOS: interface names are a heuristic, not a contract

Many iOS snippets inspect the proxy settings, find the __SCOPED__ dictionary, and look for interface keys such as tap, tun, utun2, ppp, or ipsec. That approach is a naming convention, not a documented guarantee, so an OS update that renames an interface can break it. It also tends to report true for stale entries that outlive the tunnel.

When your app owns the configuration, NEVPNManager gives you a real status instead. It describes the tunnel your app started; with a per-app VPN, other apps may sit outside that tunnel entirely.

Reload preferences before reading the status, since the value changes as the tunnel comes and goes:

let manager = NEVPNManager.shared()
manager.loadFromPreferences { error in
    let up = manager.connection.status == .connected
    print(up)
}

A platform status flag still does not prove that traffic reaches the peer. That gap is what the route test covers, and it is why one boolean is not enough for support.

Windows profiles expose a real status field

Windows tracks VPN profiles separately from adapters, and Get-VpnConnection exposes a ConnectionStatus for each profile it knows. Profiles created through the Windows client or a provisioning package show up here. The cmdlet is not a general tunnel detector.

Read the field for a named profile, adding -AllUserConnection when the profile belongs to all users:

Get-VpnConnection -Name "Fabrikam" | Select-Object Name, ConnectionStatus, ServerAddress

Third-party clients often stay invisible to this cmdlet even while their tunnel is up, so an empty result means unknown, not disconnected. Adapter listings are equally misleading: a vendor adapter can persist while idle, and some adapters exist only during a session. For a profile Windows owns, rasdial reports an exit code you can branch on. All of these describe local belief, not peer reachability.

Status plus route: the two-part check

A connected profile says nothing about where packets go. Split tunneling can keep the tunnel up while excluding the destination you care about. Always-on profiles stay connected with a dead server, and a kill switch blocks traffic on purpose when the tunnel drops.

Test both layers: confirm a VPN transport or profile is active, then confirm the destination resolves and answers along that path. On Linux, ask the kernel which interface it would use:

ip route get 192.0.2.10
resolvectl query intranet.example.test

When the client is outside your control, log both results and let the pattern point at the layer. Connected plus a failed route test suggests split tunneling, DNS, or policy. Disconnected plus a working route test means another path is carrying the traffic.

A test you can run in order

Record the platform status first, before you change any network state. Resolve the hostname and note the address family, since an AAAA record can take a different path than an A record. Open the service port and demand an application response rather than a bare handshake. Keep each observation in its own log field.

On a Linux box with dig and curl, three commands cover resolution, route, and the service itself. Run them with the tunnel up and again with it down:

dig +short intranet.example.test
ip route get 198.51.100.20
curl -sS -o /dev/null -w "%{http_code}" --max-time 8 https://intranet.example.test/health

Four separate fields beat one boolean, because status, DNS, route, and policy fail independently and land on different teams. A terminal ping remains a fine manual sanity check. It is a poor definition of connectivity for automation, where the platform API and a route test answer the actual question.

متعلقہ مضامین