#!/usr/bin/env bash # entrypoint.sh # Starts purevpn-cli (randomly selected location, rotates on reconnect) # then starts microsocks bound to 0.0.0.0 so HAProxy can reach it. set -euo pipefail export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/etc/pure-linux-cli/ SOCKS5_PORT="${SOCKS5_INNER_PORT:-1080}" VPN_IF="tun0" VPN_WAIT=60 # max seconds to wait for tun0 to appear # ── Logging ─────────────────────────────────────────────────────────────────── log() { echo "[$(date '+%H:%M:%S')] [$(hostname)] $*"; } die() { log "FATAL: $*" >&2; exit 1; } # ── Build location pool ─────────────────────────────────────────────────────── # Priority: env var PUREVPN_LOCATIONS (comma-separated) > servers.txt declare -a ALL_LOCATIONS if [[ -n "${PUREVPN_LOCATIONS:-}" ]]; then IFS=',' read -ra ALL_LOCATIONS <<< "$PUREVPN_LOCATIONS" # Trim whitespace from each element for i in "${!ALL_LOCATIONS[@]}"; do ALL_LOCATIONS[$i]="${ALL_LOCATIONS[$i]#"${ALL_LOCATIONS[$i]%%[![:space:]]*}"}" ALL_LOCATIONS[$i]="${ALL_LOCATIONS[$i]%"${ALL_LOCATIONS[$i]##*[![:space:]]}"}" done else mapfile -t ALL_LOCATIONS < <( grep -v '^\s*#' /etc/vpndock/servers.txt | grep -v '^\s*$' ) fi [[ ${#ALL_LOCATIONS[@]} -eq 0 ]] && die "No locations found. Set PUREVPN_LOCATIONS or populate servers.txt" [[ -z "${PUREVPN_USER:-}" ]] && die "PUREVPN_USER is not set" [[ -z "${PUREVPN_PASS:-}" ]] && die "PUREVPN_PASS is not set" log "Location pool (${#ALL_LOCATIONS[@]}): ${ALL_LOCATIONS[*]}" # ── Rotation state ──────────────────────────────────────────────────────────── declare -a USED_LOCATIONS=() CURRENT_LOCATION="" VPN_PID="" SOCKS_PID="" # Pick a random location that hasn't been used yet. # Resets the used list when all locations are exhausted. pick_location() { local available=() for loc in "${ALL_LOCATIONS[@]}"; do local already_used=false for used in "${USED_LOCATIONS[@]:-}"; do [[ "$loc" == "$used" ]] && already_used=true && break done $already_used || available+=("$loc") done if [[ ${#available[@]} -eq 0 ]]; then log "All locations tried — resetting rotation pool" USED_LOCATIONS=() available=("${ALL_LOCATIONS[@]}") fi local idx=$(( RANDOM % ${#available[@]} )) echo "${available[$idx]}" } # ── iptables: let HAProxy reach microsocks despite VPN kill-switch ──────────── whitelist_eth0() { local ip ip=$(ip -4 addr show eth0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || true) if [[ -n "$ip" ]]; then iptables -I INPUT -i eth0 -j ACCEPT 2>/dev/null || true iptables -I OUTPUT -o eth0 -j ACCEPT 2>/dev/null || true log "eth0 ($ip) whitelisted — HAProxy can reach microsocks" fi } # ── Non-interactive login via expect ───────────────────────────────────────── do_login() { log "Logging into PureVPN …" expect -c " set timeout 30 spawn purevpn-cli --login expect -re {[Ee]mail|[Uu]sername|[Ll]ogin} { send \"$PUREVPN_USER\r\" } expect -re {[Pp]assword} { send \"$PUREVPN_PASS\r\" } expect eof " && log "Login OK" || { # Fallback: pipe credentials (works on some CLI versions) log "expect login failed, trying stdin …" printf '%s\n%s\n' "$PUREVPN_USER" "$PUREVPN_PASS" | purevpn-cli --login || true } } # ── Disconnect any existing VPN session ────────────────────────────────────── vpn_disconnect() { purevpn-cli --disconnect 2>/dev/null || true [[ -n "$VPN_PID" ]] && kill "$VPN_PID" 2>/dev/null || true VPN_PID="" # Give the tunnel time to tear down sleep 3 } # ── Connect to a new (random, unused) location ─────────────────────────────── vpn_connect() { CURRENT_LOCATION="$(pick_location)" USED_LOCATIONS+=("$CURRENT_LOCATION") log "Connecting → '$CURRENT_LOCATION' (used so far: ${USED_LOCATIONS[*]})" purevpn-cli --connect "$CURRENT_LOCATION" & VPN_PID=$! } # ── Wait for tun0 to appear; return 1 on timeout ──────────────────────────── wait_for_tunnel() { local waited=0 while ! ip link show "$VPN_IF" &>/dev/null; do sleep 2; waited=$(( waited + 2 )) [[ $waited -ge $VPN_WAIT ]] && return 1 done return 0 } # ── Start / restart microsocks ──────────────────────────────────────────────── start_socks() { [[ -n "$SOCKS_PID" ]] && kill "$SOCKS_PID" 2>/dev/null || true log "Starting microsocks on 0.0.0.0:${SOCKS5_PORT}" microsocks -p "$SOCKS5_PORT" & SOCKS_PID=$! } # ── Connect with automatic fallback to next location on failure ─────────────── connect_with_fallback() { local attempts=0 local max_attempts=${#ALL_LOCATIONS[@]} while [[ $attempts -lt $max_attempts ]]; do vpn_connect if wait_for_tunnel; then local ext_ip ext_ip=$(curl -sf --max-time 8 https://api4.my-ip.io/ip || echo "unknown") log "VPN up — location: '$CURRENT_LOCATION' | exit IP: $ext_ip" return 0 fi log "'$CURRENT_LOCATION' timed out after ${VPN_WAIT}s — trying next location …" vpn_disconnect attempts=$(( attempts + 1 )) done die "Could not connect to any location after $attempts attempts" } # ═══════════════════════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════════════════════ whitelist_eth0 do_login connect_with_fallback start_socks # ── Reconnect helper (shared by monitor triggers) ──────────────────────────── do_reconnect() { local reason="$1" log "Reconnecting — reason: $reason (was '$CURRENT_LOCATION')" kill "$SOCKS_PID" 2>/dev/null || true SOCKS_PID="" vpn_disconnect connect_with_fallback start_socks } # ── Tunnel liveness: interface up AND traffic actually flows ────────────────── # Returns 0 if healthy, 1 if dead/stalled. tunnel_alive() { # 1. Interface must exist ip link show "$VPN_IF" &>/dev/null || return 1 # 2. purevpn-cli process must still be running [[ -n "$VPN_PID" ]] && kill -0 "$VPN_PID" 2>/dev/null || return 1 # 3. Traffic must actually flow through the tunnel (ghost-interface check). # Sends 2 pings via the VPN interface with a 5-second timeout each. ping -c 2 -W 5 -I "$VPN_IF" 1.1.1.1 &>/dev/null || return 1 return 0 } # ── Monitor loop ────────────────────────────────────────────────────────────── log "Entering monitor loop (checks every 15s) …" FAIL_COUNT=0 MAX_FAILS=2 # consecutive failed checks before reconnecting while true; do sleep 15 # ── VPN health check (interface + process + traffic) ───────────────────── if ! tunnel_alive; then FAIL_COUNT=$(( FAIL_COUNT + 1 )) log "Health check failed ($FAIL_COUNT/$MAX_FAILS) for '$CURRENT_LOCATION'" if [[ $FAIL_COUNT -ge $MAX_FAILS ]]; then FAIL_COUNT=0 do_reconnect "server drop / tunnel stalled" fi else FAIL_COUNT=0 # reset on any healthy check fi # ── microsocks process check ────────────────────────────────────────────── if ! kill -0 "$SOCKS_PID" 2>/dev/null; then log "microsocks died — restarting" start_socks fi done