Watch
1
0
Fork
You've already forked souveraine
0

vpn toggle: status, not a bool

It read "On" for an hour through a dead tunnel — NM said activated while wg had
368 B received against 5.3 KiB sent. Now off/connecting/limited/online, decided
by a 2s probe of the far-side resolver rather than by the interface being up.
Sailfish models the same distinction (Online/Limited/Connecting); Android won't
let an unvalidated network win at all.
This commit is contained in:
Fimeg 2026-07-31 13:57:29 -04:00
commit 0745b48dc5

View file

@ -39,12 +39,54 @@ QuickToggleModel {
/// True while an up/down is in flight, so a double tap cannot race itself.
property bool busy: false
/// A host that only exists at the far end of the tunnel. Reaching it is the
/// only honest proof the tunnel carries traffic.
property string probeHost: "10.10.20.5"
/// "off" | "connecting" | "limited" | "online".
///
/// NOT a bool, and that is the point. On 2026-07-31 this toggle read "On"
/// for an hour while the tunnel was deaf: NetworkManager said `activated`,
/// `wg` said 368 B received against 5.3 KiB sent, and every service behind
/// it STT, TTS, gitea was unreachable. "The interface is up" and "the
/// tunnel carries traffic" are different claims and only one of them is
/// worth showing.
///
/// Sailfish models exactly this distinction (`MobileDataConnection.status`
/// is Online / Limited / Connecting, never a bool) and Android refuses to
/// let an unvalidated network win at all. `limited` is the state both have
/// and we did not.
///
/// The probe is deliberately NOT an internet check. This is a split tunnel
/// whose whole purpose is a private 10.10.0.0/16 that no public probe can
/// see; a link that fails a general-internet probe may be exactly the one
/// we want. The question is per-consumer "can this carry *this*" so we
/// ask the far end directly.
property string status: "off"
name: Translation.tr("Wiuf VPN")
icon: root.toggled ? "vpn_lock" : "vpn_key"
statusText: root.busy ? Translation.tr("…") : (root.toggled ? Translation.tr("On") : Translation.tr("Off"))
tooltipText: root.toggled
? Translation.tr("Wiuf VPN — 10.10.0.0/16 routed over the tunnel")
: Translation.tr("Wiuf VPN — off")
icon: root.status === "online" ? "vpn_lock"
: root.status === "limited" ? "vpn_lock_off"
: "vpn_key"
statusText: {
if (root.busy)
return Translation.tr("…");
switch (root.status) {
case "online": return Translation.tr("On");
case "limited": return Translation.tr("No route");
default: return Translation.tr("Off");
}
}
tooltipText: {
switch (root.status) {
case "online":
return Translation.tr("Wiuf VPN — 10.10.0.0/16 routed over the tunnel");
case "limited":
return Translation.tr("Wiuf VPN — connected but not carrying traffic. The tunnel is up and the far side is unreachable; check which link the handshake is leaving by.");
default:
return Translation.tr("Wiuf VPN — off");
}
}
mainAction: () => {
if (root.busy)
@ -86,16 +128,37 @@ QuickToggleModel {
// brought up or down from nmcli, from Settings, or by NM itself, and a
// shadow copy that disagrees with the owning subsystem is the failure
// doctrine §4 names ("never a hand-tracked bool").
// Two questions, asked together because the answer to the first does not
// imply the second: is the connection active, and does it carry traffic.
// A single `nmcli` reply can only answer the first, which is how this
// reported "On" through an hour of a dead tunnel.
//
// The probe is one UDP DNS round-trip to the far-side resolver with a 2 s
// deadline cheap enough for a 15 s poll, and it fails in exactly the case
// that matters (interface up, nothing crossing it). `getent` is not used:
// it would consult the whole resolver list and could be answered by a
// nameserver on another link, which is the same conflation this is here to
// end.
Process {
id: refreshProc
running: true
command: ["bash", "-c", `nmcli -t -f NAME connection show --active | grep -qx '${root.connectionName}' && echo up || echo down`]
command: ["bash", "-c",
`if ! nmcli -t -f NAME connection show --active | grep -qx '${root.connectionName}'; then echo off; exit 0; fi;`
+ ` if timeout 2 bash -c 'echo > /dev/udp/${root.probeHost}/53' 2>/dev/null`
+ ` && timeout 2 ping -c1 -W2 ${root.probeHost} >/dev/null 2>&1; then echo online; else echo limited; fi`]
stdout: StdioCollector {
id: stateCollector
onStreamFinished: {
if (stateCollector.text.length === 0)
const reply = stateCollector.text.trim();
if (reply.length === 0)
return;
root.toggled = stateCollector.text.trim() === "up";
root.status = reply;
// `toggled` stays the user-facing "is it switched on", so the
// button still lights while the tunnel is limited the state
// is reported in statusText rather than by silently un-toggling
// something the user turned on. Sailfish does the same: the
// switch stays checked and the description says "Limited".
root.toggled = (reply !== "off");
}
}
}