release: 1.4.0 — one version for stable/beta/dev
Light/dark theme and compact mobile layout, per-client speed limit, real speed tests, panel domain (Let's Encrypt), client address + cascade relay, RKN scanner guard, WARP, backup/restore, router web terminal, Android VPN client with split tunnelling and Android admin app. Updating from 1.2.x keeps keys, server.env and clients; tested 1.2.8 -> 1.4.0 on live VMs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
143
server/api.py
143
server/api.py
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phobos Secondary Server API — peer management + health."""
|
||||
import json, os, subprocess
|
||||
import json, os, subprocess, time, hmac
|
||||
from pathlib import Path
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -17,7 +18,50 @@ def load_env():
|
||||
def check_api_key():
|
||||
env = load_env()
|
||||
key = request.headers.get("X-API-Key", "")
|
||||
return key == env.get("MAIN_API_KEY", "")
|
||||
return hmac.compare_digest(key, env.get("MAIN_API_KEY", ""))
|
||||
|
||||
|
||||
def default_iface():
|
||||
try:
|
||||
out = subprocess.check_output(["ip", "route", "get", "8.8.8.8"], text=True, timeout=4)
|
||||
parts = out.split()
|
||||
for i, part in enumerate(parts[:-1]):
|
||||
if part == "dev":
|
||||
return parts[i + 1].strip()
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def cpu_percent():
|
||||
try:
|
||||
with open("/proc/stat") as fh:
|
||||
a = fh.readline().split()[1:]
|
||||
time.sleep(0.1)
|
||||
with open("/proc/stat") as fh:
|
||||
b = fh.readline().split()[1:]
|
||||
a = [int(x) for x in a]
|
||||
b = [int(x) for x in b]
|
||||
idle = b[3] - a[3]
|
||||
total = sum(b) - sum(a)
|
||||
if total <= 0:
|
||||
return "?"
|
||||
return f"{round(100 - (idle * 100.0 / total), 1)}%"
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
def net_bytes():
|
||||
try:
|
||||
iface = default_iface()
|
||||
with open("/proc/net/dev") as fh:
|
||||
for line in fh:
|
||||
if line.split(":")[0].strip() == iface:
|
||||
p = line.split(":")[1].split()
|
||||
return {"iface": iface, "rx": int(p[0]), "tx": int(p[8]), "ts": int(time.time())}
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
@app.route("/api/health")
|
||||
def health():
|
||||
@@ -39,18 +83,45 @@ def health():
|
||||
peers = 0
|
||||
peer_keys = []
|
||||
handshakes = {}
|
||||
cpu = cpu_percent()
|
||||
try:
|
||||
out = subprocess.check_output("top -bn1 | grep Cpu", shell=True, text=True, timeout=5)
|
||||
idle = float([x for x in out.split(",") if "id" in x][0].split()[0])
|
||||
cpu = f"{round(100 - idle, 1)}%"
|
||||
except Exception:
|
||||
cpu = "?"
|
||||
try:
|
||||
mem = subprocess.check_output("free -m", shell=True, text=True).split("\n")[1].split()
|
||||
mem_str = f"{mem[2]}/{mem[1]}MB"
|
||||
parts = Path("/proc/meminfo").read_text().splitlines()
|
||||
vals = {}
|
||||
for line in parts:
|
||||
k, v = line.split(":", 1)
|
||||
vals[k] = int(v.strip().split()[0])
|
||||
total = vals.get("MemTotal", 0) // 1024
|
||||
avail = vals.get("MemAvailable", 0) // 1024
|
||||
used = max(total - avail, 0)
|
||||
mem_str = f"{used}/{total}MB"
|
||||
except Exception:
|
||||
mem_str = "?"
|
||||
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "cpu": cpu, "mem": mem_str})
|
||||
return jsonify({"status": "ok", "peers": peers, "peer_keys": peer_keys, "handshakes": handshakes, "cpu": cpu, "mem": mem_str, "net": net_bytes()})
|
||||
|
||||
|
||||
@app.route("/api/obf-health")
|
||||
def obf_health():
|
||||
"""Return 200 only when all local wg-obfuscator-* services are active."""
|
||||
try:
|
||||
listing = subprocess.check_output(
|
||||
["systemctl", "list-units", "--type=service", "--no-legend",
|
||||
"wg-obfuscator-*"], text=True, timeout=5)
|
||||
names = [ln.split()[0] for ln in listing.splitlines() if ln.strip()]
|
||||
except Exception:
|
||||
names = []
|
||||
if not names:
|
||||
env = load_env()
|
||||
ports = [p.strip() for p in env.get("OBFUSCATOR_PORTS", "2083,5443,993").split(",") if p.strip()]
|
||||
names = [f"wg-obfuscator-{p}.service" for p in ports]
|
||||
bad = []
|
||||
for name in names:
|
||||
status = subprocess.getoutput("systemctl is-active " + name).strip()
|
||||
if status != "active":
|
||||
bad.append(name + "=" + status)
|
||||
if bad:
|
||||
return ("obf-down: " + ",".join(bad), 503, {"Content-Type": "text/plain"})
|
||||
return ("obf-ok " + str(len(names)), 200, {"Content-Type": "text/plain"})
|
||||
|
||||
|
||||
@app.route("/api/peers", methods=["GET"])
|
||||
def list_peers():
|
||||
@@ -67,11 +138,54 @@ def list_peers():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
LIMIT_SCRIPT = "/opt/Phobos/server/phobos-limit.sh"
|
||||
|
||||
|
||||
def ensure_script(path):
|
||||
"""Fetch a helper script from the main panel if this agent predates it."""
|
||||
if os.path.exists(path):
|
||||
return
|
||||
env = load_env()
|
||||
url = "%s://%s:%s/server/%s" % (env.get("MAIN_PROTO", "http"), env.get("MAIN_SERVER", ""),
|
||||
env.get("MAIN_PORT", ""), os.path.basename(path))
|
||||
subprocess.run(["curl", "-kfsSL", "-m", "20", url, "-o", path], timeout=30)
|
||||
if os.path.exists(path):
|
||||
os.chmod(path, 0o755)
|
||||
|
||||
|
||||
@app.route("/api/speedtest", methods=["POST"])
|
||||
def speedtest():
|
||||
if not check_api_key():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
path = "/opt/Phobos/server/phobos-speedtest.sh"
|
||||
ensure_script(path)
|
||||
try:
|
||||
r = subprocess.run([path], capture_output=True, text=True, timeout=80)
|
||||
return jsonify(json.loads(r.stdout.strip().splitlines()[-1]))
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/limits", methods=["POST"])
|
||||
def set_limits():
|
||||
"""Per-client speed limits pushed by the main panel ("<tunnel_ip> <mbit>" lines)."""
|
||||
if not check_api_key():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
text = (request.get_json(force=True, silent=True) or {}).get("limits", "")
|
||||
Path("/opt/Phobos/server/limits.conf").write_text(text)
|
||||
ensure_script(LIMIT_SCRIPT)
|
||||
try:
|
||||
r = subprocess.run([LIMIT_SCRIPT, "apply"], capture_output=True, text=True, timeout=30)
|
||||
return jsonify({"status": "ok" if r.returncode == 0 else "error", "out": (r.stdout + r.stderr)[-500:]})
|
||||
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
|
||||
data = request.get_json(force=True, silent=True) or {}
|
||||
pub_key = data.get("public_key", "")
|
||||
allowed_ips = data.get("allowed_ips", "")
|
||||
if not pub_key or not allowed_ips:
|
||||
@@ -87,7 +201,8 @@ def add_peer():
|
||||
def remove_peer():
|
||||
if not check_api_key():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
pub_key = request.json.get("public_key", "")
|
||||
data = request.get_json(force=True, silent=True) or {}
|
||||
pub_key = data.get("public_key", "")
|
||||
if not pub_key:
|
||||
return jsonify({"error": "missing public_key"}), 400
|
||||
try:
|
||||
@@ -116,7 +231,7 @@ ROUTER_CONFIGS_DIR = "/opt/Phobos/server/router-configs"
|
||||
@app.route("/api/router-config/<client_id>")
|
||||
def router_config(client_id):
|
||||
"""Serve a client's failover.conf so routers can PULL via the tunnel
|
||||
(http://10.25.0.1:8444/...). Token via ?token= or X-API-Key."""
|
||||
(http://<server-tunnel-ip>:8444/...). Token via ?token= or X-API-Key."""
|
||||
token = request.args.get("token", "") or request.headers.get("X-API-Key", "")
|
||||
if token != load_env().get("MAIN_API_KEY", ""):
|
||||
return ("forbidden", 403)
|
||||
|
||||
93
server/phobos-backup.sh
Executable file
93
server/phobos-backup.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# PCA Phobos — полный бэкап / восстановление сервера
|
||||
#
|
||||
# phobos-backup create # создать бэкап -> /opt/Phobos/backups/*.tar.gz
|
||||
# phobos-backup restore <file> # восстановить из бэкапа
|
||||
# phobos-backup list # список бэкапов
|
||||
#
|
||||
# В бэкап входит ВСЁ состояние (НЕ бинарники/пакеты):
|
||||
# WireGuard ключ сервера (wg0.conf), server.env (OBF/WG ключи),
|
||||
# обфускатор-конфиги, все клиенты+ключи, токены, settings.json панели
|
||||
# (router_access, метки, назначения, токен подписчика, версия/канал/порт).
|
||||
#
|
||||
# ⚠ Бэкап содержит ПРИВАТНЫЕ КЛЮЧИ — храни в защищённом месте.
|
||||
#
|
||||
# Disaster recovery на чистом VPS:
|
||||
# 1) поставить turnkey: bash <(curl -fsSL .../install.sh) (ставит бинарники/сервисы)
|
||||
# 2) залить бэкап и: phobos-backup restore phobos-backup-*.tar.gz
|
||||
# Ключ сервера восстановится — клиенты переподключатся без пересоздания.
|
||||
# ============================================================
|
||||
set -u
|
||||
PHOBOS_DIR=/opt/Phobos
|
||||
PANEL_DIR=/opt/phobos-panel
|
||||
BACKUP_DIR="$PHOBOS_DIR/backups"
|
||||
|
||||
panel_service_name() {
|
||||
for name in phobos-dev-test phobos-panel; do
|
||||
systemctl status "$name" >/dev/null 2>&1 && { echo "$name"; return; }
|
||||
done
|
||||
echo phobos-panel
|
||||
}
|
||||
|
||||
PATHS=(
|
||||
/etc/wireguard/wg0.conf
|
||||
"$PHOBOS_DIR/server/server.env"
|
||||
"$PHOBOS_DIR/clients"
|
||||
"$PHOBOS_DIR/tokens"
|
||||
"$PHOBOS_DIR/server/warp"
|
||||
"$PANEL_DIR/settings.json"
|
||||
"$PANEL_DIR/servers.json"
|
||||
"$PANEL_DIR/.version"
|
||||
"$PANEL_DIR/.channel"
|
||||
"$PANEL_DIR/.port"
|
||||
"$PANEL_DIR/.scheme"
|
||||
"$PANEL_DIR/.gh_token"
|
||||
)
|
||||
|
||||
case "${1:-}" in
|
||||
create)
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
OUT="$BACKUP_DIR/phobos-backup-$(hostname -s 2>/dev/null || echo srv)-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||
# -P: absolute paths (restore lands in original locations). obfuscator confs by glob.
|
||||
EXIST=()
|
||||
for p in "${PATHS[@]}"; do [ -e "$p" ] && EXIST+=("$p"); done
|
||||
for f in "$PHOBOS_DIR"/server/wg-obfuscator-*.conf; do [ -e "$f" ] && EXIST+=("$f"); done
|
||||
tar -czf "$OUT" -P --ignore-failed-read "${EXIST[@]}" 2>/dev/null
|
||||
chmod 600 "$OUT"
|
||||
echo "$OUT"
|
||||
;;
|
||||
restore)
|
||||
FILE="${2:-}"
|
||||
[ -f "$FILE" ] || { echo "Файл не найден: $FILE"; exit 1; }
|
||||
# sanity: must be a phobos backup (contains server.env or wg0.conf)
|
||||
if ! tar -tzf "$FILE" 2>/dev/null | grep -qE 'server\.env|wg0\.conf'; then
|
||||
echo "Не похоже на бэкап PCA Phobos (нет server.env/wg0.conf). Отмена."; exit 1
|
||||
fi
|
||||
# safety snapshot before overwrite
|
||||
PRE="$BACKUP_DIR/pre-restore-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
PEX=()
|
||||
for p in "${PATHS[@]}"; do [ -e "$p" ] && PEX+=("$p"); done
|
||||
for f in "$PHOBOS_DIR"/server/wg-obfuscator-*.conf; do [ -e "$f" ] && PEX+=("$f"); done
|
||||
[ ${#PEX[@]} -gt 0 ] && tar -czf "$PRE" -P --ignore-failed-read "${PEX[@]}" 2>/dev/null && echo "Снимок до восстановления: $PRE"
|
||||
tar -xzf "$FILE" -P
|
||||
chmod 600 /etc/wireguard/wg0.conf 2>/dev/null || true
|
||||
systemctl restart wg-quick@wg0 2>/dev/null || true
|
||||
for s in $(systemctl list-units --all --plain --no-legend 'wg-obfuscator-*' 2>/dev/null | awk '{print $1}'); do
|
||||
systemctl restart "$s" 2>/dev/null || true
|
||||
done
|
||||
systemctl restart "$(panel_service_name)" 2>/dev/null || true
|
||||
echo "Восстановлено из $FILE. Сервисы перезапущены."
|
||||
;;
|
||||
list)
|
||||
echo "Бэкапы в $BACKUP_DIR:"
|
||||
ls -lht "$BACKUP_DIR"/phobos-backup-*.tar.gz 2>/dev/null | awk '{print " "$9" ("$5")"}' || echo " нет"
|
||||
;;
|
||||
*)
|
||||
echo "phobos-backup — бэкап/восстановление сервера PCA Phobos"
|
||||
echo " create создать бэкап"
|
||||
echo " restore <file> восстановить"
|
||||
echo " list список бэкапов"
|
||||
;;
|
||||
esac
|
||||
93
server/phobos-cascade.sh
Executable file
93
server/phobos-cascade.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# PCA Phobos — cascade relay. Run on an ENTRY VPS (e.g. in Russia, "white" IP):
|
||||
# clients connect to this server, UDP obfuscator ports are forwarded to the EXIT
|
||||
# Phobos server abroad. The entry box needs no Phobos, no keys, no panel.
|
||||
# bash <(curl -fsSL .../server/phobos-cascade.sh) enable <exit_host> [ports]
|
||||
# phobos-cascade.sh disable | status | apply
|
||||
# Then in the exit server panel: Settings -> "Адрес для клиентов" -> entry IP/domain.
|
||||
set -uo pipefail
|
||||
STATE=/etc/phobos-cascade.env
|
||||
BIN=/usr/local/sbin/phobos-cascade
|
||||
SELF_URL="${SELF_URL:-https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/phobos-cascade.sh}"
|
||||
TAG="phobos-cascade"
|
||||
|
||||
clear_rules() {
|
||||
local t
|
||||
for t in nat filter; do
|
||||
iptables -t $t -S 2>/dev/null | grep -- "--comment $TAG" | sed 's/^-A /-D /' | \
|
||||
while read -r rule; do eval iptables -t $t $rule 2>/dev/null; done
|
||||
done
|
||||
}
|
||||
|
||||
apply() {
|
||||
[ -f "$STATE" ] || { echo "cascade: not configured"; return 0; }
|
||||
. "$STATE"
|
||||
local ip p
|
||||
ip=$(getent ahostsv4 "$EXIT_HOST" | awk 'NR==1{print $1}')
|
||||
[ -n "$ip" ] || { echo "cascade: cannot resolve $EXIT_HOST" >&2; return 1; }
|
||||
sysctl -qw net.ipv4.ip_forward=1
|
||||
clear_rules
|
||||
for p in ${PORTS//,/ }; do
|
||||
iptables -t nat -A PREROUTING -p udp --dport "$p" -m comment --comment $TAG -j DNAT --to-destination "$ip:$p"
|
||||
iptables -t nat -A POSTROUTING -p udp -d "$ip" --dport "$p" -m comment --comment $TAG -j MASQUERADE
|
||||
iptables -I FORWARD -p udp -d "$ip" --dport "$p" -m comment --comment $TAG -j ACCEPT
|
||||
iptables -I FORWARD -p udp -s "$ip" --sport "$p" -m comment --comment $TAG -j ACCEPT
|
||||
done
|
||||
echo "cascade: udp ${PORTS} -> ${EXIT_HOST} (${ip})"
|
||||
}
|
||||
|
||||
enable() {
|
||||
local host="${1:-}" ports="${2:-2083,5443,993}"
|
||||
[ -n "$host" ] || { echo "usage: $0 enable <exit_host> [ports]" >&2; exit 2; }
|
||||
[[ "$ports" =~ ^[0-9]+(,[0-9]+)*$ ]] || { echo "bad ports: $ports" >&2; exit 2; }
|
||||
command -v iptables >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq iptables >/dev/null
|
||||
printf 'EXIT_HOST=%s\nPORTS=%s\n' "$host" "$ports" > "$STATE"
|
||||
# bash <(curl ...) gives a pipe as $0 -> fetch a real copy for the systemd unit
|
||||
if [ "$(readlink -f "$0")" != "$BIN" ]; then
|
||||
if [ -f "$0" ]; then cp "$0" "$BIN"; else curl -fsSL -m30 "$SELF_URL" -o "$BIN" || { echo "cannot save $BIN" >&2; exit 1; }; fi
|
||||
chmod +x "$BIN"
|
||||
fi
|
||||
cat > /etc/systemd/system/phobos-cascade.service <<EOF
|
||||
[Unit]
|
||||
Description=PCA Phobos cascade relay
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=$BIN apply
|
||||
ExecStop=$BIN stop
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable phobos-cascade.service >/dev/null 2>&1
|
||||
systemctl restart phobos-cascade.service
|
||||
apply >/dev/null
|
||||
status
|
||||
}
|
||||
|
||||
disable() {
|
||||
systemctl disable --now phobos-cascade.service >/dev/null 2>&1 || true
|
||||
clear_rules
|
||||
rm -f /etc/systemd/system/phobos-cascade.service "$STATE"
|
||||
systemctl daemon-reload
|
||||
echo "cascade: disabled"
|
||||
}
|
||||
|
||||
status() {
|
||||
local host="" ports="" rules
|
||||
[ -f "$STATE" ] && . "$STATE" && host="$EXIT_HOST" && ports="$PORTS"
|
||||
rules=$(iptables -t nat -S PREROUTING 2>/dev/null | grep -c -- "--comment $TAG")
|
||||
printf '{"enabled": %s, "exit_host": "%s", "ports": "%s", "rules": %s}\n' \
|
||||
"$([ "$rules" -gt 0 ] && echo true || echo false)" "$host" "$ports" "$rules"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
enable) enable "${2:-}" "${3:-}" ;;
|
||||
apply) apply ;;
|
||||
stop) clear_rules ;;
|
||||
disable) disable ;;
|
||||
status) status ;;
|
||||
*) echo "usage: $0 enable <exit_host> [ports]|disable|status"; exit 2 ;;
|
||||
esac
|
||||
105
server/phobos-domain.sh
Executable file
105
server/phobos-domain.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# PCA Phobos — panel domain: https://<domain>/ via nginx + Let's Encrypt.
|
||||
# The panel keeps listening on its old http port, so routers/secondaries that
|
||||
# already know http://IP:PORT keep working; the domain is an extra entrance.
|
||||
# phobos-domain.sh set <domain> [email] (SELF_SIGNED=1 for LAN tests)
|
||||
# phobos-domain.sh remove | status
|
||||
set -uo pipefail
|
||||
PANEL_DIR=/opt/phobos-panel
|
||||
SITE=/etc/nginx/sites-available/phobos-domain
|
||||
WEBROOT=/var/www/phobos-acme
|
||||
SS_DIR=/etc/phobos-domain
|
||||
|
||||
panel_port() { cat "$PANEL_DIR/.port" 2>/dev/null || echo 8443; }
|
||||
panel_scheme() { cat "$PANEL_DIR/.scheme" 2>/dev/null || echo http; }
|
||||
|
||||
write_site() { # $1=domain $2=cert $3=key ($2 empty -> http only, for ACME)
|
||||
local d="$1" up
|
||||
up="$(panel_scheme)://127.0.0.1:$(panel_port)"
|
||||
{
|
||||
echo "server {"
|
||||
echo " listen 80;"
|
||||
echo " listen [::]:80;"
|
||||
echo " server_name $d;"
|
||||
echo " location /.well-known/acme-challenge/ { root $WEBROOT; }"
|
||||
[ -n "$2" ] && echo " location / { return 301 https://\$host\$request_uri; }"
|
||||
echo "}"
|
||||
if [ -n "$2" ]; then
|
||||
cat <<EOF
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name $d;
|
||||
ssl_certificate $2;
|
||||
ssl_certificate_key $3;
|
||||
client_max_body_size 64m;
|
||||
location / {
|
||||
proxy_pass $up;
|
||||
proxy_ssl_verify off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Forwarded-For \$remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
} > "$SITE"
|
||||
ln -sf "$SITE" /etc/nginx/sites-enabled/phobos-domain
|
||||
# nginx < 1.25 has no "http2 on;" -> fall back to the old listen flag
|
||||
nginx -t >/dev/null 2>&1 || sed -i -e '/http2 on;/d' -e 's/listen 443 ssl;/listen 443 ssl http2;/' -e 's/listen \[::\]:443 ssl;/listen [::]:443 ssl http2;/' "$SITE"
|
||||
nginx -t >/dev/null 2>&1 || { nginx -t; return 1; }
|
||||
systemctl reload nginx
|
||||
}
|
||||
|
||||
set_domain() {
|
||||
local d="${1:-}" email="${2:-}" cert key
|
||||
[[ "$d" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$ ]] || { echo "bad domain: $d" >&2; exit 2; }
|
||||
command -v nginx >/dev/null || { echo "nginx not installed" >&2; exit 1; }
|
||||
mkdir -p "$WEBROOT"
|
||||
write_site "$d" "" "" || exit 1
|
||||
if [ "${SELF_SIGNED:-0}" = 1 ]; then
|
||||
mkdir -p "$SS_DIR"
|
||||
openssl req -x509 -nodes -newkey rsa:2048 -days 825 -subj "/CN=$d" \
|
||||
-keyout "$SS_DIR/$d.key" -out "$SS_DIR/$d.crt" >/dev/null 2>&1
|
||||
cert="$SS_DIR/$d.crt"; key="$SS_DIR/$d.key"
|
||||
else
|
||||
command -v certbot >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq certbot >/dev/null
|
||||
local mail_args=(--register-unsafely-without-email)
|
||||
[ -n "$email" ] && mail_args=(-m "$email")
|
||||
certbot certonly --webroot -w "$WEBROOT" -d "$d" --agree-tos -n "${mail_args[@]}" \
|
||||
--deploy-hook "systemctl reload nginx" || {
|
||||
echo "certbot failed: check that $d points to this server and port 80 is open" >&2
|
||||
rm -f /etc/nginx/sites-enabled/phobos-domain; systemctl reload nginx; exit 1; }
|
||||
cert="/etc/letsencrypt/live/$d/fullchain.pem"; key="/etc/letsencrypt/live/$d/privkey.pem"
|
||||
fi
|
||||
write_site "$d" "$cert" "$key" || exit 1
|
||||
echo "$d" > "$PANEL_DIR/.domain"
|
||||
echo "domain: https://$d/ -> panel $(panel_scheme)://127.0.0.1:$(panel_port)"
|
||||
}
|
||||
|
||||
remove() {
|
||||
rm -f /etc/nginx/sites-enabled/phobos-domain "$SITE" "$PANEL_DIR/.domain"
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx
|
||||
echo "domain: removed (certificate kept in /etc/letsencrypt)"
|
||||
}
|
||||
|
||||
status() {
|
||||
local d exp=""
|
||||
d=$(cat "$PANEL_DIR/.domain" 2>/dev/null)
|
||||
if [ -n "$d" ]; then
|
||||
exp=$(echo | timeout 5 openssl s_client -connect 127.0.0.1:443 -servername "$d" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||
fi
|
||||
printf '{"domain": "%s", "cert_expires": "%s"}\n' "$d" "$exp"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
set) set_domain "${2:-}" "${3:-}" ;;
|
||||
remove) remove ;;
|
||||
status) status ;;
|
||||
*) echo "usage: $0 set <domain> [email]|remove|status"; exit 2 ;;
|
||||
esac
|
||||
113
server/phobos-guard.sh
Executable file
113
server/phobos-guard.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# PCA Phobos — guard: drop RKN probes / scanner subnets before they reach the server.
|
||||
# Idea and IP list: github.com/Loorrr293/Auto_IPtables (list: Loorrr293/blocklist).
|
||||
# We only download the IP list (data), never third-party code.
|
||||
# phobos-guard.sh enable [list_url] install nft table + daily timer
|
||||
# phobos-guard.sh update refresh the list now
|
||||
# phobos-guard.sh disable remove table + timer
|
||||
# phobos-guard.sh status JSON
|
||||
set -uo pipefail
|
||||
STATE=/etc/phobos-guard.env
|
||||
DEFAULT_URL="https://raw.githubusercontent.com/Loorrr293/blocklist/main/blocklist.txt"
|
||||
TABLE="inet phobos_guard"
|
||||
SELF="$(readlink -f "$0")"
|
||||
|
||||
list_url() { [ -f "$STATE" ] && . "$STATE"; echo "${GUARD_URL:-$DEFAULT_URL}"; }
|
||||
|
||||
ensure_nft() {
|
||||
command -v nft >/dev/null 2>&1 && return 0
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nftables >/dev/null 2>&1
|
||||
}
|
||||
|
||||
update_list() {
|
||||
local url tmp nf
|
||||
url="$(list_url)"
|
||||
tmp="$(mktemp)"; nf="$(mktemp)"
|
||||
curl -fsSL -m60 "$url" -o "$tmp" || { rm -f "$tmp" "$nf"; echo "download failed: $url" >&2; return 1; }
|
||||
nft add table $TABLE 2>/dev/null || true
|
||||
nft add set $TABLE v4 '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null || true
|
||||
nft add set $TABLE v6 '{ type ipv6_addr; flags interval; auto-merge; }' 2>/dev/null || true
|
||||
nft add chain $TABLE input '{ type filter hook input priority raw; policy accept; }' 2>/dev/null || true
|
||||
nft list chain $TABLE input | grep -q '@v4' || nft add rule $TABLE input ip saddr @v4 counter drop
|
||||
nft list chain $TABLE input | grep -q '@v6' || nft add rule $TABLE input ip6 saddr @v6 counter drop
|
||||
# collapse + skip private/loopback so a bad list can never cut LAN/tunnel/admin
|
||||
python3 - "$tmp" > "$nf" <<'PY'
|
||||
import ipaddress, sys
|
||||
v4, v6 = [], []
|
||||
for line in open(sys.argv[1], encoding="utf-8", errors="ignore"):
|
||||
for tok in line.split("#", 1)[0].split():
|
||||
try:
|
||||
n = ipaddress.ip_network(tok, strict=False)
|
||||
except ValueError:
|
||||
continue
|
||||
if n.is_private or n.is_loopback or n.is_link_local or n.prefixlen == 0:
|
||||
continue
|
||||
(v6 if n.version == 6 else v4).append(n)
|
||||
print("flush set inet phobos_guard v4")
|
||||
print("flush set inet phobos_guard v6")
|
||||
for name, nets in (("v4", v4), ("v6", v6)):
|
||||
nets = list(ipaddress.collapse_addresses(nets))
|
||||
for i in range(0, len(nets), 2000):
|
||||
print("add element inet phobos_guard %s { %s }" % (name, ", ".join(map(str, nets[i:i + 2000]))))
|
||||
PY
|
||||
local rc=0
|
||||
nft -f "$nf" || rc=1
|
||||
rm -f "$tmp" "$nf"
|
||||
[ "$rc" = 0 ] && echo "guard: list loaded from $url"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
enable() {
|
||||
ensure_nft || { echo "nftables install failed" >&2; exit 1; }
|
||||
[ -n "${1:-}" ] && echo "GUARD_URL=$1" > "$STATE" || touch "$STATE"
|
||||
cat > /etc/systemd/system/phobos-guard.service <<EOF
|
||||
[Unit]
|
||||
Description=PCA Phobos guard (RKN scanner blocklist)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$SELF update
|
||||
EOF
|
||||
cat > /etc/systemd/system/phobos-guard.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Refresh PCA Phobos guard list daily
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnCalendar=daily
|
||||
RandomizedDelaySec=1h
|
||||
Persistent=true
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now phobos-guard.timer >/dev/null 2>&1
|
||||
update_list
|
||||
}
|
||||
|
||||
disable() {
|
||||
systemctl disable --now phobos-guard.timer >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/phobos-guard.service /etc/systemd/system/phobos-guard.timer "$STATE"
|
||||
systemctl daemon-reload
|
||||
nft delete table $TABLE 2>/dev/null || true
|
||||
echo "guard: disabled"
|
||||
}
|
||||
|
||||
status() {
|
||||
local on=false n4=0 drops=0
|
||||
if nft list table $TABLE >/dev/null 2>&1; then
|
||||
on=true
|
||||
n4=$(nft list set $TABLE v4 2>/dev/null | grep -o '[0-9.]*/[0-9]*\|[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*' | wc -l)
|
||||
drops=$(nft list chain $TABLE input 2>/dev/null | grep -o 'packets [0-9]*' | awk '{s+=$2} END {print s+0}')
|
||||
fi
|
||||
printf '{"enabled": %s, "networks": %s, "dropped_packets": %s, "timer": "%s", "url": "%s"}\n' \
|
||||
"$on" "$n4" "$drops" "$(systemctl is-active phobos-guard.timer 2>/dev/null)" "$(list_url)"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
enable) enable "${2:-}" ;;
|
||||
update) update_list ;;
|
||||
disable) disable ;;
|
||||
status) status ;;
|
||||
*) echo "usage: $0 enable [url]|update|disable|status"; exit 2 ;;
|
||||
esac
|
||||
@@ -25,7 +25,10 @@ PRIMARY_CHECK_INTERVAL=300
|
||||
# wg-obfuscator-* services are active — the bare panel port stays up even
|
||||
# when the tunnel path is dead, so we must NOT switch back on panel liveness
|
||||
# alone (that caused premature switchback to a dead primary).
|
||||
PRIMARY_PROBE_PORT=10514
|
||||
PRIMARY_PROBE_SCHEME="${PRIMARY_PROBE_SCHEME:-$(cat "$PHOBOS_DIR/panel_scheme" 2>/dev/null)}"
|
||||
PRIMARY_PROBE_SCHEME="${PRIMARY_PROBE_SCHEME:-http}"
|
||||
PRIMARY_PROBE_PORT="${PRIMARY_PROBE_PORT:-$(cat "$PHOBOS_DIR/panel_port" 2>/dev/null)}"
|
||||
PRIMARY_PROBE_PORT="${PRIMARY_PROBE_PORT:-8443}"
|
||||
PRIMARY_PROBE_PATH="/api/obf-health"
|
||||
# Connectivity check targets (logged for context only)
|
||||
CHECK_HOST_1="8.8.8.8"
|
||||
@@ -274,13 +277,22 @@ check_primary() {
|
||||
# Probe the obfuscator-health endpoint: http 200 ONLY when the primary's
|
||||
# obfuscator path is actually up. Anything else (000 no-response, 503
|
||||
# obf-down, redirects) means the tunnel path is NOT viable → stay put.
|
||||
code=$(curl -s -m 4 -o /dev/null -w '%{http_code}' "http://${host}:${PRIMARY_PROBE_PORT}${PRIMARY_PROBE_PATH}" 2>/dev/null)
|
||||
if [ "$code" = "200" ]; then
|
||||
log "PRIMARY ($host obf-health http=200) alive, switching back"
|
||||
ok_url=""
|
||||
last_code=""
|
||||
for url in "${PRIMARY_PROBE_SCHEME}://${host}:${PRIMARY_PROBE_PORT}${PRIMARY_PROBE_PATH}" "http://${host}:8444${PRIMARY_PROBE_PATH}"; do
|
||||
code=$(curl -k -s -m 4 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
||||
last_code="$code"
|
||||
if [ "$code" = "200" ]; then
|
||||
ok_url="$url"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$ok_url" ]; then
|
||||
log "PRIMARY ($ok_url http=200) alive, switching back"
|
||||
switch_endpoint 1 0
|
||||
echo "0" > "$STATE/fail_count"
|
||||
else
|
||||
log "PRIMARY still down (obf-health http=${code:-none}), staying on server $CURRENT_SERVER"
|
||||
log "PRIMARY still down (obf-health http=${last_code:-none}), staying on server $CURRENT_SERVER"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
50
server/phobos-limit.sh
Executable file
50
server/phobos-limit.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# PCA Phobos — per-client speed limit on wg0 (tc).
|
||||
# limits file: one line per client "<tunnel_ip> <mbit>" (download = upload = mbit)
|
||||
# phobos-limit.sh apply [file] rebuild tc from file (default below)
|
||||
# phobos-limit.sh clear | status
|
||||
set -uo pipefail
|
||||
DEV="${WG_DEV:-wg0}"
|
||||
FILE="${2:-/opt/Phobos/server/limits.conf}"
|
||||
|
||||
clear_tc() {
|
||||
tc qdisc del dev "$DEV" root 2>/dev/null
|
||||
tc qdisc del dev "$DEV" ingress 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
apply() {
|
||||
ip link show "$DEV" >/dev/null 2>&1 || { echo "limit: $DEV is down"; return 0; }
|
||||
clear_tc
|
||||
[ -s "$FILE" ] || { echo "limit: no limits"; return 0; }
|
||||
tc qdisc add dev "$DEV" root handle 1: htb default 9999 r2q 1000
|
||||
tc class add dev "$DEV" parent 1: classid 1:9999 htb rate 10gbit 2>/dev/null
|
||||
tc qdisc add dev "$DEV" handle ffff: ingress
|
||||
local n=10 ip mbit count=0
|
||||
while read -r ip mbit _; do
|
||||
[[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ && "$mbit" =~ ^[0-9]+$ && "$mbit" -gt 0 ]] || continue
|
||||
# download (server -> client): shaped, packets queue instead of dropping
|
||||
tc class add dev "$DEV" parent 1: classid 1:$n htb rate "${mbit}mbit" ceil "${mbit}mbit" burst 64k
|
||||
tc qdisc add dev "$DEV" parent 1:$n fq_codel 2>/dev/null
|
||||
tc filter add dev "$DEV" parent 1: protocol ip prio 1 u32 match ip dst "$ip/32" flowid 1:$n
|
||||
# upload (client -> server): policed on ingress
|
||||
tc filter add dev "$DEV" parent ffff: protocol ip prio 1 u32 match ip src "$ip/32" \
|
||||
police rate "${mbit}mbit" burst $(( mbit * 64 + 256 ))k drop flowid :1
|
||||
n=$((n + 1)); count=$((count + 1))
|
||||
done < "$FILE"
|
||||
echo "limit: $count client(s) limited on $DEV"
|
||||
}
|
||||
|
||||
status() {
|
||||
local cnt
|
||||
cnt=$(tc class show dev "$DEV" 2>/dev/null | grep -c 'htb 1:[1-9][0-9]* ' )
|
||||
cnt=$((cnt > 0 ? cnt - 1 : 0))
|
||||
printf '{"device": "%s", "limited": %s, "file": "%s"}\n' "$DEV" "$cnt" "$FILE"
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
apply) apply ;;
|
||||
clear) clear_tc; echo "limit: cleared" ;;
|
||||
status) status ;;
|
||||
*) echo "usage: $0 apply [file]|clear|status"; exit 2 ;;
|
||||
esac
|
||||
@@ -24,14 +24,18 @@ PHOBOS_DIR="/opt/etc/Phobos"
|
||||
CONF="$PHOBOS_DIR/failover.conf"
|
||||
HEALTH="$PHOBOS_DIR/phobos-health.sh"
|
||||
LOG="$PHOBOS_DIR/health.log"
|
||||
# Config sources, tried in order. TUNNEL FIRST: 10.25.0.1 is the wg0 of whatever
|
||||
# server the tunnel currently terminates on, so management rides the same
|
||||
# obfuscated channel as data — survives a public-IP ban and follows failover.
|
||||
# - 10.25.0.1:8444 -> secondary server agent (phobos-api)
|
||||
# - 10.25.0.1:10514 -> primary server panel
|
||||
# Config sources, tried in order. TUNNEL FIRST: panel_tunnel_ip is the wg0 of
|
||||
# whatever server the tunnel currently terminates on, so management rides the
|
||||
# same obfuscated channel as data — survives a public-IP ban and follows
|
||||
# failover.
|
||||
# - panel_tunnel_ip:8444 -> secondary server agent (phobos-api)
|
||||
# - panel_tunnel_ip:<panel_port> -> primary server panel
|
||||
# - public panel IP -> bootstrap / if tunnel is down
|
||||
PANEL="${PANEL:-http://212.118.52.193:10514}"
|
||||
PANEL_URLS="${PANEL_URLS:-http://10.25.0.1:8444 http://10.25.0.1:10514 $PANEL}"
|
||||
PANEL_TUNNEL_IP="${PANEL_TUNNEL_IP:-$(cat "$PHOBOS_DIR/panel_tunnel_ip" 2>/dev/null)}"
|
||||
[ -n "$PANEL_TUNNEL_IP" ] || PANEL_TUNNEL_IP="10.25.0.1"
|
||||
PANEL="${PANEL:-$(cat "$PHOBOS_DIR/panel_url" 2>/dev/null)}"
|
||||
PANEL_URLS="${PANEL_URLS:-$(cat "$PHOBOS_DIR/panel_urls" 2>/dev/null)}"
|
||||
[ -n "$PANEL_URLS" ] || PANEL_URLS="http://${PANEL_TUNNEL_IP}:8444 $PANEL"
|
||||
CLIENT_ID=$(cat "$PHOBOS_DIR/client_id" 2>/dev/null || echo "home")
|
||||
TOKEN=$(cat "$PHOBOS_DIR/pull_token" 2>/dev/null)
|
||||
TMP="/tmp/failover.conf.pull"
|
||||
@@ -50,21 +54,24 @@ trap 'rm -f "$LOCK"' EXIT
|
||||
[ -z "$TOKEN" ] && exit 0
|
||||
|
||||
detect_tunnel_iface() {
|
||||
ip -o addr show 2>/dev/null | awk '$4 ~ /^10\.25\./ {print $2; exit}'
|
||||
ip route get "$PANEL_TUNNEL_IP" 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="dev") print $(i+1)}' | head -1
|
||||
}
|
||||
|
||||
fetch_config() {
|
||||
base="$1"
|
||||
url="${base}/api/router-config/${CLIENT_ID}?token=${TOKEN}"
|
||||
case "$base" in
|
||||
http://10.25.0.1:*|https://10.25.0.1:*)
|
||||
"http://${PANEL_TUNNEL_IP}:"*|"https://${PANEL_TUNNEL_IP}:"*)
|
||||
iface=$(detect_tunnel_iface)
|
||||
if [ -n "$iface" ] && command -v curl >/dev/null; then
|
||||
curl --interface "$iface" -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
|
||||
curl -k --interface "$iface" -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
curl -s -m 8 -o "$TMP" "$url" 2>/dev/null
|
||||
if command -v curl >/dev/null; then
|
||||
curl -k -s -m 8 -o "$TMP" "$url" 2>/dev/null && return 0
|
||||
fi
|
||||
wget --no-check-certificate -q -O "$TMP" "$url" 2>/dev/null || wget -q -O "$TMP" "$url" 2>/dev/null
|
||||
}
|
||||
|
||||
# One fetch + compare + apply pass. Returns 0 always (best-effort).
|
||||
|
||||
0
server/phobos-self-check.sh
Normal file → Executable file
0
server/phobos-self-check.sh
Normal file → Executable file
28
server/phobos-speedtest.sh
Executable file
28
server/phobos-speedtest.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# PCA Phobos — real server internet speed: download/upload actual bytes, print JSON.
|
||||
# Cloudflare speed endpoints first, Hetzner/OVH files as download fallback.
|
||||
# phobos-speedtest.sh [seconds]
|
||||
set -uo pipefail
|
||||
T="${1:-8}"
|
||||
CF="https://speed.cloudflare.com"
|
||||
|
||||
down_mbit() { # $1=url -> Mbit/s
|
||||
curl -s -o /dev/null -m "$T" -w '%{speed_download}' "$1" 2>/dev/null | awk '{printf "%.1f", $1*8/1e6}'
|
||||
}
|
||||
|
||||
ping_ms() {
|
||||
curl -s -o /dev/null -m 5 -w '%{time_connect}' "$CF/__down?bytes=0" 2>/dev/null | awk '{printf "%.0f", $1*1000}'
|
||||
}
|
||||
|
||||
dl=$(down_mbit "$CF/__down?bytes=500000000")
|
||||
src="cloudflare"
|
||||
if [ -z "$dl" ] || awk "BEGIN{exit !($dl < 1)}"; then
|
||||
for u in https://speed.hetzner.de/1GB.bin https://proof.ovh.net/files/1Gb.dat; do
|
||||
dl=$(down_mbit "$u"); src="$u"
|
||||
awk "BEGIN{exit !($dl >= 1)}" && break
|
||||
done
|
||||
fi
|
||||
ul=$(head -c 200000000 /dev/zero | curl -s -o /dev/null -m "$T" -w '%{speed_upload}' \
|
||||
-X POST --data-binary @- "$CF/__up" 2>/dev/null | awk '{printf "%.1f", $1*8/1e6}')
|
||||
printf '{"download_mbit": %s, "upload_mbit": %s, "ping_ms": %s, "source": "%s", "ts": %s}\n' \
|
||||
"${dl:-0}" "${ul:-0}" "$(ping_ms || echo 0)" "$src" "$(date +%s)"
|
||||
519
server/phobos-warp.sh
Executable file
519
server/phobos-warp.sh
Executable file
@@ -0,0 +1,519 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVER_ENV="/opt/Phobos/server/server.env"
|
||||
WARP_DIR="/opt/Phobos/server/warp"
|
||||
WGCF_BIN="/usr/local/bin/wgcf"
|
||||
WARP_IF="warp"
|
||||
WARP_CONF="/etc/wireguard/${WARP_IF}.conf"
|
||||
WARP_TABLE="51820"
|
||||
WARP_RULE_PRIO="11930"
|
||||
STATE_FILE="${WARP_DIR}/state.env"
|
||||
FREE_PROFILE="${WARP_DIR}/wgcf-profile.conf"
|
||||
CUSTOM_PROFILE="${WARP_DIR}/custom-profile.conf"
|
||||
LICENSE_FILE="${WARP_DIR}/license.key"
|
||||
RELAY_TAG_PREFIX="PHOBOS_WARP_RELAY"
|
||||
|
||||
load_env() {
|
||||
if [ -f "$SERVER_ENV" ]; then
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in
|
||||
''|\#*) continue ;;
|
||||
esac
|
||||
k="${line%%=*}"
|
||||
v="${line#*=}"
|
||||
case "$k" in
|
||||
''|\#*) continue ;;
|
||||
esac
|
||||
export "$k=$v"
|
||||
done < "$SERVER_ENV"
|
||||
fi
|
||||
}
|
||||
|
||||
load_state() {
|
||||
STATE_MODE=""
|
||||
STATE_PROFILE_SOURCE=""
|
||||
STATE_RELAY_PORTS=""
|
||||
STATE_RELAY_TARGET=""
|
||||
STATE_RELAY_TARGET_IP=""
|
||||
STATE_CLIENTS=""
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$STATE_FILE"
|
||||
STATE_MODE="${MODE:-}"
|
||||
STATE_PROFILE_SOURCE="${PROFILE_SOURCE:-}"
|
||||
STATE_RELAY_PORTS="${RELAY_PORTS:-}"
|
||||
STATE_RELAY_TARGET="${RELAY_TARGET:-}"
|
||||
STATE_RELAY_TARGET_IP="${RELAY_TARGET_IP:-}"
|
||||
STATE_CLIENTS="${CLIENTS:-}"
|
||||
fi
|
||||
}
|
||||
|
||||
save_state() {
|
||||
mkdir -p "$WARP_DIR"
|
||||
cat > "$STATE_FILE" <<EOF
|
||||
MODE=${1:-}
|
||||
PROFILE_SOURCE=${2:-}
|
||||
RELAY_PORTS=${3:-}
|
||||
RELAY_TARGET=${4:-}
|
||||
RELAY_TARGET_IP=${5:-}
|
||||
CLIENTS=${6:-}
|
||||
EOF
|
||||
}
|
||||
|
||||
server_public_ip() {
|
||||
if [ -n "${SERVER_PUBLIC_IP_V4:-}" ]; then
|
||||
printf '%s\n' "$SERVER_PUBLIC_IP_V4"
|
||||
else
|
||||
curl -4 -fsS --max-time 6 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
wg_subnet() {
|
||||
if [ -n "${SERVER_WG_IPV4_NETWORK:-}" ]; then
|
||||
printf '%s\n' "$SERVER_WG_IPV4_NETWORK"
|
||||
return
|
||||
fi
|
||||
if [ -f /etc/wireguard/wg0.conf ]; then
|
||||
python3 - <<'PY' /etc/wireguard/wg0.conf
|
||||
import ipaddress, re, sys
|
||||
for raw in open(sys.argv[1], encoding="utf-8"):
|
||||
m = re.match(r"^Address\s*=\s*(.+)$", raw.strip())
|
||||
if not m:
|
||||
continue
|
||||
value = m.group(1).split(",", 1)[0].strip()
|
||||
print(ipaddress.ip_interface(value).network)
|
||||
break
|
||||
PY
|
||||
return
|
||||
fi
|
||||
printf '10.25.0.0/16\n'
|
||||
}
|
||||
|
||||
arch_name() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) echo "amd64" ;;
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
armv7l) echo "armv7" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_wgcf() {
|
||||
if [ -x "$WGCF_BIN" ]; then
|
||||
return
|
||||
fi
|
||||
local arch version url
|
||||
arch="$(arch_name)"
|
||||
[ -n "$arch" ] || { echo "unsupported architecture: $(uname -m)"; exit 1; }
|
||||
version="2.2.26"
|
||||
url="https://github.com/ViRb3/wgcf/releases/download/v${version}/wgcf_${version}_linux_${arch}"
|
||||
curl -fsSL "$url" -o "$WGCF_BIN"
|
||||
chmod +x "$WGCF_BIN"
|
||||
}
|
||||
|
||||
ensure_free_profile() {
|
||||
local license_key=""
|
||||
mkdir -p "$WARP_DIR"
|
||||
cd "$WARP_DIR"
|
||||
ensure_wgcf
|
||||
if [ ! -f "${WARP_DIR}/wgcf-account.toml" ]; then
|
||||
"$WGCF_BIN" register --accept-tos >/tmp/phobos-warp-register.log 2>&1
|
||||
fi
|
||||
if [ -s "$LICENSE_FILE" ]; then
|
||||
license_key="$(tr -d '\r\n' < "$LICENSE_FILE")"
|
||||
if [ -n "$license_key" ]; then
|
||||
"$WGCF_BIN" update --license "$license_key" >/tmp/phobos-warp-license.log 2>&1 || {
|
||||
echo "failed to apply WARP+ license key" >&2
|
||||
cat /tmp/phobos-warp-license.log >&2 || true
|
||||
exit 1
|
||||
}
|
||||
rm -f "$FREE_PROFILE"
|
||||
fi
|
||||
fi
|
||||
if [ ! -f "$FREE_PROFILE" ] || [ ! -s "$FREE_PROFILE" ]; then
|
||||
"$WGCF_BIN" generate >/tmp/phobos-warp-generate.log 2>&1 || {
|
||||
echo "failed to generate wgcf profile" >&2
|
||||
cat /tmp/phobos-warp-generate.log >&2 || true
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_custom_profile() {
|
||||
[ -s "$CUSTOM_PROFILE" ] || { echo "custom profile not found: $CUSTOM_PROFILE" >&2; exit 1; }
|
||||
}
|
||||
|
||||
profile_path_for() {
|
||||
case "${1:-free}" in
|
||||
custom) ensure_custom_profile; printf '%s\n' "$CUSTOM_PROFILE" ;;
|
||||
*) ensure_free_profile; printf '%s\n' "$FREE_PROFILE" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
profile_endpoint() {
|
||||
awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$1" | tr -d ' '
|
||||
}
|
||||
|
||||
resolve_host_ipv4() {
|
||||
getent ahostsv4 "$1" | awk 'NR==1{print $1}'
|
||||
}
|
||||
|
||||
build_warp_conf() {
|
||||
local profile_path subnet
|
||||
profile_path="$1"
|
||||
subnet="$(wg_subnet)"
|
||||
python3 - "$profile_path" "$WARP_CONF" "$subnet" "$WARP_TABLE" "$WARP_RULE_PRIO" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
profile_path = Path(sys.argv[1])
|
||||
out_path = Path(sys.argv[2])
|
||||
subnet = sys.argv[3]
|
||||
table = sys.argv[4]
|
||||
prio = sys.argv[5]
|
||||
|
||||
section = None
|
||||
data = {"Interface": {}, "Peer": {}}
|
||||
for raw in profile_path.read_text().splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
section = line.strip("[]")
|
||||
continue
|
||||
if "=" not in line or section not in data:
|
||||
continue
|
||||
k, v = [x.strip() for x in line.split("=", 1)]
|
||||
data[section][k] = v
|
||||
|
||||
iface = data["Interface"]
|
||||
peer = data["Peer"]
|
||||
|
||||
post_up = (
|
||||
"sysctl -w net.ipv4.ip_forward=1 >/dev/null; "
|
||||
f"ip route replace {subnet} dev wg0 table {table}; "
|
||||
f"ip route replace default dev %i table {table}; "
|
||||
"iptables -C FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || iptables -I FORWARD 1 -i wg0 -o %i -j ACCEPT; "
|
||||
"iptables -C FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || "
|
||||
"iptables -I FORWARD 1 -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; "
|
||||
f"iptables -t nat -C POSTROUTING -s {subnet} -o %i -j MASQUERADE 2>/dev/null || "
|
||||
f"iptables -t nat -A POSTROUTING -s {subnet} -o %i -j MASQUERADE"
|
||||
)
|
||||
post_down = (
|
||||
f"ip route del {subnet} dev wg0 table {table} 2>/dev/null || true; "
|
||||
f"ip route del default dev %i table {table} 2>/dev/null || true; "
|
||||
"iptables -D FORWARD -i wg0 -o %i -j ACCEPT 2>/dev/null || true; "
|
||||
"iptables -D FORWARD -i %i -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true; "
|
||||
f"iptables -t nat -D POSTROUTING -s {subnet} -o %i -j MASQUERADE 2>/dev/null || true"
|
||||
)
|
||||
|
||||
lines = [
|
||||
"[Interface]",
|
||||
f"PrivateKey = {iface.get('PrivateKey', '')}",
|
||||
f"Address = {iface.get('Address', '')}",
|
||||
"MTU = 1280",
|
||||
"Table = off",
|
||||
f"PostUp = {post_up}",
|
||||
f"PostDown = {post_down}",
|
||||
"",
|
||||
"[Peer]",
|
||||
f"PublicKey = {peer.get('PublicKey', '')}",
|
||||
f"AllowedIPs = {peer.get('AllowedIPs', '0.0.0.0/0, ::/0')}",
|
||||
f"Endpoint = {peer.get('Endpoint', '')}",
|
||||
f"PersistentKeepalive = {peer.get('PersistentKeepalive', '25')}",
|
||||
"",
|
||||
]
|
||||
|
||||
out_path.write_text("\n".join(lines))
|
||||
PY
|
||||
chmod 600 "$WARP_CONF"
|
||||
}
|
||||
|
||||
clear_egress() {
|
||||
systemctl disable --now "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
|
||||
ip route del "$(wg_subnet)" dev wg0 table "$WARP_TABLE" 2>/dev/null || true
|
||||
ip route del default dev "$WARP_IF" table "$WARP_TABLE" 2>/dev/null || true
|
||||
iptables -D FORWARD -i wg0 -o "$WARP_IF" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -i "$WARP_IF" -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||
iptables -t nat -D POSTROUTING -s "$(wg_subnet)" -o "$WARP_IF" -j MASQUERADE 2>/dev/null || true
|
||||
}
|
||||
|
||||
clear_legacy_subnet_rule() {
|
||||
local subnet
|
||||
subnet="$(wg_subnet)"
|
||||
ip rule del from "$subnet" table "$WARP_TABLE" priority "$WARP_RULE_PRIO" 2>/dev/null || true
|
||||
ip rule del from "$subnet" lookup "$WARP_TABLE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
client_rule_prio() {
|
||||
python3 - "$1" "$WARP_RULE_PRIO" <<'PY'
|
||||
import sys
|
||||
ip = sys.argv[1].split('/')[0].strip()
|
||||
base = int(sys.argv[2])
|
||||
parts = [int(x) for x in ip.split('.')]
|
||||
value = ((parts[2] << 8) + parts[3]) % 5000
|
||||
print(base + 1 + value)
|
||||
PY
|
||||
}
|
||||
|
||||
normalize_client_ip() {
|
||||
printf '%s\n' "${1%%/*}" | tr -d ' '
|
||||
}
|
||||
|
||||
state_clients_csv_add() {
|
||||
local ip csv
|
||||
ip="$(normalize_client_ip "$1")"
|
||||
csv="${2:-}"
|
||||
python3 - "$ip" "$csv" <<'PY'
|
||||
import sys
|
||||
ip = sys.argv[1]
|
||||
items = [x for x in sys.argv[2].split(',') if x]
|
||||
if ip not in items:
|
||||
items.append(ip)
|
||||
print(",".join(items))
|
||||
PY
|
||||
}
|
||||
|
||||
state_clients_csv_del() {
|
||||
local ip csv
|
||||
ip="$(normalize_client_ip "$1")"
|
||||
csv="${2:-}"
|
||||
python3 - "$ip" "$csv" <<'PY'
|
||||
import sys
|
||||
ip = sys.argv[1]
|
||||
items = [x for x in sys.argv[2].split(',') if x and x != ip]
|
||||
print(",".join(items))
|
||||
PY
|
||||
}
|
||||
|
||||
clear_client_rules() {
|
||||
load_state
|
||||
local ip prio
|
||||
IFS=',' read -r -a __clients <<< "${STATE_CLIENTS:-}"
|
||||
for ip in "${__clients[@]}"; do
|
||||
[ -n "$ip" ] || continue
|
||||
prio="$(client_rule_prio "$ip")"
|
||||
ip rule del from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
reapply_client_rules() {
|
||||
local csv="${1:-}" ip prio
|
||||
IFS=',' read -r -a __clients <<< "$csv"
|
||||
for ip in "${__clients[@]}"; do
|
||||
[ -n "$ip" ] || continue
|
||||
prio="$(client_rule_prio "$ip")"
|
||||
ip rule add from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
relay_ports_array() {
|
||||
local raw="${1:-4500}"
|
||||
raw="${raw// /}"
|
||||
IFS=',' read -r -a RELAY_PORTS_ARRAY <<< "$raw"
|
||||
}
|
||||
|
||||
relay_rule_tag() {
|
||||
printf '%s:%s:%s:%s\n' "$RELAY_TAG_PREFIX" "$1" "$2" "$3"
|
||||
}
|
||||
|
||||
clear_relay() {
|
||||
load_state
|
||||
local lines line cmd
|
||||
lines="$(iptables-save -t nat 2>/dev/null | grep -F "$RELAY_TAG_PREFIX" || true)"
|
||||
if [ -n "$lines" ]; then
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
case "$line" in
|
||||
-A\ *) cmd="iptables -t nat ${line/-A /-D }"; eval "$cmd" >/dev/null 2>&1 || true ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$lines
|
||||
EOF
|
||||
fi
|
||||
lines="$(iptables-save 2>/dev/null | grep -F "$RELAY_TAG_PREFIX" || true)"
|
||||
if [ -n "$lines" ]; then
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
case "$line" in
|
||||
-A\ FORWARD\ *) cmd="iptables ${line/-A /-D }"; eval "$cmd" >/dev/null 2>&1 || true ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$lines
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
disable_all() {
|
||||
clear_legacy_subnet_rule
|
||||
clear_client_rules
|
||||
clear_egress
|
||||
clear_relay
|
||||
}
|
||||
|
||||
relay_enabled() {
|
||||
iptables-save 2>/dev/null | grep -q "$RELAY_TAG_PREFIX"
|
||||
}
|
||||
|
||||
enable_egress() {
|
||||
local profile_source profile_path existing_clients
|
||||
load_env
|
||||
load_state
|
||||
existing_clients="${STATE_CLIENTS:-}"
|
||||
profile_source="${1:-free}"
|
||||
disable_all
|
||||
profile_path="$(profile_path_for "$profile_source")"
|
||||
build_warp_conf "$profile_path"
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
systemctl enable "wg-quick@${WARP_IF}" >/dev/null 2>&1 || true
|
||||
systemctl restart "wg-quick@${WARP_IF}"
|
||||
clear_legacy_subnet_rule
|
||||
reapply_client_rules "$existing_clients"
|
||||
save_state "egress" "$profile_source" "" "" "" "$existing_clients"
|
||||
status_json
|
||||
}
|
||||
|
||||
enable_relay() {
|
||||
local profile_source ports target profile_path endpoint host port_num target_ip server_ip port tag
|
||||
load_env
|
||||
profile_source="${1:-free}"
|
||||
ports="${2:-4500}"
|
||||
target="${3:-}"
|
||||
disable_all
|
||||
profile_path="$(profile_path_for "$profile_source")"
|
||||
endpoint="${target:-$(profile_endpoint "$profile_path")}"
|
||||
[ -n "$endpoint" ] || { echo "relay target endpoint is empty" >&2; exit 1; }
|
||||
host="${endpoint%:*}"
|
||||
port_num="${endpoint##*:}"
|
||||
target_ip="$(resolve_host_ipv4 "$host")"
|
||||
[ -n "$target_ip" ] || { echo "failed to resolve endpoint host: $host" >&2; exit 1; }
|
||||
server_ip="$(server_public_ip)"
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
relay_ports_array "$ports"
|
||||
for port in "${RELAY_PORTS_ARRAY[@]}"; do
|
||||
[ -n "$port" ] || continue
|
||||
tag="$(relay_rule_tag "$port" "$target_ip" "$port_num")"
|
||||
iptables -t nat -C PREROUTING -d "$server_ip" -p udp --dport "$port" -m comment --comment "$tag" -j DNAT --to-destination "$target_ip:$port_num" 2>/dev/null \
|
||||
|| iptables -t nat -A PREROUTING -d "$server_ip" -p udp --dport "$port" -m comment --comment "$tag" -j DNAT --to-destination "$target_ip:$port_num"
|
||||
iptables -t nat -C POSTROUTING -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j MASQUERADE 2>/dev/null \
|
||||
|| iptables -t nat -A POSTROUTING -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j MASQUERADE
|
||||
iptables -C FORWARD -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j ACCEPT 2>/dev/null \
|
||||
|| iptables -A FORWARD -p udp -d "$target_ip" --dport "$port_num" -m comment --comment "$tag" -j ACCEPT
|
||||
iptables -C FORWARD -p udp -s "$target_ip" --sport "$port_num" -m comment --comment "$tag" -j ACCEPT 2>/dev/null \
|
||||
|| iptables -A FORWARD -p udp -s "$target_ip" --sport "$port_num" -m comment --comment "$tag" -j ACCEPT
|
||||
done
|
||||
save_state "relay" "$profile_source" "$ports" "$endpoint" "$target_ip" ""
|
||||
status_json
|
||||
}
|
||||
|
||||
client_enable() {
|
||||
local ip prio updated
|
||||
ip="$(normalize_client_ip "${1:-}")"
|
||||
[ -n "$ip" ] || { echo "client ip required" >&2; exit 1; }
|
||||
load_state
|
||||
[ "${STATE_MODE:-}" = "egress" ] || { echo "WARP egress mode is not active" >&2; exit 1; }
|
||||
systemctl is-active --quiet "wg-quick@${WARP_IF}" || { echo "WARP interface is not active" >&2; exit 1; }
|
||||
prio="$(client_rule_prio "$ip")"
|
||||
ip rule add from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
|
||||
updated="$(state_clients_csv_add "$ip" "${STATE_CLIENTS:-}")"
|
||||
save_state "${STATE_MODE:-egress}" "${STATE_PROFILE_SOURCE:-free}" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$updated"
|
||||
status_json
|
||||
}
|
||||
|
||||
client_disable() {
|
||||
local ip prio updated
|
||||
ip="$(normalize_client_ip "${1:-}")"
|
||||
[ -n "$ip" ] || { echo "client ip required" >&2; exit 1; }
|
||||
load_state
|
||||
prio="$(client_rule_prio "$ip")"
|
||||
ip rule del from "${ip}/32" table "$WARP_TABLE" priority "$prio" 2>/dev/null || true
|
||||
updated="$(state_clients_csv_del "$ip" "${STATE_CLIENTS:-}")"
|
||||
save_state "${STATE_MODE:-}" "${STATE_PROFILE_SOURCE:-}" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$updated"
|
||||
status_json
|
||||
}
|
||||
|
||||
status_json() {
|
||||
load_env
|
||||
load_state
|
||||
local subnet service configured endpoint pubkey egress enabled mode profile_source relay_on
|
||||
subnet="$(wg_subnet)"
|
||||
service="$(systemctl is-active "wg-quick@${WARP_IF}" 2>/dev/null || true)"
|
||||
mode="${STATE_MODE:-}"
|
||||
profile_source="${STATE_PROFILE_SOURCE:-}"
|
||||
configured=false
|
||||
endpoint=""
|
||||
pubkey=""
|
||||
egress=""
|
||||
relay_on=false
|
||||
|
||||
if [ "$service" = "active" ]; then
|
||||
mode="${mode:-egress}"
|
||||
configured=true
|
||||
endpoint="$(awk -F'= *' '/^Endpoint *=/{print $2; exit}' "$WARP_CONF" 2>/dev/null || true)"
|
||||
pubkey="$(wg show "${WARP_IF}" public-key 2>/dev/null || true)"
|
||||
egress="$(curl -4 --interface "${WARP_IF}" -fsS --max-time 6 https://api.ipify.org 2>/dev/null || true)"
|
||||
[ -n "$profile_source" ] || profile_source="$( [ -s "$CUSTOM_PROFILE" ] && echo custom || echo free )"
|
||||
fi
|
||||
|
||||
if relay_enabled; then
|
||||
relay_on=true
|
||||
[ -n "$mode" ] || mode="relay"
|
||||
configured=true
|
||||
[ -n "$endpoint" ] || endpoint="${STATE_RELAY_TARGET:-}"
|
||||
[ -n "$profile_source" ] || profile_source="$( [ -s "$CUSTOM_PROFILE" ] && echo custom || echo free )"
|
||||
fi
|
||||
|
||||
enabled=false
|
||||
if [ "$service" = "active" ] || [ "$relay_on" = true ]; then
|
||||
enabled=true
|
||||
fi
|
||||
|
||||
python3 - "$enabled" "$configured" "$service" "$subnet" "$endpoint" "$pubkey" "$egress" "$mode" "$profile_source" "${STATE_RELAY_PORTS:-}" "${STATE_RELAY_TARGET:-}" "${STATE_RELAY_TARGET_IP:-}" "$( [ -s "$CUSTOM_PROFILE" ] && echo true || echo false )" "$relay_on" "$( [ -s "$LICENSE_FILE" ] && echo true || echo false )" "${STATE_CLIENTS:-}" <<'PY'
|
||||
import json, sys
|
||||
enabled, configured, service, subnet, endpoint, pubkey, egress, mode, profile_source, relay_ports, relay_target, relay_target_ip, custom_saved, relay_on, license_saved, clients_csv = sys.argv[1:]
|
||||
if relay_on == "true" and service != "active":
|
||||
service = "relay"
|
||||
print(json.dumps({
|
||||
"enabled": enabled == "true",
|
||||
"configured": configured == "true",
|
||||
"service": service or "inactive",
|
||||
"mode": mode or "off",
|
||||
"profile_source": profile_source or "free",
|
||||
"subnet": subnet,
|
||||
"endpoint": endpoint,
|
||||
"public_key": pubkey,
|
||||
"egress_ipv4": egress,
|
||||
"relay_enabled": relay_on == "true",
|
||||
"relay_ports": relay_ports,
|
||||
"relay_target": relay_target,
|
||||
"relay_target_ip": relay_target_ip,
|
||||
"clients": [x for x in clients_csv.split(",") if x],
|
||||
"custom_profile_saved": custom_saved == "true",
|
||||
"license_key_saved": license_saved == "true",
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
}
|
||||
|
||||
disable_warp() {
|
||||
disable_all
|
||||
save_state "" "" "" "" "" ""
|
||||
status_json
|
||||
}
|
||||
|
||||
case "${1:-status}" in
|
||||
enable|up)
|
||||
case "${2:-egress}" in
|
||||
relay) enable_relay "${3:-free}" "${4:-4500}" "${5:-}" ;;
|
||||
*) enable_egress "${3:-free}" ;;
|
||||
esac
|
||||
;;
|
||||
client-enable) client_enable "${2:-}" ;;
|
||||
client-disable) client_disable "${2:-}" ;;
|
||||
disable|down) disable_warp ;;
|
||||
status|check|--json) status_json ;;
|
||||
*)
|
||||
echo "usage: $0 {status|enable [egress|relay] [free|custom] [ports] [endpoint]|client-enable <ip>|client-disable <ip>|disable}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -4,8 +4,8 @@
|
||||
# Deploys WG + obfuscator + mini-API (no web panel)
|
||||
#
|
||||
# Usage:
|
||||
# MAIN_SERVER=212.118.52.193 MAIN_PORT=10514 MAIN_API_KEY=secret123 \
|
||||
# bash <(curl -fsSL https://raw.githubusercontent.com/andrey271192/PCA_Phobos/main/server/secondary-setup.sh)
|
||||
# MAIN_PROTO=http MAIN_SERVER=203.0.113.10 MAIN_PORT=10514 MAIN_API_KEY=secret123 \
|
||||
# bash <(curl -fsSL http://203.0.113.10:10514/server/secondary-setup.sh)
|
||||
#
|
||||
# Uninstall:
|
||||
# bash /opt/Phobos/server/phobos-secondary-uninstall.sh
|
||||
@@ -15,13 +15,27 @@ 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}"
|
||||
MAIN_PROTO="${MAIN_PROTO:-http}"
|
||||
MAIN_PORT="${MAIN_PORT:-8443}"
|
||||
OBF_PORTS="${OBF_PORTS:-2083,5443,993}"
|
||||
SERVER_WG_IPV4_NETWORK="${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}"
|
||||
PHOBOS_DIR="/opt/Phobos"
|
||||
WG_SERVER_IPV4="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
||||
import ipaddress, sys
|
||||
net = ipaddress.ip_network(sys.argv[1], strict=False)
|
||||
print(next(net.hosts()))
|
||||
PY
|
||||
)"
|
||||
WG_PREFIXLEN="$(python3 - "$SERVER_WG_IPV4_NETWORK" <<'PY'
|
||||
import ipaddress, sys
|
||||
print(ipaddress.ip_network(sys.argv[1], strict=False).prefixlen)
|
||||
PY
|
||||
)"
|
||||
|
||||
SERVER_IP=$(curl -s https://api.ipify.org || hostname -I | awk '{print $1}')
|
||||
SERVER_IP="${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<NF;i++) if($i=="dev") print $(i+1)}' | head -1)
|
||||
IFACE="${IFACE:-eth0}"
|
||||
case "$MAIN_PROTO" in http|https) ;; *) echo "ERROR: MAIN_PROTO must be http or https"; exit 1;; esac
|
||||
|
||||
# ── Pre-flight safety checks ──
|
||||
echo ""
|
||||
@@ -31,6 +45,7 @@ echo "╠═══════════════════════
|
||||
echo "║ Server IP : $SERVER_IP"
|
||||
echo "║ Interface : $IFACE"
|
||||
echo "║ Main server : $MAIN_SERVER"
|
||||
echo "║ Main proto : $MAIN_PROTO"
|
||||
echo "║ Main port : $MAIN_PORT"
|
||||
echo "║ OBF ports : $OBF_PORTS"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
@@ -106,7 +121,7 @@ if [ "$WARNINGS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Found $WARNINGS warning(s). Existing configs will be preserved where possible."
|
||||
echo "Press Enter to continue or Ctrl+C to abort..."
|
||||
read -r < /dev/tty 2>/dev/null || true
|
||||
[ -t 0 ] && read -r < /dev/tty 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -128,7 +143,7 @@ fi
|
||||
# ── 1. Dependencies ──
|
||||
echo "[1/7] Installing dependencies..."
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard jq curl python3 python3-flask gunicorn 2>/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard wireguard-tools iptables jq curl git ca-certificates python3 python3-flask gunicorn 2>/dev/null
|
||||
|
||||
# ── 2. Phobos binaries ──
|
||||
echo "[2/7] Getting Phobos binaries..."
|
||||
@@ -193,7 +208,7 @@ else
|
||||
|
||||
cat > /etc/wireguard/wg0.conf << WGEOF
|
||||
[Interface]
|
||||
Address = 10.25.0.1/16
|
||||
Address = ${WG_SERVER_IPV4}/${WG_PREFIXLEN}
|
||||
ListenPort = 51820
|
||||
PrivateKey = $WG_PRIV
|
||||
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE
|
||||
@@ -259,9 +274,12 @@ 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
|
||||
SERVER_WG_IPV4_NETWORK=$SERVER_WG_IPV4_NETWORK
|
||||
SERVER_WG_IPV4_ADDRESS=${WG_SERVER_IPV4}/${WG_PREFIXLEN}
|
||||
OBFUSCATOR_KEY=$OBF_KEY
|
||||
OBFUSCATOR_PORTS=$OBF_PORTS
|
||||
MAIN_SERVER=$MAIN_SERVER
|
||||
MAIN_PROTO=$MAIN_PROTO
|
||||
MAIN_API_KEY=$MAIN_API_KEY
|
||||
MAIN_PORT=$MAIN_PORT
|
||||
ROLE=secondary
|
||||
@@ -269,13 +287,17 @@ EOF
|
||||
|
||||
# ── 6. Mini-API ──
|
||||
echo "[6/7] Setting up mini-API..."
|
||||
# Mini-API (fetched from PCA — includes /api/router-config for tunnel pull)
|
||||
# Mini-API (fetched from the main panel first, then PCA GitHub fallback)
|
||||
PCA_BRANCH="${PCA_BRANCH:-main}"
|
||||
RAW="https://raw.githubusercontent.com/andrey271192/PCA_Phobos/${PCA_BRANCH}"
|
||||
if [ -n "${GH_TOKEN:-}" ]; then
|
||||
curl -fsSL -H "Authorization: token $GH_TOKEN" "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
else
|
||||
curl -fsSL "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
PCA_REPO="${PCA_REPO:-andrey271192/PCA_Phobos}"
|
||||
RAW="${PCA_RAW_BASE:-https://raw.githubusercontent.com/${PCA_REPO}/${PCA_BRANCH}}"
|
||||
PANEL_BOOTSTRAP="${MAIN_PROTO}://${MAIN_SERVER}:${MAIN_PORT}/server/api.py"
|
||||
if ! curl -kfsSL "$PANEL_BOOTSTRAP" -o "$PHOBOS_DIR/server/api.py"; then
|
||||
if [ -n "${GH_TOKEN:-}" ]; then
|
||||
curl -fsSL -H "Authorization: token $GH_TOKEN" "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
else
|
||||
curl -fsSL "$RAW/server/api.py" -o "$PHOBOS_DIR/server/api.py"
|
||||
fi
|
||||
fi
|
||||
[ -s "$PHOBOS_DIR/server/api.py" ] || { echo "ERROR: api.py fetch failed"; exit 1; }
|
||||
|
||||
@@ -322,7 +344,7 @@ echo ""
|
||||
echo "Backups (if any) in $PHOBOS_DIR/backup/ will be KEPT."
|
||||
echo ""
|
||||
echo "Press Enter to continue or Ctrl+C to abort..."
|
||||
read -r < /dev/tty 2>/dev/null || true
|
||||
[ -t 0 ] && read -r < /dev/tty 2>/dev/null || true
|
||||
|
||||
echo "Stopping services..."
|
||||
# Stop obfuscator instances
|
||||
@@ -398,7 +420,7 @@ REG_DATA=$(cat << EOF
|
||||
}
|
||||
EOF
|
||||
)
|
||||
curl -s -X POST "http://${MAIN_SERVER}:${MAIN_PORT}/api/servers/register" \
|
||||
curl -ks -X POST "${MAIN_PROTO}://${MAIN_SERVER}:${MAIN_PORT}/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)"
|
||||
|
||||
Reference in New Issue
Block a user