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
+
+ | IP | Порты | Peers | Статус | |
+ {rows}
+
+
+
+
+
+
Установка 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 ""