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
84 lines
3.1 KiB
Bash
Executable File
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
|