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"
|
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
|
# Sunshine keeps credentials and logs beside its config; those must stay out of git
|
||||||
mkdir -p "$HOME/.config/sunshine"
|
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"
|
mkdir -p -m 700 "$HOME/.ssh"
|
||||||
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
|
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
|
||||||
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
|
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
|
||||||
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
|
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
step "Enabling user services"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable vpn-notify.service
|
||||||
|
|
||||||
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
|
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
|
||||||
step "Setting zsh as login shell"
|
step "Setting zsh as login shell"
|
||||||
chsh -s /usr/bin/zsh
|
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)
|
# 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
|
libgsf # Extensible I/O abstraction library for dealing with structured file formats
|
||||||
libmatroska # Matroska library
|
libmatroska # Matroska library
|
||||||
|
libnotify # Library for sending desktop notifications (notify-send, used by vpn-notify)
|
||||||
libpulse # A featureful, general-purpose sound server (client library)
|
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
|
# libva-nvidia-driver # VA-API implementation that uses NVDEC as a backend -- auto-detected by bootstrap.sh
|
||||||
linux # The Linux kernel and modules
|
linux # The Linux kernel and modules
|
||||||
|
|||||||
Reference in New Issue
Block a user