Files
Panama/config/dot/quickshell/scripts/panama-vpn
T
Gabriel Brown 36fdd4e076 Give the VPN a toggle, an indicator, and a way back
Turning on a WireGuard profile whose server was unreachable used to cost the
whole network stack, and the only way out was nmcli typed into a terminal.
Quickshell's Networking module has no VPN surface, so this arrives as the one
sanctioned nmcli exception: a helper that lists, raises and lowers profiles,
a service that watches NetworkManager for changes made anywhere, a quick
settings tile (left-click toggles the most recently used profile, right-click
picks among them), and a bar glyph while a tunnel is up.

The safety property is in the helper, where it cannot be skipped: activation
waits a bounded 25 seconds, and a failure is rolled back down and reported
instead of leaving a black-hole default route. The contract pins exactly that,
against a stateful stub NetworkManager.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 10:32:17 -04:00

84 lines
3.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# VPN connections, via NetworkManager.
#
# Quickshell.Networking covers Wi-Fi and wired devices but has no surface for
# VPN or WireGuard connections at all, so this is the one sanctioned exception
# to the no-nmcli rule stated in services/Connectivity.qml: there is nothing
# else to talk to. The moment Quickshell grows VPN support, this helper is what
# gets deleted.
#
# Usage:
# panama-vpn list -> {"connections":[{"name","uuid","kind","active","timestamp"}],"error":""}
# panama-vpn up <uuid>
# panama-vpn down <uuid>
#
# `up` is bounded and self-cleaning: a VPN whose server is unreachable is
# exactly the case this exists for, and NetworkManager's default is to keep
# trying for 90 seconds while every packet on a full-tunnel profile goes into
# the void. Waiting a bounded time and deactivating on failure is what makes
# the quick-settings toggle safe to press on a broken profile.
set -uo pipefail
emit_error() {
printf '{"connections":[],"error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
exit 0
}
command -v nmcli >/dev/null 2>&1 || emit_error 'nmcli is not available'
cmd_list() {
local rows active
# TIMESTAMP is when the connection last activated successfully; the toggle
# uses it to pick the profile the person most recently used.
rows="$(nmcli -t -f NAME,UUID,TYPE,TIMESTAMP connection show 2>/dev/null)" \
|| emit_error 'NetworkManager did not answer'
active="$(nmcli -t -f UUID connection show --active 2>/dev/null)" || active=""
# Fields are parsed from the end because NAME may contain escaped colons;
# UUID, TYPE and TIMESTAMP never do.
awk -F: -v active="$active" '
$(NF-1) == "wireguard" || $(NF-1) == "vpn" {
timestamp = $NF; kind = $(NF-1); uuid = $(NF-2);
name = $1;
for (i = 2; i <= NF-3; i++) name = name FS $i;
gsub(/\\:/, ":", name);
is_active = index(active, uuid) > 0 ? "true" : "false";
printf "%s\t%s\t%s\t%s\t%s\n", name, uuid, kind, is_active, timestamp;
}
' <<<"$rows" | jq -Rn '
{"connections": [inputs | split("\t")
| {name: .[0], uuid: .[1], kind: .[2],
active: (.[3] == "true"), timestamp: (.[4] | tonumber? // 0)}],
"error": ""}'
}
cmd_up() {
local uuid="$1" output
if ! output="$(nmcli -w 25 connection up uuid "$uuid" 2>&1)"; then
# Roll the half-activated connection back down so a dead server does
# not leave the machine with a black-hole default route.
nmcli connection down uuid "$uuid" >/dev/null 2>&1 || true
printf '%s\n' "$output" >&2
exit 1
fi
}
cmd_down() {
local uuid="$1" output
if ! output="$(nmcli connection down uuid "$uuid" 2>&1)"; then
printf '%s\n' "$output" >&2
exit 1
fi
}
case "${1:-}" in
list) cmd_list ;;
up) [[ -n "${2:-}" ]] || { echo 'panama-vpn up needs a connection uuid' >&2; exit 1; }
cmd_up "$2" ;;
down) [[ -n "${2:-}" ]] || { echo 'panama-vpn down needs a connection uuid' >&2; exit 1; }
cmd_down "$2" ;;
*) echo 'usage: panama-vpn list | up <uuid> | down <uuid>' >&2; exit 1 ;;
esac