Feature: Desktop notifications when a VPN connects or disconnects
Adds the vpn-notify script and its systemd user service, which notify on WireGuard/tun/Tailscale changes and reconnect NetworkManager WireGuard tunnels after a network switch. bootstrap.sh now enables the service and packages.txt lists libnotify explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -172,12 +172,18 @@ step "Linking dotfiles with stow"
|
||||
mkdir -p "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share/fonts" "$HOME/Pictures"
|
||||
# Sunshine keeps credentials and logs beside its config; those must stay out of git
|
||||
mkdir -p "$HOME/.config/sunshine"
|
||||
# systemd keeps its own wants/ symlinks beside the stowed user units
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
mkdir -p -m 700 "$HOME/.ssh"
|
||||
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
|
||||
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
|
||||
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
|
||||
fi
|
||||
|
||||
step "Enabling user services"
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable vpn-notify.service
|
||||
|
||||
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
|
||||
step "Setting zsh as login shell"
|
||||
chsh -s /usr/bin/zsh
|
||||
|
||||
12
dotfiles/.config/systemd/user/vpn-notify.service
Normal file
12
dotfiles/.config/systemd/user/vpn-notify.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Desktop notifications for VPN connect/disconnect
|
||||
PartOf=graphical-session.target
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
ExecStart=%h/.local/bin/vpn-notify
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
134
dotfiles/.local/bin/vpn-notify
Executable file
134
dotfiles/.local/bin/vpn-notify
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# Sends a desktop notification whenever a VPN connects or disconnects.
|
||||
# Covers WireGuard/tun interfaces (NetworkManager, wg-quick, Proton, OpenVPN)
|
||||
# and Tailscale (whose tailscale0 interface stays up even when stopped).
|
||||
#
|
||||
# NetworkManager WireGuard connections go stale when the underlying network
|
||||
# changes (wifi -> ethernet, wifi -> wifi), so when that happens any that were
|
||||
# up get restarted, checked, retried if needed, and reported as reconnected.
|
||||
|
||||
INTERVAL=2
|
||||
RETRIES=5
|
||||
|
||||
ICON_ON=/usr/share/icons/hicolor/scalable/apps/nm-vpn-active-lock-symbolic.svg
|
||||
ICON_OFF=/usr/share/icons/hicolor/scalable/apps/nm-vpn-standalone-lock-symbolic.svg
|
||||
|
||||
# Friendly name for an interface: NetworkManager connection name if it has one
|
||||
vpn_name() {
|
||||
local name
|
||||
name=$(nmcli -g GENERAL.CONNECTION device show "$1" 2>/dev/null)
|
||||
echo "${name:-$1}"
|
||||
}
|
||||
|
||||
active_vpns() {
|
||||
ip -d -o link show up 2>/dev/null \
|
||||
| awk -F': ' '/ (wireguard|tun) / { sub(/@.*/, "", $2); print $2 }' \
|
||||
| grep -vx tailscale0 \
|
||||
| while read -r iface; do vpn_name "$iface"; done
|
||||
|
||||
if command -v tailscale >/dev/null &&
|
||||
[[ $(tailscale status --json 2>/dev/null | jq -r .BackendState) == Running ]]; then
|
||||
echo Tailscale
|
||||
fi
|
||||
}
|
||||
|
||||
# Active NetworkManager connections, as UUID:TYPE:NAME
|
||||
nm_active() {
|
||||
nmcli -t -f UUID,TYPE,NAME connection show --active 2>/dev/null
|
||||
}
|
||||
|
||||
# UUIDs of the physical networks we're on (wifi, ethernet, tethering, ...)
|
||||
underlying_networks() {
|
||||
nm_active | awk -F: '$2 !~ /^(wireguard|tun|vpn|loopback|bridge)$/ { print $1 }' | sort
|
||||
}
|
||||
|
||||
# UUIDs of active NetworkManager WireGuard connections
|
||||
nm_wireguard() {
|
||||
nm_active | awk -F: '$2 == "wireguard" { print $1 }' | sort
|
||||
}
|
||||
|
||||
current_network_name() {
|
||||
nm_active | awk -F: '$2 !~ /^(wireguard|tun|vpn|loopback|bridge)$/ { sub(/^[^:]*:[^:]*:/, ""); print; exit }'
|
||||
}
|
||||
|
||||
notify() {
|
||||
notify-send -a "VPN" -i "$2" "${@:4}" "$1" "$3"
|
||||
}
|
||||
|
||||
# Confirm traffic flows through the tunnel by pinging its DNS server
|
||||
tunnel_works() {
|
||||
local uuid=$1 iface target
|
||||
iface=$(nmcli -g connection.interface-name connection show "$uuid" 2>/dev/null)
|
||||
target=$(nmcli -g ipv4.dns connection show "$uuid" 2>/dev/null | cut -d, -f1)
|
||||
[[ -z $target ]] && return 0
|
||||
ping -c 3 -W 2 -I "$iface" "$target" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
local uuid=$1 name attempt
|
||||
name=$(nmcli -g connection.id connection show "$uuid" 2>/dev/null)
|
||||
|
||||
for ((attempt = 1; attempt <= RETRIES; attempt++)); do
|
||||
nmcli connection down "$uuid" >/dev/null 2>&1
|
||||
if nmcli -w 20 connection up "$uuid" >/dev/null 2>&1 && sleep 2 && tunnel_works "$uuid"; then
|
||||
notify "🔄 VPN reconnected" "$ICON_ON" \
|
||||
"“$name” was reconnected after switching to “$(current_network_name)”."
|
||||
return 0
|
||||
fi
|
||||
sleep $((attempt * 3))
|
||||
done
|
||||
|
||||
notify "⚠️ VPN reconnect failed" "$ICON_OFF" \
|
||||
"Couldn't reconnect “$name” after $RETRIES attempts." -u critical
|
||||
return 1
|
||||
}
|
||||
|
||||
prev=$(active_vpns | sort)
|
||||
prev_net=$(underlying_networks)
|
||||
prev_wg=$(nm_wireguard)
|
||||
pending=""
|
||||
|
||||
while sleep "$INTERVAL"; do
|
||||
cur_net=$(underlying_networks)
|
||||
cur_wg=$(nm_wireguard)
|
||||
|
||||
# Network changed: queue WireGuard connections that were up for a restart
|
||||
if [[ $cur_net != "$prev_net" ]]; then
|
||||
pending=$(printf '%s\n%s\n' "$pending" "$prev_wg" | grep -v '^$' | sort -u)
|
||||
fi
|
||||
|
||||
# Drop any the user switched off themselves while waiting for a network
|
||||
if [[ -n $pending && $cur_net == "$prev_net" ]]; then
|
||||
switched_off=$(comm -23 <(echo "$prev_wg") <(echo "$cur_wg"))
|
||||
pending=$(comm -23 <(echo "$pending") <(echo "$switched_off") | grep -v '^$')
|
||||
fi
|
||||
|
||||
# Reconnect once we're on a network again (give DHCP/routes a moment)
|
||||
if [[ -n $pending && -n $cur_net ]]; then
|
||||
sleep 3
|
||||
while read -r uuid; do
|
||||
[[ -n $uuid ]] && reconnect "$uuid"
|
||||
done <<<"$pending"
|
||||
pending=""
|
||||
prev=$(active_vpns | sort)
|
||||
prev_net=$(underlying_networks)
|
||||
prev_wg=$(nm_wireguard)
|
||||
continue
|
||||
fi
|
||||
|
||||
prev_net=$cur_net
|
||||
prev_wg=$cur_wg
|
||||
|
||||
cur=$(active_vpns | sort)
|
||||
[[ $cur == "$prev" ]] && continue
|
||||
|
||||
while read -r vpn; do
|
||||
[[ -n $vpn ]] && notify "🔒 VPN connected" "$ICON_ON" "You are now connected to “$vpn”."
|
||||
done < <(comm -13 <(echo "$prev") <(echo "$cur"))
|
||||
|
||||
while read -r vpn; do
|
||||
[[ -n $vpn ]] && notify "🔓 VPN disconnected" "$ICON_OFF" "“$vpn” has been disconnected."
|
||||
done < <(comm -23 <(echo "$prev") <(echo "$cur"))
|
||||
|
||||
prev=$cur
|
||||
done
|
||||
@@ -57,6 +57,7 @@ jq # Command-line JSON processor
|
||||
# kitty # A modern, hackable, featureful, OpenGL-based terminal emulator !! you use foot as terminal (kitty config not in dotfiles)
|
||||
libgsf # Extensible I/O abstraction library for dealing with structured file formats
|
||||
libmatroska # Matroska library
|
||||
libnotify # Library for sending desktop notifications (notify-send, used by vpn-notify)
|
||||
libpulse # A featureful, general-purpose sound server (client library)
|
||||
# libva-nvidia-driver # VA-API implementation that uses NVDEC as a backend -- auto-detected by bootstrap.sh
|
||||
linux # The Linux kernel and modules
|
||||
|
||||
Reference in New Issue
Block a user