macOS: show the VPN pane only while a VPN is connected (closes #8)
check / check (push) Successful in 3m1s

A utun tunnel counted as the VPN whenever it was up with a routable
IPv4 address, so idle Tailscale or a tunnel left behind by a
disconnected client became the VPN pane and its probes failed. A
tunnel is now the VPN only while it carries the IPv4 default route in
netstat: an unscoped default row, or both 0/1 and 128.0/1 rows on it.
A default row scoped to a tunnel (flag I) does not count; on the
physical interface it still does, since a VPN holding the default
leaves the physical default scoped. Tests feed netstat text in the
macOS layout through the parser into Select. Not run on a real Mac.

Model: opus-5-5
This commit was merged in pull request #15.
This commit is contained in:
2026-10-03 15:26:39 +02:00
parent 66fb8bf149
commit 3578d18777
3 changed files with 248 additions and 66 deletions
+16 -11
View File
@@ -46,22 +46,27 @@ single pane.
setup, unchanged. When neither exists, the single default-route interface is
monitored instead.
**macOS.** The physical internet interface is found from the default route. When
a VPN client is running (Mullvad and similar clients create a `utun` tunnel that
carries a default route or holds a routable address), that tunnel is monitored
as the primary pane alongside the physical interface. With no VPN running, only
the physical interface is monitored. Interface names are detected on macOS; the
**macOS.** The physical internet interface is found from the default route. The
VPN pane appears only while a VPN is connected, meaning its `utun` tunnel
carries the IPv4 default route: in `netstat -rn -f inet` that is a `default` row
on the tunnel, or both a `0/1` and a `128.0/1` row on it, which some VPN clients
install instead of replacing the default. That tunnel is then monitored as the
primary pane alongside the physical interface. A tunnel that is up without the
default route is ignored, whatever its address: Tailscale without an exit node,
or a tunnel a disconnected client left behind. A default route scoped to the
tunnel alone (flag `I`) does not count either. With no VPN connected, only the
physical interface is monitored. Interface names are detected on macOS; the
`--ifaceA`/`--ifaceB` flags are not used there, but `--labelA`/`--labelB` still
set the pane labels.
### Supported matrix
| OS | Interfaces monitored |
| ----- | ----------------------------------------------------------- |
| Linux | `gu0` + `backhaul0` when both exist (two panes) |
| Linux | the single default-route interface otherwise (one pane) |
| macOS | VPN tunnel + physical default-route interface (two panes) |
| macOS | the physical default-route interface with no VPN (one pane) |
| OS | Interfaces monitored |
| ----- | --------------------------------------------------------------------- |
| Linux | `gu0` + `backhaul0` when both exist (two panes) |
| Linux | the single default-route interface otherwise (one pane) |
| macOS | connected VPN tunnel + physical default-route interface (two panes) |
| macOS | the physical default-route interface with no VPN connected (one pane) |
Anything outside this matrix — on Linux, only one of the named pair present, or
no/multiple default routes when neither is present; on macOS, no default route