139 lines
3.5 KiB
Bash
139 lines
3.5 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ---- config (defaults; overridable via /etc/keep-wireguard-connected.conf) ----
|
|
CONF_FILE="/etc/keep-wireguard-connected.conf"
|
|
|
|
# Set defaults FIRST (so set -u won't explode if config omits a var)
|
|
WG_IFACE_DEFAULT="va3"
|
|
EXPECTED_EGRESS_IP_DEFAULT="51.81.16.137"
|
|
VPN_PING_TARGET_DEFAULT="10.0.0.1"
|
|
|
|
TOTAL_WAIT_SEC_DEFAULT=90
|
|
SLEEP_STEP_SEC_DEFAULT=5
|
|
|
|
# Default IP echo endpoints
|
|
IP_ECHO_URLS_DEFAULT=(
|
|
"https://canhazip.com"
|
|
"https://api.ipify.org"
|
|
"https://ifconfig.me/ip"
|
|
"https://icanhazip.com"
|
|
)
|
|
|
|
# Default reboot cmd
|
|
REBOOT_CMD_DEFAULT=(/sbin/reboot -f)
|
|
|
|
# Load config overrides (optional)
|
|
if [[ -f "$CONF_FILE" ]]; then
|
|
# shellcheck disable=SC1090
|
|
source "$CONF_FILE"
|
|
fi
|
|
|
|
# Apply defaults if variables were not set by config/env
|
|
WG_IFACE="${WG_IFACE:-$WG_IFACE_DEFAULT}"
|
|
WG_SERVICE="wg-quick@${WG_IFACE}.service"
|
|
|
|
EXPECTED_EGRESS_IP="${EXPECTED_EGRESS_IP:-$EXPECTED_EGRESS_IP_DEFAULT}"
|
|
VPN_PING_TARGET="${VPN_PING_TARGET:-$VPN_PING_TARGET_DEFAULT}"
|
|
|
|
TOTAL_WAIT_SEC="${TOTAL_WAIT_SEC:-$TOTAL_WAIT_SEC_DEFAULT}"
|
|
SLEEP_STEP_SEC="${SLEEP_STEP_SEC:-$SLEEP_STEP_SEC_DEFAULT}"
|
|
|
|
# Allow config to override endpoints by defining IP_ECHO_URLS as an array
|
|
# If not defined, use defaults.
|
|
if ! declare -p IP_ECHO_URLS >/dev/null 2>&1; then
|
|
IP_ECHO_URLS=("${IP_ECHO_URLS_DEFAULT[@]}")
|
|
fi
|
|
|
|
# Allow config to override reboot command by defining REBOOT_CMD as an array
|
|
if ! declare -p REBOOT_CMD >/dev/null 2>&1; then
|
|
REBOOT_CMD=("${REBOOT_CMD_DEFAULT[@]}")
|
|
fi
|
|
|
|
|
|
# ---- helpers ----
|
|
log() { echo "[$(date -Is)] $*"; }
|
|
|
|
get_external_ip() {
|
|
local url ip
|
|
for url in "${IP_ECHO_URLS[@]}"; do
|
|
ip="$(curl -fsS --max-time 6 "$url" | tr -d '[:space:]' || true)"
|
|
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "$ip"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
vpn_ping_ok() {
|
|
[[ -z "${VPN_PING_TARGET}" ]] && return 0
|
|
ping -c 1 -W 2 "${VPN_PING_TARGET}" >/dev/null 2>&1
|
|
}
|
|
|
|
wg_handshake_ok() {
|
|
# Checks if there's at least one peer with a recent handshake
|
|
# (wg show gives unix timestamps; 0 means never)
|
|
local ts now
|
|
now="$(date +%s)"
|
|
while read -r ts; do
|
|
[[ "$ts" == "0" ]] && continue
|
|
# consider handshake "fresh" if within last 120s
|
|
if (( now - ts <= 120 )); then
|
|
return 0
|
|
fi
|
|
done < <(wg show "${WG_IFACE}" latest-handshakes 2>/dev/null | awk '{print $2}' || true)
|
|
|
|
return 1
|
|
}
|
|
|
|
egress_ok() {
|
|
# Primary: expected public egress IP
|
|
if [[ -n "${EXPECTED_EGRESS_IP}" ]]; then
|
|
local ip
|
|
ip="$(get_external_ip || true)"
|
|
[[ -z "$ip" ]] && return 2
|
|
[[ "$ip" == "${EXPECTED_EGRESS_IP}" ]]
|
|
return $?
|
|
fi
|
|
|
|
# Secondary: VPN-only ping + handshake
|
|
vpn_ping_ok && wg_handshake_ok
|
|
}
|
|
|
|
restart_wg() {
|
|
log "Restarting ${WG_SERVICE} ..."
|
|
systemctl restart "${WG_SERVICE}"
|
|
}
|
|
|
|
main() {
|
|
# Quick sanity: is wg binary present?
|
|
command -v wg >/dev/null 2>&1 || { log "ERROR: wg not found"; exit 2; }
|
|
|
|
if egress_ok; then
|
|
log "OK: WireGuard egress appears correct."
|
|
exit 0
|
|
fi
|
|
|
|
log "WARN: WireGuard egress NOT correct. Attempting restart."
|
|
restart_wg || log "WARN: systemctl restart returned non-zero"
|
|
|
|
local waited=0
|
|
while (( waited < TOTAL_WAIT_SEC )); do
|
|
sleep "${SLEEP_STEP_SEC}"
|
|
waited=$(( waited + SLEEP_STEP_SEC ))
|
|
|
|
if egress_ok; then
|
|
log "RECOVERED: WireGuard egress correct after ${waited}s."
|
|
exit 0
|
|
fi
|
|
|
|
log "Still not correct after ${waited}s..."
|
|
done
|
|
|
|
log "FAIL: WireGuard did not recover within ${TOTAL_WAIT_SEC}s. Forcing reboot."
|
|
"${REBOOT_CMD[@]}"
|
|
}
|
|
|
|
main "$@"
|