Files
vpndock/vpn-node/entrypoint.sh
Malin 06e24a3776 fix: pre-download pured daemon + start it before purevpn-cli
The purevpn-cli binary hardcodes a wrong S3 path for the daemon download
which returns HTTP 403. The correct URL (from the installer script) is:
  /cross-platform/linux-daemon/1.4.1/pured-linux-x64.gz

purevpn-cli is a client that talks to the pured daemon on :9485.
Pre-download the daemon during Docker build and start it in the entrypoint
before any purevpn-cli invocations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 18:55:50 +01:00

231 lines
10 KiB
Bash
Executable File

#!/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 <ctr> 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
# ── Start pured daemon ────────────────────────────────────────────────────────
log "Starting pured daemon …"
NODE_ENV=production /opt/purevpn-cli/pured-linux-x64 --start &
# Wait until daemon is listening on :9485 (up to 15s)
for i in $(seq 1 15); do
nc -z 127.0.0.1 9485 2>/dev/null && { log "pured listening on :9485"; break; }
sleep 1
done
nc -z 127.0.0.1 9485 2>/dev/null || log "WARNING: pured not listening after 15s — continuing anyway"
if [[ "$MANUAL_CONNECT" == "true" ]]; then
manual_mode
else
auto_mode
fi