From 28bcd7c1f2b439ebf4514e35225efdead5d7eeae Mon Sep 17 00:00:00 2001 From: zenaku Date: Sun, 30 Aug 2026 13:20:30 +0000 Subject: [PATCH] universal zero-downtime migration from legacy WireGuard/Go stack --- 02-migrate-peers.sh | 268 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 02-migrate-peers.sh diff --git a/02-migrate-peers.sh b/02-migrate-peers.sh new file mode 100644 index 0000000..37acec2 --- /dev/null +++ b/02-migrate-peers.sh @@ -0,0 +1,268 @@ +#!/bin/bash -eu +# ============================================================================= +# 02-migrate-peers.sh — Run as Linux user after 01 +# 1. Shuts down legacy WireGuard +# 2. Injects legacy private key into new wg0.conf +# 3. Starts new WireGuard +# 4. Verifies public key is identical +# 5. Migrates peers via API +# 6. Destroys private key +# Auto-rollback: restores legacy if anything fails +# ============================================================================= +if [ "$EUID" -eq 0 ]; then + echo "[X] Run as your Linux user, not root" + exit 1 +fi + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RESET='\033[0m' + +BACKUP_DIR="$HOME/.wg-migration" +LOG="$BACKUP_DIR/migration.log" +OPERATOR_DIR="$HOME/operator-docker" +WG_CONF="$OPERATOR_DIR/services/wireguard/config/wg_confs/wg0.conf" +WG_CONF_BAK="" + +ok() { echo -e "${GREEN}[OK]${RESET} $1"; echo "[OK] $1" >> "$LOG"; } +info() { echo -e "${CYAN}[i]${RESET} $1"; echo "[i] $1" >> "$LOG"; } +warn() { echo -e "${YELLOW}[!]${RESET} $1"; echo "[!] $1" >> "$LOG"; } +fail() { echo -e "${RED}[X]${RESET} $1"; echo "[X] $1" >> "$LOG"; exit 1; } +sep() { echo -e "\n${CYAN}------------------------------------${RESET}"; } + +rollback_02() { + echo "" + warn "Error detected — starting rollback..." + echo "[ROLLBACK-02] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + + [ -n "$WG_CONF_BAK" ] && [ -f "$WG_CONF_BAK" ] && \ + cp "$WG_CONF_BAK" "$WG_CONF" && warn "wg0.conf restored." + + cd "$OPERATOR_DIR" 2>/dev/null || true + docker compose stop wireguard 2>/dev/null || true + + LEGACY_COMPOSE_DIR=$(cat "$BACKUP_DIR/legacy_compose_dir" 2>/dev/null || true) + if [ -n "$LEGACY_COMPOSE_DIR" ] && [ -d "$LEGACY_COMPOSE_DIR" ]; then + sudo modprobe ip6table_filter 2>/dev/null || true + cd "$LEGACY_COMPOSE_DIR" + docker compose up -d 2>/dev/null || true + sleep 5 + LEGACY_UP=$(docker ps --format '{{.Names}}' | grep -i wireguard | head -1 || true) + [ -n "$LEGACY_UP" ] && ok "Legacy restored: $LEGACY_UP" || \ + warn "Legacy failed to start. Run manually: cd $LEGACY_COMPOSE_DIR && docker compose up -d" + fi + warn "Rollback complete. Check: cat $LOG" +} +trap rollback_02 ERR + +echo "" +echo "=========================================" +echo " 02 - Identity and peer migration" +echo " $(date '+%Y-%m-%d %H:%M:%S')" +echo "=========================================" +echo "[START-02] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + +# ─── PREREQUISITES ──────────────────────────────────────────────────────────── +sep +info "Checking prerequisites..." + +[ -f "$BACKUP_DIR/private_key" ] || fail "No private key backup found." +[ -f "$BACKUP_DIR/wg0-legacy.conf" ] || fail "No peer backup found." +[ -f "$BACKUP_DIR/public_key" ] || fail "No public key backup found." +[ -f "$BACKUP_DIR/legacy_compose_dir" ] || fail "No legacy snapshot found." +ok "Backups found." + +[ -d "$OPERATOR_DIR" ] || fail "operator-docker not found. Run 01 first." +[ -f "$WG_CONF" ] || fail "wg0.conf not found: $WG_CONF" +ok "operator-docker found." + +for CONTAINER in operator-api mysql; do + STATUS=$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo "false") + [ "$STATUS" = "true" ] || fail "Container '$CONTAINER' is not running." + ok "Running: $CONTAINER" +done + +# ─── LOAD CREDENTIALS ───────────────────────────────────────────────────────── +sep +info "Loading credentials..." +set -a; source "$OPERATOR_DIR/.env"; set +a + +[ -z "${OPERATOR_API_TOKEN:-}" ] && fail "OPERATOR_API_TOKEN not found in .env" +[ -z "${BASE_DOMAIN:-}" ] && fail "BASE_DOMAIN not found in .env" +[ -z "${API_SUBDOMAIN:-}" ] && fail "API_SUBDOMAIN not found in .env" + +echo "$BASE_DOMAIN" | grep -qE '[/;|&<> ]|^[0-9]+-' && \ + fail "Invalid BASE_DOMAIN in .env: '$BASE_DOMAIN'" + +API_URL="https://${API_SUBDOMAIN}.${BASE_DOMAIN}" +ok "API URL: $API_URL" + +# ─── STOP LEGACY WIREGUARD ──────────────────────────────────────────────────── +sep +info "Stopping legacy WireGuard..." +LEGACY_COMPOSE_DIR=$(cat "$BACKUP_DIR/legacy_compose_dir") +[ -d "$LEGACY_COMPOSE_DIR" ] || fail "Legacy directory not found: $LEGACY_COMPOSE_DIR" + +cd "$LEGACY_COMPOSE_DIR" +docker compose stop wireguard 2>/dev/null || true +sleep 3 +STILL_UP=$(docker compose ps --services --filter "status=running" 2>/dev/null | grep -i wireguard || true) +[ -n "$STILL_UP" ] && fail "Legacy WireGuard is still running." +ok "Legacy WireGuard stopped." + +# ─── BACKUP NEW CONF ────────────────────────────────────────────────────────── +sep +info "Backing up new wg0.conf..." +WG_CONF_BAK="$WG_CONF.bak.$(date +%s)" +cp "$WG_CONF" "$WG_CONF_BAK" +ok "Backup: $WG_CONF_BAK" + +# ─── INJECT PRIVATE KEY ─────────────────────────────────────────────────────── +sep +info "Injecting legacy private key..." +LEGACY_PRIVATE_KEY=$(cat "$BACKUP_DIR/private_key" | tr -d '[:space:]') +LEGACY_PUBLIC_KEY=$(cat "$BACKUP_DIR/public_key" | tr -d '[:space:]') +[ -z "$LEGACY_PRIVATE_KEY" ] && fail "Private key is empty." +[ -z "$LEGACY_PUBLIC_KEY" ] && fail "Public key is empty." + +sed -i "s|^PrivateKey = .*|PrivateKey = $LEGACY_PRIVATE_KEY|" "$WG_CONF" +ok "Private key written to wg0.conf." + +WG_SERVER_KEY="$OPERATOR_DIR/services/wireguard/config/server/privatekey-server" +WG_SERVER_PUB="$OPERATOR_DIR/services/wireguard/config/server/publickey-server" +mkdir -p "$(dirname "$WG_SERVER_KEY")" +echo "$LEGACY_PRIVATE_KEY" > "$WG_SERVER_KEY" +echo "$LEGACY_PUBLIC_KEY" > "$WG_SERVER_PUB" +ok "Private key written to server keyfile." + +# ─── START NEW WIREGUARD ────────────────────────────────────────────────────── +sep +info "Starting new WireGuard with legacy identity..." +echo "[WIREGUARD-START] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + +cd "$OPERATOR_DIR" +sudo modprobe ip6table_filter 2>/dev/null || true +docker compose up -d wireguard +info "Waiting for startup..." +sleep 8 + +STATUS=$(docker inspect -f '{{.State.Running}}' wireguard 2>/dev/null || echo "false") +[ "$STATUS" = "true" ] || fail "New WireGuard failed to start. Check: docker logs wireguard" +ok "New WireGuard running." + +# ─── VERIFY PUBLIC KEY ──────────────────────────────────────────────────────── +sep +info "Verifying server identity..." + +NEW_PUBLIC_KEY=$(docker exec wireguard cat /config/server/publickey-server 2>/dev/null | \ + tr -d '[:space:]' || true) +if [ -z "$NEW_PUBLIC_KEY" ]; then + NEW_PUBLIC_KEY=$(docker exec wireguard wg show wg0 2>/dev/null | \ + grep "public key" | awk '{print $NF}' | tr -d '[:space:]' || true) +fi +[ -z "$NEW_PUBLIC_KEY" ] && fail "Could not retrieve public key from new WireGuard." + +if [ "$NEW_PUBLIC_KEY" != "$LEGACY_PUBLIC_KEY" ]; then + warn "Public key MISMATCH!" + warn " Expected : $LEGACY_PUBLIC_KEY" + warn " Got : $NEW_PUBLIC_KEY" + fail "Server identity was not preserved." +fi + +ok "Public key verified: $NEW_PUBLIC_KEY" +ok "Existing users will reconnect automatically." + +# ─── VERIFY API ─────────────────────────────────────────────────────────────── +sep +info "Verifying API..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ + -H "Authorization: Bearer $OPERATOR_API_TOKEN" \ + "$API_URL/wireguard/peers" 2>/dev/null || echo "000") +[ "$HTTP_CODE" = "200" ] || fail "API not responding (HTTP $HTTP_CODE)." +ok "API responding." + +# ─── MIGRATE PEERS ──────────────────────────────────────────────────────────── +sep +info "Migrating peers to new API..." + +LEGACY_CONF=$(cat "$BACKUP_DIR/wg0-legacy.conf") +MIGRATED=0 +FAILED=0 +CURRENT_PUBKEY="" +CURRENT_IPS="" + +migrate_peer() { + local PUBKEY="$1" + local IPS="$2" + + IPS_JSON=$(echo "$IPS" | tr ',' '\n' | \ + sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | \ + awk 'NF {print "\"" $0 "\""}' | paste -sd ',' | \ + sed 's/^/[/' | sed 's/$/]/') + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 \ + -X POST "$API_URL/wireguard/peers" \ + -H "Authorization: Bearer $OPERATOR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"public_key\": \"$PUBKEY\", \"allowed_ips\": $IPS_JSON}" 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" = "200" ]; then + ok "Peer migrated: ${PUBKEY:0:20}... → $IPS" + MIGRATED=$((MIGRATED + 1)) + else + warn "Peer failed: ${PUBKEY:0:20}... (HTTP $HTTP_CODE)" + FAILED=$((FAILED + 1)) + fi +} + +while IFS= read -r line; do + if [[ "$line" == "[Peer]" ]]; then + if [ -n "$CURRENT_PUBKEY" ] && [ -n "$CURRENT_IPS" ]; then + migrate_peer "$CURRENT_PUBKEY" "$CURRENT_IPS" + fi + CURRENT_PUBKEY="" + CURRENT_IPS="" + elif [[ "$line" =~ ^PublicKey\ =\ (.+)$ ]]; then + CURRENT_PUBKEY="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^AllowedIPs\ =\ (.+)$ ]]; then + CURRENT_IPS="${BASH_REMATCH[1]}" + fi +done <<< "$LEGACY_CONF" + +[ -n "$CURRENT_PUBKEY" ] && [ -n "$CURRENT_IPS" ] && migrate_peer "$CURRENT_PUBKEY" "$CURRENT_IPS" + +# ─── VERIFY PEERS ───────────────────────────────────────────────────────────── +sep +info "Verifying peers in WireGuard..." +REGISTERED=$(docker exec wireguard wg show wg0 | grep -c "^ peer:" 2>/dev/null || echo 0) +ok "$REGISTERED peers active in wg0." + +# ─── DESTROY PRIVATE KEY ────────────────────────────────────────────────────── +sep +info "Destroying private key..." +command -v shred &>/dev/null && \ + shred -u "$BACKUP_DIR/private_key" 2>/dev/null || \ + rm -f "$BACKUP_DIR/private_key" +ok "Private key deleted from disk." + +trap - ERR +echo "[END-02] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + +echo "" +echo "=========================================" +echo " Migration complete" +echo "=========================================" +echo "" +echo " Public key : $NEW_PUBLIC_KEY" +echo " API : $API_URL" +echo " Peers migrated : $MIGRATED" +if [ "$FAILED" -gt 0 ]; then + echo -e " ${RED}Peers failed : $FAILED${RESET}" + echo " Check: cat $LOG" +else + echo " Errors : none" +fi +echo "" +echo " Existing users reconnect automatically" +echo " without new configuration." +echo "" +echo " Log: cat $LOG" +echo "========================================="