From 4f1fca52225269d22092a2cedbce19f768a0b765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Thu, 28 May 2026 23:28:34 +0300 Subject: [PATCH] feat: multi-server failover, health monitor, port hopping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - client/phobos-health.sh: router-side health monitor + auto-heal - Cron every 60s, checks WG handshake age - Auto-restart obfuscator (3min), WireGuard (5min) - Port hopping (10min), server failover (15min) - Auto-return to primary when recovered - client/failover.conf.example: multi-server config template - server/secondary-setup.sh: one-command secondary server deploy - WG + multi-port obfuscator + mini-API on :8444 - Auto-registers with main server - Panel: new "Серверы" page - Add/remove secondary servers - Health check, peer sync, fetch info - API key management - Secondary server install command Co-Authored-By: Claude Opus 4.6 --- app.py | 222 ++++++++++++++++++++++++++++ client/failover.conf.example | 15 ++ client/phobos-health.sh | 247 +++++++++++++++++++++++++++++++ server/secondary-setup.sh | 279 +++++++++++++++++++++++++++++++++++ 4 files changed, 763 insertions(+) create mode 100644 client/failover.conf.example create mode 100644 client/phobos-health.sh create mode 100644 server/secondary-setup.sh diff --git a/app.py b/app.py index fb237a1..1a37795 100644 --- a/app.py +++ b/app.py @@ -419,6 +419,7 @@ def sessions_page(): Сессии ({online_count}) Клиенты Метки + Серверы Настройки Выход @@ -555,6 +556,7 @@ def clients_page(): Сессии Клиенты Метки + Серверы Настройки Выход @@ -612,6 +614,7 @@ def labels_page(): Сессии Клиенты Метки + Серверы Настройки Выход @@ -725,6 +728,225 @@ def _render_install_commands(): return lines +SERVERS_FILE = f"{PANEL_DIR}/servers.json" + + +def load_servers(): + if os.path.exists(SERVERS_FILE): + with open(SERVERS_FILE) as f: + return json.load(f) + return [] + + +def save_servers(servers): + with open(SERVERS_FILE, "w") as f: + json.dump(servers, f, indent=2) + + +def check_server_health(server): + """Check secondary server health via API.""" + try: + import urllib.request + url = f"http://{server['ip']}:8444/api/health" + req = urllib.request.Request(url, headers={"X-API-Key": server.get("api_key", "")}) + resp = urllib.request.urlopen(req, timeout=5) + return json.loads(resp.read()) + except Exception as e: + return {"status": "error", "error": str(e)} + + +def sync_peer_to_server(server, public_key, allowed_ips, action="add"): + """Add or remove peer on secondary server.""" + try: + import urllib.request + url = f"http://{server['ip']}:8444/api/peers/{action}" + data = json.dumps({"public_key": public_key, "allowed_ips": allowed_ips}).encode() + req = urllib.request.Request(url, data=data, headers={ + "Content-Type": "application/json", + "X-API-Key": server.get("api_key", "") + }) + resp = urllib.request.urlopen(req, timeout=10) + return json.loads(resp.read()) + except Exception: + return {"status": "error"} + + +def sync_peer_to_all_servers(public_key, allowed_ips, action="add"): + """Sync peer to all secondary servers.""" + servers = load_servers() + for srv in servers: + if srv.get("enabled", True): + sync_peer_to_server(srv, public_key, allowed_ips, action) + + +@app.route("/api/servers/register", methods=["POST"]) +def api_register_server(): + s = load_settings() + api_key = request.headers.get("X-API-Key", "") + if api_key != s.get("server_api_key", ""): + return json.dumps({"error": "unauthorized"}), 401, {"Content-Type": "application/json"} + + data = request.json + servers = load_servers() + ip = data.get("ip", "") + + for srv in servers: + if srv["ip"] == ip: + srv.update(data) + srv["last_seen"] = datetime.now().isoformat() + save_servers(servers) + return json.dumps({"status": "updated"}), 200, {"Content-Type": "application/json"} + + data["last_seen"] = datetime.now().isoformat() + data["enabled"] = True + data["api_key"] = api_key + servers.append(data) + save_servers(servers) + return json.dumps({"status": "registered"}), 200, {"Content-Type": "application/json"} + + +@app.route("/servers", methods=["GET", "POST"]) +@auth_required +def servers_page(): + s = load_settings() + servers = load_servers() + msg = "" + + if request.method == "POST": + action = request.form.get("action") + if action == "add": + ip = request.form.get("ip", "").strip() + api_key = request.form.get("api_key", "").strip() + if ip: + servers.append({ + "ip": ip, + "api_key": api_key, + "enabled": True, + "last_seen": "", + "wg_public_key": "", + "obfuscator_key": "", + "ports": "" + }) + save_servers(servers) + msg = f'
Сервер {ip} добавлен
' + + elif action == "remove": + ip = request.form.get("ip", "").strip() + servers = [s for s in servers if s.get("ip") != ip] + save_servers(servers) + msg = f'
Сервер удалён
' + + elif action == "sync": + ip = request.form.get("ip", "").strip() + srv = next((s for s in servers if s["ip"] == ip), None) + if srv: + clients = get_clients() + synced = 0 + for c in clients: + pub = c.get("public_key", "") + tip = c.get("tunnel_ip_v4", "") + if pub and tip: + res = sync_peer_to_server(srv, pub, f"{tip}/32", "add") + if res.get("status") == "ok": + synced += 1 + msg = f'
Синхронизировано {synced} клиентов на {ip}
' + + elif action == "fetch_info": + ip = request.form.get("ip", "").strip() + for srv in servers: + if srv["ip"] == ip: + try: + import urllib.request + url = f"http://{ip}:8444/api/info" + req = urllib.request.Request(url, headers={"X-API-Key": srv.get("api_key", "")}) + resp = urllib.request.urlopen(req, timeout=5) + info = json.loads(resp.read()) + srv["wg_public_key"] = info.get("wg_public_key", "") + srv["obfuscator_key"] = info.get("obfuscator_key", "") + srv["ports"] = ",".join(info.get("ports", [])) + save_servers(servers) + msg = f'
Инфо получено от {ip}
' + except Exception as e: + msg = f'
Ошибка: {e}
' + + elif action == "set_api_key": + new_key = request.form.get("server_api_key", "").strip() + if new_key: + s["server_api_key"] = new_key + save_settings(s) + msg = '
API ключ обновлён
' + + # Build server rows + rows = "" + for srv in servers: + ip = srv.get("ip", "") + health = check_server_health(srv) + status_class = "badge-on" if health.get("status") == "ok" else "badge-off" + status_text = "Online" if health.get("status") == "ok" else "Offline" + peers = health.get("peers", "?") + ports = srv.get("ports", "?") + + rows += f""" + {ip} + {ports} + {peers} + {status_text} + +
+ + + + +
+ + """ + + api_key = s.get("server_api_key", "") + + html = f""" +
+ + {msg} +
+

