2026-03-11 09:45:42 +01:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# entrypoint.sh
|
2026-03-12 07:23:19 +01:00
|
|
|
#
|
|
|
|
|
# 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 <ctr> bash
|
|
|
|
|
# then: purevpn-cli --login
|
|
|
|
|
# purevpn-cli --connect "Germany"
|
|
|
|
|
#
|
2026-03-11 09:45:42 +01:00
|
|
|
set -euo pipefail
|
|
|
|
|
|
2026-03-11 10:11:01 +01:00
|
|
|
export PATH=/opt/purevpn-cli/bin:/opt/purevpn-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
2026-03-11 10:08:44 +01:00
|
|
|
|
2026-03-11 09:45:42 +01:00
|
|
|
SOCKS5_PORT="${SOCKS5_INNER_PORT:-1080}"
|
2026-03-12 07:23:19 +01:00
|
|
|
MANUAL_CONNECT="${MANUAL_CONNECT:-false}"
|
2026-03-11 09:45:42 +01:00
|
|
|
VPN_IF="tun0"
|
2026-03-12 07:23:19 +01:00
|
|
|
VPN_WAIT=60
|
2026-03-11 09:45:42 +01:00
|
|
|
|
|
|
|
|
log() { echo "[$(date '+%H:%M:%S')] [$(hostname)] $*"; }
|
|
|
|
|
die() { log "FATAL: $*" >&2; exit 1; }
|
|
|
|
|
|
|
|
|
|
SOCKS_PID=""
|
|
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ── iptables: let HAProxy reach danted regardless of VPN kill-switch ─────────
|
2026-03-11 09:45:42 +01:00
|
|
|
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
|
2026-03-11 09:58:44 +01:00
|
|
|
log "eth0 ($ip) whitelisted — HAProxy can reach danted"
|
2026-03-11 09:45:42 +01:00
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ── Start / restart danted ────────────────────────────────────────────────────
|
2026-03-11 09:45:42 +01:00
|
|
|
start_socks() {
|
|
|
|
|
[[ -n "$SOCKS_PID" ]] && kill "$SOCKS_PID" 2>/dev/null || true
|
2026-03-11 09:58:44 +01:00
|
|
|
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 &
|
2026-03-11 09:45:42 +01:00
|
|
|
SOCKS_PID=$!
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ── 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
|
|
|
|
|
}
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# ── MANUAL MODE ──────────────────────────────────────────────────────────────
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
manual_mode() {
|
|
|
|
|
log "═══ MANUAL CONNECT MODE ═══"
|
|
|
|
|
log "Connect to this container with:"
|
|
|
|
|
log " docker exec -it $(hostname) bash"
|
|
|
|
|
log "Then run:"
|
|
|
|
|
log " purevpn-cli --login"
|
|
|
|
|
log " purevpn-cli --connect \"Germany\" # or any location"
|
|
|
|
|
|
|
|
|
|
while true; do
|
|
|
|
|
log "Waiting for $VPN_IF to appear …"
|
|
|
|
|
# Wait indefinitely (3600s per loop iteration)
|
|
|
|
|
if wait_for_tunnel 3600; then
|
2026-03-11 09:45:42 +01:00
|
|
|
local ext_ip
|
|
|
|
|
ext_ip=$(curl -sf --max-time 8 https://api4.my-ip.io/ip || echo "unknown")
|
2026-03-12 07:23:19 +01:00
|
|
|
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"
|
2026-03-11 09:45:42 +01:00
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# ── 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[*]}"
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
declare -a USED_LOCATIONS=()
|
|
|
|
|
VPN_PID=""
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
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[@]} ))]}"
|
|
|
|
|
}
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ── Login ────────────────────────────────────────────────────────────────
|
|
|
|
|
log "Logging into PureVPN …"
|
|
|
|
|
expect -c "
|
|
|
|
|
set timeout 120
|
|
|
|
|
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" || {
|
|
|
|
|
log "expect login failed, trying stdin …"
|
|
|
|
|
printf '%s\n%s\n' "$PUREVPN_USER" "$PUREVPN_PASS" | purevpn-cli --login || true
|
|
|
|
|
}
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ── Connect loop (rotates through locations on failure/drop) ─────────────
|
|
|
|
|
while true; do
|
|
|
|
|
local loc
|
|
|
|
|
loc=$(pick_location)
|
|
|
|
|
USED_LOCATIONS+=("$loc")
|
|
|
|
|
log "Connecting → '$loc'"
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
purevpn-cli --connect "$loc" &
|
|
|
|
|
VPN_PID=$!
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
purevpn-cli --disconnect 2>/dev/null || true
|
|
|
|
|
sleep 3
|
|
|
|
|
fi
|
|
|
|
|
done
|
2026-03-11 09:45:42 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# Main
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
whitelist_eth0
|
2026-03-11 09:45:42 +01:00
|
|
|
|
2026-03-12 07:23:19 +01:00
|
|
|
if [[ "$MANUAL_CONNECT" == "true" ]]; then
|
|
|
|
|
manual_mode
|
|
|
|
|
else
|
|
|
|
|
auto_mode
|
|
|
|
|
fi
|