A reconnect that finishes in a second tells you the client is fast. It does not tell you whether anything escaped while the tunnel was down. A reconnect that takes a minute tells you the client is slow, and still not whether direct traffic was permitted in the gap. Those are two measurements, and a kill-switch test that only reports the first one has not tested the switch.

Two observations, two separate verdicts

The availability question is simple: when did the tunnel become usable again, and did the protected application need a new connection to get there. The enforcement question is different: which packets left the machine between the moment protection dropped and the moment it returned.

Answer them with separate records. One line says the gap lasted a given number of seconds. Another line says whether an unprotected request succeeded during that gap. Merging them into a single pass or fail hides the case that matters most, which is a fast reconnect with a leak.

Build a probe you can time

Use a request that is harmless, cheap and independent of the tunnel's own control channel. The loop below prints a timestamp, an HTTP status and the elapsed time once a second, and it fails loudly instead of hanging:

while true; do
  printf '%s ' "$(date +%H:%M:%S)"
  curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 2 https://example.com || echo "curl-exit-$?"
  sleep 1
done

Read the curl exit codes rather than only the status. A 28 means the request timed out, which is what a blocked path looks like. A 6 means name resolution failed, which is a different finding and often the more interesting one. A 200 with a small elapsed time during the gap means traffic was allowed out.

Write the output to a file and keep it. A terminal scrollback is not evidence, and the interesting part is usually three lines in the middle.

Trigger a real failure, not a tidy one

Disconnecting from the client's own menu tests the cooperative path, where the client has time to install its blocking rules. Kill the process instead, or drop the underlying link, and you test the path an actual failure takes.

Each trigger exercises a different code path. A server-side outage, a local link loss and a client crash can each leave different state behind, and one of them is likely to be the case you eventually encounter. Run the probe across at least two triggers and label the results by trigger.

Read the log for what unblocked traffic looks like

A 200 or a fast connection failure during the gap is a leak, whichever direction it points. Timeouts throughout the gap are the expected shape of a blocking policy. Name resolution that succeeds while connections fail suggests the resolver is still reachable, which is a leak of a different kind.

Existing sockets need care. A program can keep using a connection it opened before the disconnect, so apparently live traffic may not prove a new path was permitted. The socket list shows whether a connection predates the event:

ss -tunp | grep -E 'ESTAB|SYN-SENT'
ip route get 198.51.100.7
resolvectl status | sed -n '1,20p'

Compare the socket start time with the timestamp of the disconnect. A connection older than the outage is evidence of socket reuse, not of a leak, and reporting it as a failure sends the next person hunting for a bug that does not exist.

Record the result in a form that survives an update

Write down the trigger, the gap in seconds, whether any unblocked request succeeded, whether name resolution succeeded, and the client and operating system versions. That set of five facts can be compared across releases, which a paragraph of prose cannot.

Re-run the same test after a client update or a profile change, because blocking behaviour is implementation detail and it moves. The point of the record is to notice the move before it becomes an incident, not to certify the client once.

Аффингæй хæстæг статьялтæ