#!/usr/bin/bash # VPN monitor — connects OpenVPN when internet is up, disconnects when down. # Drops and reconnects when WiFi SSID changes (stale tunnel prevention). # On non-home networks, resolves VPN hostname via 8.8.8.8 and passes IP directly. # Keepalive: pings gateway through tunnel, two failures 10s apart = reconnect. # SIGTERM: gracefully stops tunnel and exits. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CONF="$SCRIPT_DIR/vpn.ovpn" VPN_HOST="vpn.hanson.xyz" VPN_PORT="1194" HOME_SSID="risa" VPN_GW="192.168.69.1" CHECK_HOST="1.1.1.1" INTERVAL=30 CONNECT_TIMEOUT=30 MAX_FAILURES=3 PREV_SSID="" FAIL_COUNT=0 ACTIVE_VPN_IP="" kill_vpn() { killall openvpn 2>/dev/null # Clean up host route to VPN server if [ -n "$ACTIVE_VPN_IP" ]; then ip route del "$ACTIVE_VPN_IP/32" 2>/dev/null ACTIVE_VPN_IP="" fi } get_default_gw() { ip route show default | awk '/via/ {print $3; exit}' } resolve_vpn() { if [ "$CURR_SSID" != "$HOME_SSID" ]; then dig +short @8.8.8.8 "$VPN_HOST" 2>/dev/null | tail -1 else dig +short "$VPN_HOST" 2>/dev/null | tail -1 fi } # Graceful shutdown on SIGTERM shutdown() { echo "$(date): SIGTERM received, stopping vpn and exiting" kill_vpn exit 0 } trap shutdown SIGTERM SIGINT # Kill other instances of this script and wait for graceful shutdown for pid in $(pgrep -f 'vpn-monitor.sh' | grep -v $$); do kill "$pid" 2>/dev/null done sleep 5 # Force kill any that didn't exit for pid in $(pgrep -f 'vpn-monitor.sh' | grep -v $$); do kill -9 "$pid" 2>/dev/null done # Kill any existing VPN and clean up kill_vpn sleep 1 while true; do CURR_SSID="$(iwgetid -r 2>/dev/null)" # Detect SSID change (only when switching between two known networks) if [ -n "$PREV_SSID" ] && [ -n "$CURR_SSID" ] && [ "$PREV_SSID" != "$CURR_SSID" ]; then echo "$(date): wifi changed from '$PREV_SSID' to '$CURR_SSID', dropping vpn" kill_vpn FAIL_COUNT=0 sleep 5 fi PREV_SSID="$CURR_SSID" if ping -c 1 -W 3 "$CHECK_HOST" > /dev/null 2>&1; then # Internet is up — check tunnel health if connected if ip link show tun0 > /dev/null 2>&1; then # Keepalive: ping gateway through tunnel, two failures 10s apart = dead if ! ping -c 1 -W 3 -I tun0 "$VPN_GW" > /dev/null 2>&1; then sleep 10 if ! ping -c 1 -W 3 -I tun0 "$VPN_GW" > /dev/null 2>&1; then echo "$(date): keepalive failed twice, dropping vpn" kill_vpn sleep 5 continue fi fi fi # Start VPN if not running if ! ip link show tun0 > /dev/null 2>&1; then if [ "$FAIL_COUNT" -ge "$MAX_FAILURES" ]; then # Back off after repeated failures — just wait for next interval sleep "$INTERVAL" continue fi # Resolve VPN server IP (via 8.8.8.8 on non-home networks) RESOLVED_IP="$(resolve_vpn)" if [ -z "$RESOLVED_IP" ]; then echo "$(date): failed to resolve $VPN_HOST (ssid=$CURR_SSID)" FAIL_COUNT=$((FAIL_COUNT + 1)) sleep "$INTERVAL" continue fi # Add host route to VPN server via current default gateway # so VPN traffic survives tun0 coming up GW="$(get_default_gw)" if [ -n "$GW" ]; then ip route replace "$RESOLVED_IP/32" via "$GW" echo "$(date): host route $RESOLVED_IP via $GW" fi ACTIVE_VPN_IP="$RESOLVED_IP" echo "$(date): starting openvpn -> $RESOLVED_IP (attempt $((FAIL_COUNT + 1))/$MAX_FAILURES, ssid=$CURR_SSID)" nice -n 19 openvpn --config "$CONF" --remote "$RESOLVED_IP" "$VPN_PORT" --daemon --log-append /tmp/openvpn.log # Wait for tunnel to come up CONNECTED=0 for i in $(seq 1 "$CONNECT_TIMEOUT"); do if ip link show tun0 > /dev/null 2>&1; then CONNECTED=1 break fi sleep 1 done if [ "$CONNECTED" -eq 1 ]; then echo "$(date): vpn connected (took ${i}s)" FAIL_COUNT=0 else echo "$(date): vpn failed to connect within ${CONNECT_TIMEOUT}s, killing" kill_vpn FAIL_COUNT=$((FAIL_COUNT + 1)) if [ "$FAIL_COUNT" -ge "$MAX_FAILURES" ]; then echo "$(date): $MAX_FAILURES consecutive failures, backing off" fi fi fi else # Internet is down — kill VPN if running if ip link show tun0 > /dev/null 2>&1; then echo "$(date): internet down, stopping openvpn" kill_vpn fi FAIL_COUNT=0 fi sleep "$INTERVAL" done