API ключ для secondary серверов

+
+ + + +
+
+ +
+

Серверы VPN

+ + + {rows} +
IPПортыPeersСтатус
+
+ + + + +
+
+ +
+

Установка secondary сервера

+

На новом VPS выполнить:

+ MAIN_SERVER={SERVER_IP} MAIN_API_KEY={api_key} bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/secondary-setup.sh) +
+
""" + return render(html) + + def _load_server_env(): """Load server.env as dict for subprocess env.""" env = {} diff --git a/client/failover.conf.example b/client/failover.conf.example new file mode 100644 index 0000000..0d2dc03 --- /dev/null +++ b/client/failover.conf.example @@ -0,0 +1,15 @@ +# Phobos Failover Configuration +# Format: SERVER_N=host:port1,port2,port3 +# KEY_N=obfuscation_key_for_server_N + +# Primary server (main, with web panel) +SERVER_1=144.124.252.104:51821,51822,51823 +KEY_1=Fc3X8GbVQD2FuHPbOjKiaP83nWSwNojJ + +# Secondary server (backup, VPN only) +# SERVER_2=backup.example.com:51821,51822 +# KEY_2=backup_obfuscation_key_here + +# Emergency server (last resort) +# SERVER_3=emergency.example.com:43210 +# KEY_3=emergency_key_here diff --git a/client/phobos-health.sh b/client/phobos-health.sh new file mode 100644 index 0000000..414bb11 --- /dev/null +++ b/client/phobos-health.sh @@ -0,0 +1,247 @@ +#!/bin/sh +# ============================================================ +# Phobos Health Monitor — Auto-heal + Failover for routers +# Runs via cron every 60 seconds on Keenetic/Entware +# +# Install: crontab -e → */1 * * * * /opt/etc/Phobos/phobos-health.sh +# ============================================================ + +PHOBOS_DIR="/opt/etc/Phobos" +CONF="$PHOBOS_DIR/failover.conf" +STATE="$PHOBOS_DIR/state" +LOG="$PHOBOS_DIR/health.log" +WG_CONF="$PHOBOS_DIR/wg0.conf" +OBF_CONF="$PHOBOS_DIR/wg-obfuscator.conf" +LOCKFILE="/tmp/phobos-health.lock" + +MAX_LOG_LINES=200 +HANDSHAKE_WARN=180 +HANDSHAKE_RESTART_OBF=300 +HANDSHAKE_RESTART_WG=600 +HANDSHAKE_FAILOVER=900 + +log() { + echo "$(date '+%H:%M:%S') $1" >> "$LOG" + # Trim log + if [ "$(wc -l < "$LOG" 2>/dev/null)" -gt "$MAX_LOG_LINES" ]; then + tail -n 100 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG" + fi +} + +# Prevent concurrent runs +if [ -f "$LOCKFILE" ]; then + pid=$(cat "$LOCKFILE" 2>/dev/null) + if kill -0 "$pid" 2>/dev/null; then + exit 0 + fi +fi +echo $$ > "$LOCKFILE" +trap 'rm -f "$LOCKFILE"' EXIT + +# Create state dir +mkdir -p "$STATE" + +# ── Read failover config ── +if [ ! -f "$CONF" ]; then + log "ERROR: no failover.conf" + exit 1 +fi + +# Parse servers: SERVER_1=ip:port1,port2,port3 +# Parse current state +CURRENT_SERVER=$(cat "$STATE/current_server" 2>/dev/null || echo "1") +CURRENT_PORT_IDX=$(cat "$STATE/current_port_idx" 2>/dev/null || echo "0") +RESTART_COUNT=$(cat "$STATE/restart_count" 2>/dev/null || echo "0") +PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0") + +# Load server list +SERVER_COUNT=0 +idx=1 +while true; do + val=$(grep "^SERVER_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-) + if [ -z "$val" ]; then + break + fi + eval "SERVER_${idx}_HOST=$(echo "$val" | cut -d: -f1)" + eval "SERVER_${idx}_PORTS=$(echo "$val" | cut -d: -f2-)" + eval "SERVER_${idx}_KEY=$(grep "^KEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)" + SERVER_COUNT=$idx + idx=$((idx + 1)) +done + +if [ "$SERVER_COUNT" -eq 0 ]; then + log "ERROR: no servers in failover.conf" + exit 1 +fi + +# ── Get current handshake age ── +get_handshake_age() { + hs=$(wg show wg0 latest-handshakes 2>/dev/null | awk '{print $2}') + if [ -z "$hs" ] || [ "$hs" = "0" ]; then + echo "9999" + return + fi + now=$(date +%s) + age=$((now - hs)) + echo "$age" +} + +# ── Get port by index from comma-separated list ── +get_port() { + server_idx=$1 + port_idx=$2 + eval "ports=\$SERVER_${server_idx}_PORTS" + echo "$ports" | tr ',' '\n' | sed -n "$((port_idx + 1))p" +} + +get_port_count() { + server_idx=$1 + eval "ports=\$SERVER_${server_idx}_PORTS" + echo "$ports" | tr ',' '\n' | wc -l +} + +# ── Switch to specific server:port ── +switch_endpoint() { + server_idx=$1 + port_idx=$2 + + eval "host=\$SERVER_${server_idx}_HOST" + eval "obf_key=\$SERVER_${server_idx}_KEY" + port=$(get_port "$server_idx" "$port_idx") + + if [ -z "$host" ] || [ -z "$port" ]; then + log "ERROR: invalid server $server_idx port_idx $port_idx" + return 1 + fi + + log "SWITCH → server $server_idx ($host:$port)" + + # Update obfuscator config target + if [ -f "$OBF_CONF" ]; then + sed -i "s|^target = .*|target = ${host}:${port}|" "$OBF_CONF" + if [ -n "$obf_key" ]; then + sed -i "s|^key = .*|key = ${obf_key}|" "$OBF_CONF" + fi + fi + + # Restart obfuscator + killall wg-obfuscator 2>/dev/null + sleep 1 + wg-obfuscator --config "$OBF_CONF" & + + # Save state + echo "$server_idx" > "$STATE/current_server" + echo "$port_idx" > "$STATE/current_port_idx" + echo "0" > "$STATE/restart_count" +} + +# ── Try next port on current server ── +try_next_port() { + port_count=$(get_port_count "$CURRENT_SERVER") + next_idx=$(( (CURRENT_PORT_IDX + 1) % port_count )) + + if [ "$next_idx" -eq 0 ]; then + return 1 + fi + + log "PORT HOP → port idx $next_idx on server $CURRENT_SERVER" + switch_endpoint "$CURRENT_SERVER" "$next_idx" + return 0 +} + +# ── Try next server ── +try_next_server() { + next=$((CURRENT_SERVER + 1)) + if [ "$next" -gt "$SERVER_COUNT" ]; then + next=1 + fi + + if [ "$next" -eq "$CURRENT_SERVER" ]; then + log "WARN: only one server, can't failover" + return 1 + fi + + log "FAILOVER → server $next" + switch_endpoint "$next" "0" + return 0 +} + +# ── Check if primary is back (every 5 min when on secondary) ── +check_primary() { + if [ "$CURRENT_SERVER" -eq 1 ]; then + return + fi + + now=$(date +%s) + elapsed=$((now - PRIMARY_CHECK_TS)) + if [ "$elapsed" -lt 300 ]; then + return + fi + echo "$now" > "$STATE/primary_check_ts" + + eval "host=\$SERVER_1_HOST" + port=$(get_port 1 0) + + # Quick UDP probe — send a byte, see if port responds + if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then + log "PRIMARY alive, switching back" + switch_endpoint 1 0 + fi +} + +# ── Main logic ── +AGE=$(get_handshake_age) + +if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then + # All good, reset counters + if [ "$RESTART_COUNT" -gt 0 ]; then + log "OK: connection restored (age=${AGE}s)" + echo "0" > "$STATE/restart_count" + fi + check_primary + exit 0 +fi + +log "STALE handshake: ${AGE}s (server=$CURRENT_SERVER port_idx=$CURRENT_PORT_IDX restarts=$RESTART_COUNT)" + +if [ "$AGE" -lt "$HANDSHAKE_RESTART_OBF" ]; then + # Restart obfuscator only + RESTART_COUNT=$((RESTART_COUNT + 1)) + echo "$RESTART_COUNT" > "$STATE/restart_count" + log "ACTION: restart obfuscator (attempt $RESTART_COUNT)" + killall wg-obfuscator 2>/dev/null + sleep 1 + wg-obfuscator --config "$OBF_CONF" & + exit 0 +fi + +if [ "$AGE" -lt "$HANDSHAKE_RESTART_WG" ]; then + # Restart WireGuard + obfuscator + RESTART_COUNT=$((RESTART_COUNT + 1)) + echo "$RESTART_COUNT" > "$STATE/restart_count" + log "ACTION: restart WG + obfuscator (attempt $RESTART_COUNT)" + wg-quick down wg0 2>/dev/null + killall wg-obfuscator 2>/dev/null + sleep 2 + wg-quick up wg0 2>/dev/null + sleep 1 + wg-obfuscator --config "$OBF_CONF" & + exit 0 +fi + +if [ "$AGE" -lt "$HANDSHAKE_FAILOVER" ]; then + # Try next port + log "ACTION: try port hop" + if ! try_next_port; then + log "ACTION: all ports exhausted, try next server" + try_next_server + fi + exit 0 +fi + +# Full failover +log "ACTION: failover (handshake ${AGE}s)" +if ! try_next_server; then + # Only one server, cycle ports + try_next_port +fi diff --git a/server/secondary-setup.sh b/server/secondary-setup.sh new file mode 100644 index 0000000..ecbf64b --- /dev/null +++ b/server/secondary-setup.sh @@ -0,0 +1,279 @@ +#!/bin/bash +# ============================================================ +# Phobos Secondary Server Setup +# Deploys WG + obfuscator + mini-API (no web panel) +# +# Usage: +# MAIN_SERVER=144.124.252.104 MAIN_API_KEY=secret123 \ +# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/secondary-setup.sh) +# ============================================================ + +set -e + +MAIN_SERVER="${MAIN_SERVER:?Set MAIN_SERVER=ip_of_main_server}" +MAIN_API_KEY="${MAIN_API_KEY:?Set MAIN_API_KEY=your_api_key}" +OBF_PORTS="${OBF_PORTS:-51821,51822,51823}" +PHOBOS_DIR="/opt/Phobos" + +SERVER_IP=$(curl -s https://api.ipify.org || hostname -I | awk '{print $1}') +IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i .git/info/sparse-checkout +git pull origin main -q 2>/dev/null +cp -f wg-obfuscator/bin/wg-obfuscator-$(uname -m) "$PHOBOS_DIR/bin/" 2>/dev/null || true +chmod +x "$PHOBOS_DIR/bin/"wg-obfuscator-* +ln -sf "$PHOBOS_DIR/bin/wg-obfuscator-$(uname -m)" /usr/local/bin/wg-obfuscator +rm -rf "$REPO_DIR" +echo " Obfuscator installed." + +# ── 3. WireGuard ── +echo "[3/6] Configuring WireGuard..." +WG_PRIV=$(wg genkey) +WG_PUB=$(echo "$WG_PRIV" | wg pubkey) + +cat > /etc/wireguard/wg0.conf << WGEOF +[Interface] +Address = 10.25.0.1/16 +ListenPort = 51820 +PrivateKey = $WG_PRIV +PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE +PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE +WGEOF + +sysctl -w net.ipv4.ip_forward=1 -q +grep -q "net.ipv4.ip_forward = 1" /etc/sysctl.conf || echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf + +systemctl enable wg-quick@wg0 -q +systemctl restart wg-quick@wg0 +echo " WireGuard running." + +# ── 4. Obfuscator (multi-port) ── +echo "[4/6] Setting up obfuscator..." +OBF_KEY=$(head -c 32 /dev/urandom | base64 | tr -d "/+=" | head -c 32) + +# Block direct WG access +iptables -C INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP 2>/dev/null \ + || iptables -A INPUT -p udp --dport 51820 ! -s 127.0.0.1 -j DROP + +IFS=',' read -ra PORTS <<< "$OBF_PORTS" +for PORT in "${PORTS[@]}"; do + cat > "$PHOBOS_DIR/server/wg-obfuscator-${PORT}.conf" << EOF +[instance] +source-if = 0.0.0.0 +source-lport = ${PORT} +target = 127.0.0.1:51820 +key = ${OBF_KEY} +masking = AUTO +verbose = INFO +idle-timeout = 300 +max-dummy = 50 +EOF + + cat > /etc/systemd/system/wg-obfuscator-${PORT}.service << EOF +[Unit] +Description=WireGuard Obfuscator (port ${PORT}) +After=network.target wg-quick@wg0.service + +[Service] +Type=simple +ExecStart=/usr/local/bin/wg-obfuscator --config ${PHOBOS_DIR}/server/wg-obfuscator-${PORT}.conf +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +done + +systemctl daemon-reload +for PORT in "${PORTS[@]}"; do + systemctl enable "wg-obfuscator-${PORT}" -q + systemctl start "wg-obfuscator-${PORT}" +done +echo " Obfuscator running on ports: $OBF_PORTS" + +# ── 5. Save config ── +echo "[5/6] Saving config..." +cat > "$PHOBOS_DIR/server/server.env" << EOF +SERVER_WG_PRIVATE_KEY=$WG_PRIV +SERVER_WG_PUBLIC_KEY=$WG_PUB +SERVER_PUBLIC_IP_V4=$SERVER_IP +OBFUSCATOR_KEY=$OBF_KEY +OBFUSCATOR_PORTS=$OBF_PORTS +MAIN_SERVER=$MAIN_SERVER +MAIN_API_KEY=$MAIN_API_KEY +ROLE=secondary +EOF + +# ── 6. Mini-API ── +echo "[6/6] Setting up mini-API..." +cat > "$PHOBOS_DIR/server/api.py" << 'PYEOF' +#!/usr/bin/env python3 +"""Phobos Secondary Server API — peer management + health.""" +import json, os, subprocess +from flask import Flask, request, jsonify + +app = Flask(__name__) + +def load_env(): + env = {} + with open("/opt/Phobos/server/server.env") as f: + for line in f: + if "=" in line and not line.startswith("#"): + k, v = line.strip().split("=", 1) + env[k] = v + return env + +def check_api_key(): + env = load_env() + key = request.headers.get("X-API-Key", "") + return key == env.get("MAIN_API_KEY", "") + +@app.route("/api/health") +def health(): + try: + wg = subprocess.check_output(["wg", "show", "wg0"], text=True, timeout=5) + peers = wg.count("peer:") + return jsonify({"status": "ok", "peers": peers}) + except Exception as e: + return jsonify({"status": "error", "error": str(e)}), 500 + +@app.route("/api/peers", methods=["GET"]) +def list_peers(): + if not check_api_key(): + return jsonify({"error": "unauthorized"}), 401 + try: + out = subprocess.check_output(["wg", "show", "wg0", "allowed-ips"], text=True, timeout=5) + peers = {} + for line in out.strip().split("\n"): + if "\t" in line: + pub, ips = line.split("\t", 1) + peers[pub.strip()] = ips.strip() + return jsonify({"peers": peers}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/peers/add", methods=["POST"]) +def add_peer(): + if not check_api_key(): + return jsonify({"error": "unauthorized"}), 401 + data = request.json + pub_key = data.get("public_key", "") + allowed_ips = data.get("allowed_ips", "") + if not pub_key or not allowed_ips: + return jsonify({"error": "missing public_key or allowed_ips"}), 400 + try: + subprocess.run(["wg", "set", "wg0", "peer", pub_key, "allowed-ips", allowed_ips], check=True, timeout=5) + subprocess.run(["wg-quick", "save", "wg0"], timeout=5) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/peers/remove", methods=["POST"]) +def remove_peer(): + if not check_api_key(): + return jsonify({"error": "unauthorized"}), 401 + pub_key = request.json.get("public_key", "") + if not pub_key: + return jsonify({"error": "missing public_key"}), 400 + try: + subprocess.run(["wg", "set", "wg0", "peer", pub_key, "remove"], check=True, timeout=5) + subprocess.run(["wg-quick", "save", "wg0"], timeout=5) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/info") +def info(): + if not check_api_key(): + return jsonify({"error": "unauthorized"}), 401 + env = load_env() + return jsonify({ + "ip": env.get("SERVER_PUBLIC_IP_V4"), + "wg_public_key": env.get("SERVER_WG_PUBLIC_KEY"), + "obfuscator_key": env.get("OBFUSCATOR_KEY"), + "ports": env.get("OBFUSCATOR_PORTS", "51821").split(","), + "role": "secondary" + }) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8444) +PYEOF + +cat > /etc/systemd/system/phobos-api.service << EOF +[Unit] +Description=Phobos Secondary API +After=network.target wg-quick@wg0.service + +[Service] +Type=simple +WorkingDirectory=/opt/Phobos/server +ExecStart=/usr/bin/gunicorn -w 1 -b 0.0.0.0:8444 api:app +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable phobos-api -q +systemctl start phobos-api + +# ── 7. Register with main server ── +echo "" +echo "Registering with main server..." +REG_DATA=$(cat << EOF +{ + "ip": "$SERVER_IP", + "wg_public_key": "$WG_PUB", + "obfuscator_key": "$OBF_KEY", + "ports": "$(echo $OBF_PORTS)" +} +EOF +) +curl -s -X POST "http://${MAIN_SERVER}:8443/api/servers/register" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: ${MAIN_API_KEY}" \ + -d "$REG_DATA" || echo " (Registration will be done manually via panel)" + +echo "" +echo "╔══════════════════════════════════════════════════════╗" +echo "║ Secondary Server Ready! ║" +echo "╠══════════════════════════════════════════════════════╣" +echo "║ Server IP : $SERVER_IP" +echo "║ WG Public Key : $WG_PUB" +echo "║ OBF Key : $OBF_KEY" +echo "║ OBF Ports : $OBF_PORTS" +echo "║ API : http://$SERVER_IP:8444" +echo "║ Main server : $MAIN_SERVER" +echo "╚══════════════════════════════════════════════════════╝" +echo ""