#!/usr/bin/env bash # entrypoint.sh # # Two modes (set via docker-compose env var): # # MANUAL_CONNECT=false (default) — auto login + connect via purevpn-cli # MANUAL_CONNECT=true — wait for tun0 to appear; user connects # manually with: docker exec -it bash # then: purevpn-cli --login # purevpn-cli --connect "Germany" # set -euo pipefail export PATH=/opt/purevpn-cli/bin:/opt/purevpn-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin SOCKS5_PORT="${SOCKS5_INNER_PORT:-1080}" MANUAL_CONNECT="${MANUAL_CONNECT:-false}" VPN_IF="tun0" VPN_WAIT=60 log() { echo "[$(date '+%H:%M:%S')] [$(hostname)] $*"; } die() { log "FATAL: $*" >&2; exit 1; } # purevpn-cli must run as non-root; it calls sudo internally for VPN setup. # vpnuser has home=/root so login tokens written here are found automatically. pvpn() { su -s /bin/bash -c "HOME=/root $(printf '%q ' "$@")" vpnuser; } SOCKS_PID="" # ── iptables: let HAProxy reach danted regardless of 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 danted" fi } # ── Start / restart danted ──────────────────────────────────────────────────── start_socks() { [[ -n "$SOCKS_PID" ]] && kill "$SOCKS_PID" 2>/dev/null || true cat > /etc/danted.conf << EOF logoutput: stderr user.privileged: root user.unprivileged: nobody internal: 0.0.0.0 port = ${SOCKS5_PORT} external: ${VPN_IF} socksmethod: none clientmethod: none client pass { from: 0.0.0.0/0 to: 0.0.0.0/0 log: error } socks pass { from: 0.0.0.0/0 to: 0.0.0.0/0 command: bind connect udpassociate log: error } EOF log "Starting danted on 0.0.0.0:${SOCKS5_PORT} → external: ${VPN_IF}" danted -f /etc/danted.conf & SOCKS_PID=$! } # ── Wait for tun0; return 1 on timeout ─────────────────────────────────────── wait_for_tunnel() { local waited=0 limit="${1:-$VPN_WAIT}" while ! ip link show "$VPN_IF" &>/dev/null; do sleep 2; waited=$(( waited + 2 )) [[ $waited -ge $limit ]] && return 1 done return 0 } # ── Monitor: watch tunnel + danted; reconnect on drop ──────────────────────── monitor_loop() { log "Monitor loop started (15s interval)" local fail=0 while true; do sleep 15 # Tunnel health if ! ip link show "$VPN_IF" &>/dev/null \ || ! ping -c 1 -W 5 -I "$VPN_IF" 1.1.1.1 &>/dev/null; then fail=$(( fail + 1 )) log "Tunnel check failed ($fail/2)" if [[ $fail -ge 2 ]]; then fail=0 log "Tunnel lost — stopping danted" kill "$SOCKS_PID" 2>/dev/null || true SOCKS_PID="" return 1 # caller decides what to do next fi else fail=0 fi # danted health if ! kill -0 "$SOCKS_PID" 2>/dev/null; then log "danted died — restarting" start_socks fi done } # ════════════════════════════════════════════════════════════════════════════════ # ── MANUAL MODE ────────────────────────────────────────────────────────────── # ════════════════════════════════════════════════════════════════════════════════ manual_mode() { log "═══ MANUAL CONNECT MODE ═══" log "Connect to this container with:" log " docker exec -it $(hostname) bash" log "Then run (as vpnuser or root with HOME=/root):" log " su -s /bin/bash -c 'HOME=/root purevpn-cli --login' vpnuser" log " su -s /bin/bash -c 'HOME=/root purevpn-cli --connect \"Germany\"' vpnuser" while true; do log "Waiting for $VPN_IF to appear …" # Wait indefinitely (3600s per loop iteration) if wait_for_tunnel 3600; then local ext_ip ext_ip=$(curl -sf --max-time 8 https://api4.my-ip.io/ip || echo "unknown") log "Tunnel UP — exit IP: $ext_ip" start_socks # Monitor until tunnel drops, then loop back and wait again monitor_loop || log "Tunnel dropped — waiting for reconnect" fi done } # ════════════════════════════════════════════════════════════════════════════════ # ── AUTO MODE ──────────────────────────────────────────────────────────────── # ════════════════════════════════════════════════════════════════════════════════ auto_mode() { # ── Location pool ──────────────────────────────────────────────────────── declare -a ALL_LOCATIONS if [[ -n "${PUREVPN_LOCATIONS:-}" ]]; then IFS=',' read -ra ALL_LOCATIONS <<< "$PUREVPN_LOCATIONS" 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" [[ -z "${PUREVPN_USER:-}" ]] && die "PUREVPN_USER not set" [[ -z "${PUREVPN_PASS:-}" ]] && die "PUREVPN_PASS not set" log "Location pool (${#ALL_LOCATIONS[@]}): ${ALL_LOCATIONS[*]}" declare -a USED_LOCATIONS=() VPN_PID="" pick_location() { local available=() for loc in "${ALL_LOCATIONS[@]}"; do local used=false for u in "${USED_LOCATIONS[@]:-}"; do [[ "$loc" == "$u" ]] && used=true && break; done $used || available+=("$loc") done if [[ ${#available[@]} -eq 0 ]]; then log "All locations used — resetting pool" USED_LOCATIONS=(); available=("${ALL_LOCATIONS[@]}") fi echo "${available[$(( RANDOM % ${#available[@]} ))]}" } # ── Login ──────────────────────────────────────────────────────────────── log "Logging into PureVPN …" expect -c " set timeout 120 spawn su -s /bin/bash -c {HOME=/root purevpn-cli --login} vpnuser 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" || { log "expect login failed, trying stdin …" printf '%s\n%s\n' "$PUREVPN_USER" "$PUREVPN_PASS" | pvpn purevpn-cli --login || true } # ── Connect loop (rotates through locations on failure/drop) ───────────── while true; do local loc loc=$(pick_location) USED_LOCATIONS+=("$loc") log "Connecting → '$loc'" pvpn purevpn-cli --connect "$loc" & VPN_PID=$! if wait_for_tunnel "$VPN_WAIT"; then local ext_ip ext_ip=$(curl -sf --max-time 8 https://api4.my-ip.io/ip || echo "unknown") log "VPN up — location: '$loc' | exit IP: $ext_ip" start_socks monitor_loop || true # Tunnel dropped — kill VPN process and try next location kill "$VPN_PID" 2>/dev/null || true pvpn purevpn-cli --disconnect 2>/dev/null || true sleep 3 log "Rotating to next location …" else log "'$loc' timed out — trying next location" kill "$VPN_PID" 2>/dev/null || true pvpn purevpn-cli --disconnect 2>/dev/null || true sleep 3 fi done } # ════════════════════════════════════════════════════════════════════════════════ # Main # ════════════════════════════════════════════════════════════════════════════════ whitelist_eth0 # pured daemon is NOT pre-started here intentionally. # purevpn-cli will call `systemctl is-active pured` → inactive → trigger # `sudo purevpn-cli --install-missing-components` which downloads the daemon # (via the patched correct URL), writes pured-linux-x64 (no ETXTBSY since # the daemon isn't running yet), then starts it via `systemctl start pured`. # Pre-starting the daemon caused ETXTBSY when the install tried to overwrite it. if [[ "$MANUAL_CONNECT" == "true" ]]; then manual_mode else auto_mode fi