Your application keeps working while the tunnel dies. Wi-Fi is still up, the internet still answers, and the private API the app depends on has become unreachable. The .NET event people usually reach for here, NetworkAvailabilityChanged, does not describe that situation at all. It reports a change in the aggregate condition of the machine, and that condition can stay true across a VPN disconnect and reconnect. An observer has to watch the client that owns the tunnel, not the network stack's summary of it.

Availability is an aggregate, not your tunnel

NetworkAvailabilityChanged fires when the machine's aggregate availability answer changes, and that answer counts any up interface other than loopback or a tunnel.

A VPN drop while Wi-Fi is up changes nothing it measures, which is why the event can stay silent through the exact failure you care about.

The state stream belongs to the client that owns the tunnel

Identify which component owns the connection: the OpenVPN GUI or service, OpenVPN Connect, or a process your application started and controls. Those are different owners with different supported interfaces, and the observer must use the one that matches the deployment. For an OpenVPN process you are authorised to integrate with, the management interface reports the current state, a history and real-time notifications. Parse the documented structured messages instead of searching a localised GUI caption for the word connected.

The management channel is a privileged control surface, so do not open a new listener or fight the GUI for a channel it already holds.

Generation guards stop stale results

Every observation belongs to a specific connection attempt. Increment a generation counter when a new attempt starts, when the channel is lost, and when the client reports exit.

Consider a probe started during connection A. The tunnel drops, connection B begins, and A's delayed HTTP success arrives afterwards. Without a guard, that old result turns the display green while B is still authenticating. Cancellation reduces wasted work, and the generation check is what stops an obsolete completion from becoming current state:

var generation = _generation;
var reachable = await ProbeAsync(token);
if (generation != _generation) return;
Publish(reachable);

Apply the same rule around the UI dispatch, because the marshalled update can arrive after the generation has moved on.

Initialization and reconnect are one lifecycle

Treat first connect, reconnect and channel loss as one lifecycle with a single set of rules:

  • Read the current state and subscribe to notifications in one step where the interface allows it.
  • Tag every observation with the connection identity and the generation.
  • Invalidate readiness on reconnect, exit, or a lost observation channel.
  • Probe the private resource only after the owner reports initialization complete.
  • Publish only results that still belong to the current generation.

A lost channel means unknown, not disconnected. Reporting unknown is more honest than reporting a disconnect you cannot prove, and it stops the retry logic from acting on a guess.

Report evidence, not a boolean

Return the named session state and the resource readiness as separate values, each carrying an age. A consumer that has to decide something can then see that the last successful probe is forty seconds old, which is different information from a fresh one.

A public IP lookup is not evidence that a private API used the required route, so do not present it as if it were.

Keep enforcement outside the observer

A callback runs after the fact. It cannot guarantee that no packet left the machine between the event and the handler. If the design requires blocking, the networking policy has to provide it, and the observer's job is to explain the evidence to its caller. Mixing the two produces code that believes it is a firewall because it holds a boolean. Keep the enforcement layer boring and testable, and let the observer stay a reader.

Then the UI question becomes simple: show what is known, with its age, and say unknown when it is.

Akụkọ ndị metụtara ya