440 lines
14 KiB
Bash
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
|