#!/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 # panama-vpn down # # `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 | down ' >&2; exit 1 ;; esac