openvpn-endpoint-server/openvpn/entrypoint.sh

440 lines
14 KiB
Bash

#!/bin/bash
# =============================================================================
# mGuard VPN - Multi-Server OpenVPN Entrypoint
# =============================================================================
# This script manages multiple OpenVPN server instances dynamically.
# It polls the API for active servers and starts/stops them as needed.
# =============================================================================
set -e
# Configuration
API_URL="${API_URL:-http://127.0.0.1:8000/api/internal}"
API_TIMEOUT="${API_TIMEOUT:-120}"
API_RETRY_INTERVAL="${API_RETRY_INTERVAL:-5}"
POLL_INTERVAL="${POLL_INTERVAL:-30}"
# Directories
SERVERS_DIR="/etc/openvpn/servers"
SUPERVISOR_DIR="/etc/openvpn/supervisor.d"
LOG_DIR="/var/log/openvpn"
RUN_DIR="/var/run/openvpn"
# Ensure directories exist (volumes may override Dockerfile-created dirs)
mkdir -p "$SERVERS_DIR" "$SUPERVISOR_DIR" "$LOG_DIR" "$RUN_DIR"
# State tracking
RUNNING_SERVERS=""
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >&2
}
# =============================================================================
# Wait for API to be ready
# =============================================================================
wait_for_api() {
log "Waiting for API at $API_URL..."
local attempts=0
local max_attempts=$((API_TIMEOUT / API_RETRY_INTERVAL))
while [ $attempts -lt $max_attempts ]; do
if curl -sf "$API_URL/health" > /dev/null 2>&1; then
log "API is ready"
return 0
fi
attempts=$((attempts + 1))
log "API not ready, retrying in ${API_RETRY_INTERVAL}s... ($attempts/$max_attempts)"
sleep $API_RETRY_INTERVAL
done
log_error "API not available after ${API_TIMEOUT}s"
return 1
}
# =============================================================================
# Fetch active servers from API
# =============================================================================
fetch_active_servers() {
curl -sf "$API_URL/vpn-servers/active" 2>/dev/null || echo "[]"
}
# =============================================================================
# Fetch CCD files for a server
# =============================================================================
fetch_ccd_files() {
local server_id="$1"
local ccd_dir="$2"
log " Fetching CCD files for server $server_id..."
# Get all CCD files from API
local ccd_json
ccd_json=$(curl -sf "$API_URL/vpn-servers/$server_id/ccd" 2>/dev/null || echo '{"files":{}}')
# Parse and create CCD files
echo "$ccd_json" | jq -r '.files | to_entries[] | @base64' | while read -r entry; do
local cn=$(echo "$entry" | base64 -d | jq -r '.key')
local content=$(echo "$entry" | base64 -d | jq -r '.value')
if [ -n "$cn" ] && [ "$cn" != "null" ]; then
echo "$content" > "$ccd_dir/$cn"
log " Created CCD file: $cn"
fi
done
local count=$(echo "$ccd_json" | jq -r '.count // 0')
log " Fetched $count CCD files"
}
# =============================================================================
# Setup a single VPN server
# =============================================================================
setup_server() {
local server_id="$1"
local server_name="$2"
local port="$3"
local protocol="$4"
local mgmt_port="$5"
local server_dir="$SERVERS_DIR/$server_id"
log "Setting up server $server_id ($server_name) on port $port/$protocol"
# Create server directory
mkdir -p "$server_dir"
# Fetch all required files
log " Fetching configuration files..."
if ! curl -sf "$API_URL/vpn-servers/$server_id/config" > "$server_dir/server.conf"; then
log_error "Failed to fetch server config for $server_id"
return 1
fi
if ! curl -sf "$API_URL/vpn-servers/$server_id/ca" > "$server_dir/ca.crt"; then
log_error "Failed to fetch CA for $server_id"
return 1
fi
if ! curl -sf "$API_URL/vpn-servers/$server_id/cert" > "$server_dir/server.crt"; then
log_error "Failed to fetch server cert for $server_id"
return 1
fi
if ! curl -sf "$API_URL/vpn-servers/$server_id/key" > "$server_dir/server.key"; then
log_error "Failed to fetch server key for $server_id"
return 1
fi
chmod 600 "$server_dir/server.key"
if ! curl -sf "$API_URL/vpn-servers/$server_id/dh" > "$server_dir/dh.pem"; then
log_error "Failed to fetch DH params for $server_id"
return 1
fi
# TA key is optional
curl -sf "$API_URL/vpn-servers/$server_id/ta" > "$server_dir/ta.key" 2>/dev/null || true
if [ -f "$server_dir/ta.key" ] && [ -s "$server_dir/ta.key" ]; then
chmod 600 "$server_dir/ta.key"
else
rm -f "$server_dir/ta.key"
fi
# CRL
curl -sf "$API_URL/vpn-servers/$server_id/crl" > "$server_dir/crl.pem" 2>/dev/null || true
# Create client-config directory and fetch CCD files
mkdir -p "$server_dir/ccd"
fetch_ccd_files "$server_id" "$server_dir/ccd"
# Create status file location
touch "$RUN_DIR/openvpn-$server_id.status"
# Create supervisor config for this server
cat > "$SUPERVISOR_DIR/openvpn-$server_id.conf" << EOF
[program:openvpn-$server_id]
command=/usr/sbin/openvpn --config $server_dir/server.conf
directory=$server_dir
autostart=true
autorestart=true
startsecs=5
startretries=3
exitcodes=0
stopsignal=TERM
stopwaitsecs=10
stdout_logfile=$LOG_DIR/server-$server_id.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=3
stderr_logfile=$LOG_DIR/server-$server_id-error.log
stderr_logfile_maxbytes=5MB
stderr_logfile_backups=2
EOF
log " Server $server_id configured successfully"
return 0
}
# =============================================================================
# Remove a VPN server
# =============================================================================
remove_server() {
local server_id="$1"
log "Removing server $server_id..."
# Stop the process via supervisorctl if supervisor is running
if [ -S /var/run/supervisor.sock ]; then
supervisorctl stop "openvpn-$server_id" 2>/dev/null || true
supervisorctl remove "openvpn-$server_id" 2>/dev/null || true
fi
# Remove supervisor config
rm -f "$SUPERVISOR_DIR/openvpn-$server_id.conf"
# Keep server directory for logs, but mark as inactive
# rm -rf "$SERVERS_DIR/$server_id"
log " Server $server_id removed"
}
# =============================================================================
# Notify API that server started
# =============================================================================
notify_started() {
local server_id="$1"
curl -sf -X POST "$API_URL/vpn-servers/$server_id/started" > /dev/null 2>&1 || true
}
# =============================================================================
# Notify API that server stopped
# =============================================================================
notify_stopped() {
local server_id="$1"
curl -sf -X POST "$API_URL/vpn-servers/$server_id/stopped" > /dev/null 2>&1 || true
}
# =============================================================================
# Initial setup - configure all active servers
# =============================================================================
initial_setup() {
log "Performing initial server setup..."
local servers_json
servers_json=$(fetch_active_servers)
if [ "$servers_json" = "[]" ]; then
log "No active VPN servers found. Waiting for configuration via web UI..."
return 0
fi
# Parse JSON and setup each ready server
echo "$servers_json" | jq -c '.[] | select(.is_ready == true and .has_ca == true and .has_cert == true)' | while read -r server; do
local id=$(echo "$server" | jq -r '.id')
local name=$(echo "$server" | jq -r '.name')
local port=$(echo "$server" | jq -r '.port')
local protocol=$(echo "$server" | jq -r '.protocol')
local mgmt_port=$(echo "$server" | jq -r '.management_port')
if setup_server "$id" "$name" "$port" "$protocol" "$mgmt_port"; then
RUNNING_SERVERS="$RUNNING_SERVERS $id"
fi
done
log "Initial setup complete. Running servers:$RUNNING_SERVERS"
}
# =============================================================================
# Poll for changes and update servers
# =============================================================================
poll_for_changes() {
local servers_json
servers_json=$(fetch_active_servers)
# Get list of ready server IDs from API
local api_server_ids
api_server_ids=$(echo "$servers_json" | jq -r '.[] | select(.is_ready == true and .has_ca == true and .has_cert == true) | .id' | sort)
# Get list of currently configured servers
local current_server_ids
current_server_ids=$(ls -1 "$SUPERVISOR_DIR" 2>/dev/null | grep "^openvpn-" | sed 's/openvpn-\([0-9]*\)\.conf/\1/' | sort)
# Find new servers to add
for id in $api_server_ids; do
if ! echo "$current_server_ids" | grep -q "^${id}$"; then
log "New server detected: $id"
local server
server=$(echo "$servers_json" | jq -c ".[] | select(.id == $id)")
local name=$(echo "$server" | jq -r '.name')
local port=$(echo "$server" | jq -r '.port')
local protocol=$(echo "$server" | jq -r '.protocol')
local mgmt_port=$(echo "$server" | jq -r '.management_port')
if setup_server "$id" "$name" "$port" "$protocol" "$mgmt_port"; then
# Add to supervisor
if [ -S /var/run/supervisor.sock ]; then
supervisorctl reread > /dev/null 2>&1 || true
supervisorctl add "openvpn-$id" > /dev/null 2>&1 || true
fi
fi
fi
done
# Find servers to remove (no longer active)
for id in $current_server_ids; do
if ! echo "$api_server_ids" | grep -q "^${id}$"; then
log "Server $id no longer active"
remove_server "$id"
fi
done
# Update CRL and CCD files for all running servers
for id in $api_server_ids; do
if [ -d "$SERVERS_DIR/$id" ]; then
# Update CRL
curl -sf "$API_URL/vpn-servers/$id/crl" > "$SERVERS_DIR/$id/crl.pem.new" 2>/dev/null && \
mv "$SERVERS_DIR/$id/crl.pem.new" "$SERVERS_DIR/$id/crl.pem" || \
rm -f "$SERVERS_DIR/$id/crl.pem.new"
# Update CCD files (gateway routes)
fetch_ccd_files "$id" "$SERVERS_DIR/$id/ccd"
fi
done
}
# =============================================================================
# Setup iptables for NAT
# =============================================================================
setup_iptables() {
log "Setting up iptables NAT rules..."
# Enable IP forwarding (may fail in container, that's OK if host has it enabled)
if [ -w /proc/sys/net/ipv4/ip_forward ]; then
echo 1 > /proc/sys/net/ipv4/ip_forward
log " IP forwarding enabled"
else
log " IP forwarding: /proc/sys not writable (ensure host has net.ipv4.ip_forward=1)"
fi
# Basic NAT masquerade (adjust interface as needed)
if iptables -t nat -C POSTROUTING -s 10.0.0.0/8 -j MASQUERADE 2>/dev/null; then
log " NAT masquerade rule already exists"
elif iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -j MASQUERADE 2>/dev/null; then
log " NAT masquerade rule added"
else
log " WARNING: Could not add NAT masquerade rule"
fi
log "iptables setup complete"
}
# =============================================================================
# Cleanup on exit
# =============================================================================
cleanup() {
log "Shutting down..."
# Notify API about all servers stopping
for conf in "$SUPERVISOR_DIR"/openvpn-*.conf; do
if [ -f "$conf" ]; then
local id=$(basename "$conf" | sed 's/openvpn-\([0-9]*\)\.conf/\1/')
notify_stopped "$id"
fi
done
# Stop supervisor
if [ -S /var/run/supervisor.sock ]; then
supervisorctl shutdown 2>/dev/null || true
fi
exit 0
}
# =============================================================================
# Main
# =============================================================================
main() {
log "=== mGuard VPN Multi-Server Container Starting ==="
log "API URL: $API_URL"
log "Poll Interval: ${POLL_INTERVAL}s"
# Setup signal handlers
trap cleanup SIGTERM SIGINT
# Wait for API
if ! wait_for_api; then
log_error "Cannot start without API"
exit 1
fi
# Setup iptables
setup_iptables
# Initial setup of all servers
initial_setup
# Check if any servers were configured
if [ -z "$(ls -A $SUPERVISOR_DIR 2>/dev/null)" ]; then
log "No VPN servers configured yet."
log "Please create a CA and VPN server via the web UI."
log "Container will poll every ${POLL_INTERVAL}s for new servers..."
# Start supervisor anyway (will have no programs)
/usr/bin/supervisord -c /etc/supervisord.conf &
SUPERVISOR_PID=$!
# Polling loop waiting for first server
while true; do
sleep $POLL_INTERVAL
poll_for_changes
# Check if supervisor is still running
if ! kill -0 $SUPERVISOR_PID 2>/dev/null; then
log_error "Supervisor died unexpectedly"
exit 1
fi
done
else
log "Starting supervisord with configured servers..."
# Start supervisor in background
/usr/bin/supervisord -c /etc/supervisord.conf &
SUPERVISOR_PID=$!
# Wait a moment for supervisor to start
sleep 3
# Notify API about started servers
for conf in "$SUPERVISOR_DIR"/openvpn-*.conf; do
if [ -f "$conf" ]; then
local id=$(basename "$conf" | sed 's/openvpn-\([0-9]*\)\.conf/\1/')
# Check if actually running
if supervisorctl status "openvpn-$id" 2>/dev/null | grep -q RUNNING; then
notify_started "$id"
fi
fi
done
log "All servers started. Entering polling mode..."
# Polling loop for changes
while true; do
sleep $POLL_INTERVAL
poll_for_changes
# Check if supervisor is still running
if ! kill -0 $SUPERVISOR_PID 2>/dev/null; then
log_error "Supervisor died unexpectedly"
exit 1
fi
done
fi
}
# Run main
main