release 1.5.0: one panel in all channels; Apple apps (iOS, new macOS tab) only in beta/dev
- stable gets the beta panel and server features (new design, presets, firewall, monthly traffic, router failover over all ports/addresses + TCP, altip, HR tab, Windows tab, update banner); ios/ macos/ windows/ sources stay out of stable - menu item "Update to N" under Settings when a new version is out - mobile: top menu no longer stretches to 200px - urllib User-Agent: Cloudflare in front of git.bobyrev.com returns 403 to Python-urllib - docs: GitHub adleibzon-rgb as source, password/uninstall/devices sections, new screenshots, PORTS.md TCP fallback and second IP Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,48 @@ def speedtest():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
# --- Google geo (09.30): how Google sees the server's addresses ---------------
|
||||
GOOGLE_CLOSED = {"Russia", "Belarus", "China", "Iran", "Hong Kong", "North Korea", "Cuba", "Syria"}
|
||||
|
||||
|
||||
def _google_country(*args):
|
||||
"""«Country version» at the bottom of policies.google.com/terms = the country Google assigns to the address."""
|
||||
import re as _re
|
||||
import subprocess as _sp
|
||||
try:
|
||||
html = _sp.run(["curl", *args, "-s", "-m", "15", "https://policies.google.com/terms?hl=en"],
|
||||
capture_output=True, text=True, timeout=25).stdout
|
||||
m = _re.search(r"Country version:\s*</a>\s*([A-Za-z][A-Za-z .'-]*?)\s*<", html)
|
||||
return m.group(1) if m else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def google_geo():
|
||||
"""v4/v6 = default route (what clients exit with); ips = every public IPv4 on the server."""
|
||||
import re as _re
|
||||
import subprocess as _sp
|
||||
import time as _t
|
||||
out = {"v4": _google_country("-4"), "v6": _google_country("-6"), "ips": {}, "ts": int(_t.time())}
|
||||
try:
|
||||
for line in _sp.run(["ip", "-4", "-o", "addr", "show", "scope", "global"], capture_output=True,
|
||||
text=True, timeout=10).stdout.splitlines():
|
||||
ip = line.split()[3].split("/")[0]
|
||||
if not _re.match(r"^(10|127|172\.(1[6-9]|2\d|3[01])|192\.168|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7]))\.", ip):
|
||||
out["ips"][ip] = _google_country("-4", "--interface", ip)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
@app.route("/api/google-geo")
|
||||
def api_google_geo():
|
||||
if not check_api_key():
|
||||
return jsonify({"error": "unauthorized"}), 401
|
||||
return jsonify(google_geo())
|
||||
|
||||
|
||||
@app.route("/api/limits", methods=["POST"])
|
||||
def set_limits():
|
||||
"""Per-client speed limits pushed by the main panel ("<tunnel_ip> <mbit>" lines)."""
|
||||
|
||||
43
server/phobos-altip.sh
Executable file
43
server/phobos-altip.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
# PCA Phobos — второй/запасной IP сервера (09.33).
|
||||
# wg-obfuscator и udptcp слушают 0.0.0.0, но отвечают с основного IP, а провайдер
|
||||
# клиента может резать именно основной. DNAT «второй IP:порт → основной IP:порт»
|
||||
# заставляет ответ уходить с того адреса, на который пришёл запрос.
|
||||
# phobos-altip apply — правила для всех доп. IPv4 сервера (служба делает это при старте)
|
||||
# phobos-altip status — что настроено
|
||||
# phobos-altip enable — поставить службу phobos-altip
|
||||
ENV=/opt/Phobos/server/server.env
|
||||
[ -f "$ENV" ] || ENV=/opt/Phobos/server.env
|
||||
get() { grep -E "^$1=" "$ENV" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'"'"; }
|
||||
MAIN=$(get SERVER_PUBLIC_IP_V4)
|
||||
PORTS=$(get OBFUSCATOR_PORTS); [ -n "$PORTS" ] || PORTS=2083,5443,993
|
||||
TCP=$(get TCP_PORT)
|
||||
alts() { ip -4 -o addr show scope global | awk '{split($4,a,"/"); print a[1]}' | grep -vxF "$MAIN" | grep -v '^10\.\|^172\.\(1[6-9]\|2[0-9]\|3[01]\)\.\|^192\.168\.'; }
|
||||
rule() { # $1 proto $2 alt $3 port
|
||||
iptables -t nat -C PREROUTING -d "$2" -p "$1" --dport "$3" -j DNAT --to-destination "$MAIN:$3" 2>/dev/null \
|
||||
|| iptables -t nat -I PREROUTING -d "$2" -p "$1" --dport "$3" -j DNAT --to-destination "$MAIN:$3"
|
||||
}
|
||||
case "$1" in
|
||||
apply)
|
||||
[ -n "$MAIN" ] || { echo "SERVER_PUBLIC_IP_V4 not set"; exit 1; }
|
||||
for a in $(alts); do
|
||||
for p in $(echo "$PORTS" | tr ',' ' '); do rule udp "$a" "$p"; done
|
||||
[ -n "$TCP" ] && rule tcp "$a" "$TCP"
|
||||
echo "alt $a -> $MAIN (udp $PORTS${TCP:+, tcp $TCP})"
|
||||
done ;;
|
||||
status) echo "main: $MAIN"; echo "alt: $(alts | tr '\n' ' ')"; iptables -t nat -S PREROUTING | grep -- "--to-destination $MAIN:" ;;
|
||||
enable)
|
||||
cat > /etc/systemd/system/phobos-altip.service <<UNIT
|
||||
[Unit]
|
||||
Description=PCA Phobos: second IP answers from itself (DNAT)
|
||||
After=network-online.target
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/sh $(readlink -f "$0") apply
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
systemctl daemon-reload && systemctl enable --now phobos-altip ;;
|
||||
*) echo "usage: $0 apply|status|enable"; exit 1 ;;
|
||||
esac
|
||||
@@ -6,11 +6,19 @@
|
||||
# ============================================================
|
||||
|
||||
PHOBOS_DIR="/opt/etc/Phobos"
|
||||
CONF="$PHOBOS_DIR/failover.conf"
|
||||
STATE="$PHOBOS_DIR/state"
|
||||
LOG="$PHOBOS_DIR/health.log"
|
||||
# 02.11.2026: несколько учёток на одном роутере. Файлы учётки (failover.conf, state,
|
||||
# client_id, panel_*) — в PHOBOS_ACC (доп. учётки: $PHOBOS_DIR/acc/<клиент>),
|
||||
# главная — прямо в $PHOBOS_DIR. Конфиги обфускаторов общие, свой ищем по *.link.
|
||||
ACC_DIR="${PHOBOS_ACC:-$PHOBOS_DIR}"
|
||||
CONF="$ACC_DIR/failover.conf"
|
||||
STATE="$ACC_DIR/state"
|
||||
LOG="$ACC_DIR/health.log"
|
||||
CID=$(cat "$ACC_DIR/client_id" 2>/dev/null)
|
||||
OBF_CONF="$PHOBOS_DIR/wg-obfuscator.conf"
|
||||
LOCKFILE="/tmp/phobos-health.lock"
|
||||
for l in "$PHOBOS_DIR"/wg-obfuscator*.link; do
|
||||
[ -n "$CID" ] && [ "$(cat "$l" 2>/dev/null)" = "$CID" ] && OBF_CONF="${l%.link}.conf" && break
|
||||
done
|
||||
LOCKFILE="/tmp/phobos-health${PHOBOS_ACC:+-$CID}.lock"
|
||||
WG_IF="Wireguard3"
|
||||
|
||||
MAX_LOG_LINES=200
|
||||
@@ -25,9 +33,9 @@ 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_SCHEME="${PRIMARY_PROBE_SCHEME:-$(cat "$PHOBOS_DIR/panel_scheme" 2>/dev/null)}"
|
||||
PRIMARY_PROBE_SCHEME="${PRIMARY_PROBE_SCHEME:-$(cat "$ACC_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:-$(cat "$ACC_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)
|
||||
@@ -55,6 +63,16 @@ mkdir -p "$STATE"
|
||||
|
||||
# ── Detect WG interface ──
|
||||
detect_wg_interface() {
|
||||
# точное совпадение "Phobos-<клиент>"; доп. учётка не должна схватить чужой интерфейс
|
||||
for i in 0 1 2 3 4 5 6 7 8 9; do
|
||||
desc=$(ndmc -c "show interface Wireguard${i}" 2>/dev/null | grep description | head -1)
|
||||
# HydraRoute Neo names the interface by the bare client id (09.33)
|
||||
if [ -n "$CID" ] && echo "$desc" | grep -qE "(Phobos-|: )${CID}\$"; then
|
||||
WG_IF="Wireguard${i}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
[ -n "$PHOBOS_ACC" ] && return 1
|
||||
for i in 0 1 2 3 4 5 6 7 8 9; do
|
||||
desc=$(ndmc -c "show interface Wireguard${i}" 2>/dev/null | grep description | head -1)
|
||||
if echo "$desc" | grep -qi phobos; then
|
||||
@@ -65,6 +83,19 @@ detect_wg_interface() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Restart own obfuscator (init script whose ARGS name our config) ──
|
||||
restart_obf() {
|
||||
sc=$(grep -l "/$(basename "$OBF_CONF")\"\$" /opt/etc/init.d/S*obfuscator* 2>/dev/null | head -1)
|
||||
[ -z "$sc" ] && [ -f /opt/etc/init.d/S49wg-obfuscator ] && sc=/opt/etc/init.d/S49wg-obfuscator
|
||||
if [ -n "$sc" ]; then
|
||||
"$sc" restart >/dev/null 2>&1
|
||||
else
|
||||
killall wg-obfuscator 2>/dev/null
|
||||
sleep 1
|
||||
wg-obfuscator --config "$OBF_CONF" &
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Read failover config ──
|
||||
if [ ! -f "$CONF" ]; then
|
||||
log "ERROR: no failover.conf"
|
||||
@@ -80,6 +111,7 @@ while true; do
|
||||
eval "SERVER_${idx}_PORTS=$(echo "$val" | cut -d: -f2-)"
|
||||
eval "SERVER_${idx}_KEY=$(grep "^KEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
|
||||
eval "SERVER_${idx}_WGKEY=$(grep "^WGKEY_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
|
||||
eval "SERVER_${idx}_TCP=$(grep "^TCP_${idx}=" "$CONF" 2>/dev/null | cut -d= -f2-)"
|
||||
SERVER_COUNT=$idx
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
@@ -94,14 +126,26 @@ CURRENT_SERVER=$(cat "$STATE/current_server" 2>/dev/null || echo "1")
|
||||
CURRENT_PORT_IDX=$(cat "$STATE/current_port_idx" 2>/dev/null || echo "0")
|
||||
FAIL_COUNT=$(cat "$STATE/fail_count" 2>/dev/null || echo "0")
|
||||
PRIMARY_CHECK_TS=$(cat "$STATE/primary_check_ts" 2>/dev/null || echo "0")
|
||||
TCP_MODE=$(cat "$STATE/tcp_mode" 2>/dev/null || echo "0") # 0 = UDP, N = TCP via server N
|
||||
CYCLES=$(cat "$STATE/udp_cycles" 2>/dev/null || echo "0") # full UDP rounds without success
|
||||
UDPTCP_BIN=/opt/bin/udptcp
|
||||
UDPTCP_LPORT=13256
|
||||
|
||||
# ── Detect Phobos WG interface (non-fatal, fallback to Wireguard3) ──
|
||||
# Нет своего интерфейса — выходим: запасной Wireguard3 бывает чужим туннелем.
|
||||
# ── Detect Phobos WG interface ──
|
||||
# Нет своего интерфейса — выходим: запасной Wireguard3 бывает чужим (так сторож
|
||||
# удалённой доп. учётки вписал пир Phobos в посторонний туннель).
|
||||
detect_wg_interface || {
|
||||
log "ERROR: own WG interface not found, skip"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Panel tunnel IP must go through our tunnel (HydraRoute-made interface has no
|
||||
# such route → pull went to WAN and hung, 09.33) ──
|
||||
PT_IP=$(cat "$ACC_DIR/panel_tunnel_ip" 2>/dev/null)
|
||||
if [ -n "$PT_IP" ] && ip route get "$PT_IP" 2>/dev/null | grep -q "dev ppp\|dev eth\|dev wan"; then
|
||||
ndmc -c "ip route $PT_IP 255.255.255.255 $WG_IF auto" >/dev/null 2>&1 && log "ROUTE: $PT_IP via $WG_IF"
|
||||
fi
|
||||
|
||||
# ── Get handshake age via ndmc ──
|
||||
# A peer switch can leave the removed peer's stale handshake listed in ndmc
|
||||
# output (often a huge sentinel like 2147483647). Take the FRESHEST (minimum
|
||||
@@ -151,9 +195,13 @@ list_iface_keys() {
|
||||
count_server_peers() {
|
||||
present=$(list_iface_keys)
|
||||
n=0
|
||||
seen=" "
|
||||
i=1
|
||||
while [ "$i" -le "$SERVER_COUNT" ]; do
|
||||
eval "wk=\$SERVER_${i}_WGKEY"
|
||||
# one server may have several addresses = same key in several entries
|
||||
case "$seen" in *" $wk "*) i=$((i + 1)); continue ;; esac
|
||||
seen="$seen$wk "
|
||||
if [ -n "$wk" ] && echo "$present" | grep -q "^${wk}$"; then
|
||||
n=$((n + 1))
|
||||
fi
|
||||
@@ -194,7 +242,7 @@ switch_wg_peer() {
|
||||
# 01.11.2026: порт — из конфига обфускатора (было зашито 13255, а установщик
|
||||
# берёт свободный, на части роутеров 51820 → после переключения туннель мёртв).
|
||||
# Через ndmc по шагам: JSON-RCI с allow-ips "::"/0 молча не ставил allow-ips.
|
||||
lport=$(grep 'source-lport' "$PHOBOS_DIR/wg-obfuscator.conf" 2>/dev/null | tr -d ' ' | cut -d'=' -f2)
|
||||
lport=$(grep 'source-lport' "$OBF_CONF" 2>/dev/null | tr -d ' ' | cut -d'=' -f2)
|
||||
lport=${lport:-13255}
|
||||
ndmc -c "interface $WG_IF wireguard peer $new_key" >/dev/null 2>&1
|
||||
ndmc -c "interface $WG_IF wireguard peer $new_key endpoint 127.0.0.1:$lport" >/dev/null 2>&1
|
||||
@@ -226,6 +274,7 @@ switch_endpoint() {
|
||||
fi
|
||||
|
||||
log "SWITCH → server $server_idx ($host:$port)"
|
||||
stop_tcp
|
||||
|
||||
# 1. Switch WG peer key if different server
|
||||
switch_wg_peer "$wg_key"
|
||||
@@ -239,13 +288,7 @@ switch_endpoint() {
|
||||
fi
|
||||
|
||||
# 3. Restart obfuscator
|
||||
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
|
||||
/opt/etc/init.d/S49wg-obfuscator restart >/dev/null 2>&1
|
||||
else
|
||||
killall wg-obfuscator 2>/dev/null
|
||||
sleep 1
|
||||
wg-obfuscator --config "$OBF_CONF" &
|
||||
fi
|
||||
restart_obf
|
||||
|
||||
# 4. Save state (fail_count is managed by the caller, NOT reset here —
|
||||
# resetting on a port-hop would prevent escalation to server failover)
|
||||
@@ -253,6 +296,45 @@ switch_endpoint() {
|
||||
echo "$port_idx" > "$STATE/current_port_idx"
|
||||
}
|
||||
|
||||
# ── TCP channel (09.33/1.5.0): obfuscated UDP inside TCP via local udptcp ──
|
||||
stop_tcp() {
|
||||
[ "$TCP_MODE" = "0" ] && ! pidof udptcp >/dev/null 2>&1 && return 0
|
||||
killall udptcp 2>/dev/null
|
||||
TCP_MODE=0
|
||||
echo 0 > "$STATE/tcp_mode"
|
||||
}
|
||||
|
||||
# start_tcp N — tunnel through server N's TCP port; 1 = started
|
||||
start_tcp() {
|
||||
n=$1
|
||||
eval "host=\$SERVER_${n}_HOST"
|
||||
eval "tport=\$SERVER_${n}_TCP"
|
||||
eval "obf_key=\$SERVER_${n}_KEY"
|
||||
eval "wg_key=\$SERVER_${n}_WGKEY"
|
||||
[ -n "$tport" ] && [ -x "$UDPTCP_BIN" ] || return 1
|
||||
killall udptcp 2>/dev/null
|
||||
"$UDPTCP_BIN" -c "127.0.0.1:${UDPTCP_LPORT}" "${host}:${tport}" >/dev/null 2>&1 &
|
||||
switch_wg_peer "$wg_key"
|
||||
sed -i "s|^target = .*|target = 127.0.0.1:${UDPTCP_LPORT}|" "$OBF_CONF"
|
||||
[ -n "$obf_key" ] && sed -i "s|^key = .*|key = ${obf_key}|" "$OBF_CONF"
|
||||
restart_obf
|
||||
TCP_MODE=$n
|
||||
echo "$n" > "$STATE/tcp_mode"
|
||||
echo "$n" > "$STATE/current_server"
|
||||
log "TCP MODE → server $n (${host}:${tport} over TCP)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# try_tcp_after N — first server after N (no wrap) that has a TCP port; 1 = none
|
||||
try_tcp_after() {
|
||||
i=$(( $1 + 1 ))
|
||||
while [ "$i" -le "$SERVER_COUNT" ]; do
|
||||
start_tcp "$i" && return 0
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Try next port on current server ──
|
||||
try_next_port() {
|
||||
port_count=$(get_port_count "$CURRENT_SERVER")
|
||||
@@ -267,7 +349,9 @@ try_next_port() {
|
||||
# ── Try next server ──
|
||||
try_next_server() {
|
||||
next=$((CURRENT_SERVER + 1))
|
||||
[ "$next" -gt "$SERVER_COUNT" ] && next=1
|
||||
if [ "$next" -gt "$SERVER_COUNT" ]; then
|
||||
next=1
|
||||
fi
|
||||
[ "$next" -eq "$CURRENT_SERVER" ] && return 1
|
||||
|
||||
log "FAILOVER → server $next"
|
||||
@@ -277,7 +361,7 @@ try_next_server() {
|
||||
|
||||
# ── Check if primary is back ──
|
||||
check_primary() {
|
||||
[ "$CURRENT_SERVER" -eq 1 ] && return
|
||||
[ "$CURRENT_SERVER" -eq 1 ] && [ "$TCP_MODE" = "0" ] && return
|
||||
|
||||
now=$(date +%s)
|
||||
elapsed=$((now - PRIMARY_CHECK_TS))
|
||||
@@ -299,7 +383,10 @@ check_primary() {
|
||||
fi
|
||||
done
|
||||
if [ -n "$ok_url" ]; then
|
||||
log "PRIMARY ($ok_url http=200) alive, switching back"
|
||||
# HTTP alive does not mean UDP passes (провайдер режет UDP к IP, 09.33):
|
||||
# пробуем, а если рукопожатия не будет — trial_check вернёт как было.
|
||||
log "PRIMARY ($ok_url http=200) alive, TRIAL switch back"
|
||||
echo "$CURRENT_SERVER $CURRENT_PORT_IDX $TCP_MODE $(date +%s)" > "$STATE/trial"
|
||||
switch_endpoint 1 0
|
||||
echo "0" > "$STATE/fail_count"
|
||||
else
|
||||
@@ -341,6 +428,8 @@ heal_lan_routing() {
|
||||
while read -r mac pol; do
|
||||
[ -z "$mac" ] && continue
|
||||
case "$mac" in \#*) continue;; esac
|
||||
# политику удалили — не воскрешаем привязку к ней
|
||||
echo "$rc" | grep -qx "ip policy $pol" || continue
|
||||
if ! echo "$rc" | grep -q "host $mac policy $pol"; then
|
||||
ndmc -c "ip hotspot host $mac policy $pol" >/dev/null 2>&1
|
||||
log "HEAL: re-bound host $mac -> $pol"
|
||||
@@ -417,12 +506,31 @@ fi
|
||||
AGE=$(get_handshake_age)
|
||||
WAN=$(check_connectivity && echo "yes" || echo "no")
|
||||
|
||||
# ── Trial of the primary: fresh handshake within ~2 min or go back ──
|
||||
if [ -f "$STATE/trial" ]; then
|
||||
read t_srv t_port t_tcp t_ts < "$STATE/trial"
|
||||
if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then
|
||||
log "TRIAL ok: primary works"
|
||||
rm -f "$STATE/trial"; echo 0 > "$STATE/udp_cycles"
|
||||
elif [ $(( $(date +%s) - ${t_ts:-0} )) -ge 110 ]; then
|
||||
rm -f "$STATE/trial"
|
||||
log "TRIAL failed: back to server $t_srv"
|
||||
if [ "${t_tcp:-0}" != "0" ]; then start_tcp "$t_tcp" || switch_endpoint "$t_srv" "$t_port"
|
||||
else switch_endpoint "$t_srv" "$t_port"; fi
|
||||
echo 0 > "$STATE/fail_count"
|
||||
exit 0
|
||||
else
|
||||
exit 0 # give the trial time
|
||||
fi
|
||||
fi
|
||||
|
||||
# Tunnel healthy = fresh handshake
|
||||
if [ "$AGE" -lt "$HANDSHAKE_WARN" ]; then
|
||||
if [ "$FAIL_COUNT" -gt 0 ]; then
|
||||
log "OK: tunnel up (handshake=${AGE}s, server=$CURRENT_SERVER, wan=$WAN)"
|
||||
echo "0" > "$STATE/fail_count"
|
||||
fi
|
||||
echo 0 > "$STATE/udp_cycles"
|
||||
check_primary
|
||||
exit 0
|
||||
fi
|
||||
@@ -438,23 +546,30 @@ log "STALE: handshake=${AGE}s wan=${WAN} server=$CURRENT_SERVER port=$CURRENT_PO
|
||||
# Stage 1 (fail 1): Restart obfuscator
|
||||
if [ "$FAIL_COUNT" -eq 1 ]; then
|
||||
log "ACTION: restart obfuscator"
|
||||
if [ -f /opt/etc/init.d/S49wg-obfuscator ]; then
|
||||
/opt/etc/init.d/S49wg-obfuscator restart >/dev/null 2>&1
|
||||
else
|
||||
killall wg-obfuscator 2>/dev/null
|
||||
sleep 1
|
||||
wg-obfuscator --config "$OBF_CONF" &
|
||||
fi
|
||||
restart_obf
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stage 2 (fail 2): Port hop to an alternate port on the SAME server
|
||||
if [ "$FAIL_COUNT" -eq 2 ]; then
|
||||
# TCP mode went stale → back to the UDP round (next server)
|
||||
if [ "$TCP_MODE" != "0" ] && [ "$FAIL_COUNT" -ge 2 ]; then
|
||||
log "TCP stale on server $TCP_MODE"
|
||||
if try_tcp_after "$TCP_MODE"; then echo 0 > "$STATE/fail_count"; exit 0; fi
|
||||
stop_tcp
|
||||
switch_endpoint 1 0; echo 0 > "$STATE/fail_count"; exit 0
|
||||
fi
|
||||
|
||||
# Stage 2 (fail 2+): hop through ALL ports of the SAME server (was: only one)
|
||||
if [ "$FAIL_COUNT" -ge 2 ]; then
|
||||
if try_next_port; then
|
||||
exit 0
|
||||
fi
|
||||
# only one port → fall through to server failover
|
||||
log "single port, escalate to failover"
|
||||
log "ports of server $CURRENT_SERVER exhausted, escalate to failover"
|
||||
fi
|
||||
|
||||
# Whole UDP round failed (all servers × ports) → TCP, if any server offers it
|
||||
if [ "$CURRENT_SERVER" -eq "$SERVER_COUNT" ]; then
|
||||
CYCLES=$((CYCLES + 1)); echo "$CYCLES" > "$STATE/udp_cycles"
|
||||
if try_tcp_after 0; then echo 0 > "$STATE/fail_count"; exit 0; fi
|
||||
fi
|
||||
|
||||
# Stage 3 (fail 3+): Server failover. Reset fail_count so the new server
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
export PATH=/opt/bin:/opt/sbin:/opt/usr/bin:$PATH # cron без /opt/bin -> busybox wget висел вечно (09.29)
|
||||
# ============================================================
|
||||
# Phobos Config Pull — NAT-friendly management channel.
|
||||
# Runs ON the router via cron (every 2 min). OUTBOUND ONLY:
|
||||
@@ -21,9 +22,11 @@
|
||||
# PANEL env or default below -> panel base URL
|
||||
# ============================================================
|
||||
PHOBOS_DIR="/opt/etc/Phobos"
|
||||
CONF="$PHOBOS_DIR/failover.conf"
|
||||
# 02.11.2026: доп. учётки — PHOBOS_ACC=$PHOBOS_DIR/acc/<клиент> (см. phobos-health.sh)
|
||||
ACC_DIR="${PHOBOS_ACC:-$PHOBOS_DIR}"
|
||||
CONF="$ACC_DIR/failover.conf"
|
||||
HEALTH="$PHOBOS_DIR/phobos-health.sh"
|
||||
LOG="$PHOBOS_DIR/health.log"
|
||||
LOG="$ACC_DIR/health.log"
|
||||
# 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
|
||||
@@ -31,15 +34,18 @@ LOG="$PHOBOS_DIR/health.log"
|
||||
# - 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_TUNNEL_IP="${PANEL_TUNNEL_IP:-$(cat "$PHOBOS_DIR/panel_tunnel_ip" 2>/dev/null)}"
|
||||
PANEL_TUNNEL_IP="${PANEL_TUNNEL_IP:-$(cat "$ACC_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)}"
|
||||
PANEL="${PANEL:-$(cat "$ACC_DIR/panel_url" 2>/dev/null)}"
|
||||
PANEL_URLS="${PANEL_URLS:-$(cat "$ACC_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"
|
||||
LOCK="/tmp/phobos-pull.lock"
|
||||
# + public URLs the panel announced in failover.conf (second IP etc., 09.33)
|
||||
_extra=$(grep "^PANEL_URLS_EXTRA=" "$CONF" 2>/dev/null | cut -d= -f2-)
|
||||
for _u in $_extra; do case " $PANEL_URLS " in *" $_u "*) ;; *) PANEL_URLS="$PANEL_URLS $_u" ;; esac; done
|
||||
CLIENT_ID=$(cat "$ACC_DIR/client_id" 2>/dev/null || echo "home")
|
||||
TOKEN=$(cat "$ACC_DIR/pull_token" 2>/dev/null)
|
||||
TMP="/tmp/failover.conf.pull${PHOBOS_ACC:+-$CLIENT_ID}"
|
||||
LOCK="/tmp/phobos-pull${PHOBOS_ACC:+-$CLIENT_ID}.lock"
|
||||
|
||||
log() { echo "$(date '+%H:%M:%S') $1" >> "$LOG"; }
|
||||
|
||||
@@ -74,8 +80,21 @@ fetch_config() {
|
||||
wget -T 8 --no-check-certificate -q -O "$TMP" "$url" 2>/dev/null || wget -T 8 -q -O "$TMP" "$url" 2>/dev/null
|
||||
}
|
||||
|
||||
# TCP channel for routers (1.5.0): the guard needs /opt/bin/udptcp when failover.conf has TCP_N.
|
||||
ensure_udptcp() {
|
||||
[ -x /opt/bin/udptcp ] && return 0
|
||||
grep -q "^TCP_[0-9]*=" "$CONF" 2>/dev/null || return 0
|
||||
case "$(uname -m)" in aarch64) a=aarch64 ;; mips*) a=mipsel ;; *) return 0 ;; esac
|
||||
command -v curl >/dev/null || return 0
|
||||
{ curl -fsSL -m 30 -o /opt/bin/udptcp.tmp "https://raw.githubusercontent.com/adleibzon-rgb/PCA_Phobos/main/udptcp/bin/udptcp-$a" 2>/dev/null \
|
||||
|| curl -fsSL -m 30 -o /opt/bin/udptcp.tmp "https://git.bobyrev.com/PCA/PCA_Phobos/raw/branch/main/udptcp/bin/udptcp-$a" 2>/dev/null; } \
|
||||
&& [ -s /opt/bin/udptcp.tmp ] && chmod 755 /opt/bin/udptcp.tmp && mv /opt/bin/udptcp.tmp /opt/bin/udptcp \
|
||||
&& log "PULL: udptcp installed ($a)" || rm -f /opt/bin/udptcp.tmp
|
||||
}
|
||||
|
||||
# One fetch + compare + apply pass. Returns 0 always (best-effort).
|
||||
do_pull() {
|
||||
ensure_udptcp
|
||||
# Try each source (tunnel first); accept first that returns a valid conf.
|
||||
got=0
|
||||
for base in $PANEL_URLS; do
|
||||
@@ -102,6 +121,48 @@ do_pull() {
|
||||
if [ "$old_s1" != "$new_s1" ] && [ -f "$HEALTH" ]; then
|
||||
sh "$HEALTH" apply-server 1
|
||||
fi
|
||||
|
||||
apply_obf_params
|
||||
}
|
||||
|
||||
# Apply obfuscator params (masking/max-dummy/idle) delivered in failover.conf.
|
||||
apply_obf_params() {
|
||||
M=$(grep "^OBF_MASKING=" "$CONF" 2>/dev/null | cut -d= -f2-)
|
||||
[ -z "$M" ] && return 0
|
||||
D=$(grep "^OBF_MAX_DUMMY=" "$CONF" 2>/dev/null | cut -d= -f2-)
|
||||
I=$(grep "^OBF_IDLE=" "$CONF" 2>/dev/null | cut -d= -f2-)
|
||||
changed=0
|
||||
# только свой обфускатор: у других учёток свои failover.conf
|
||||
own=""
|
||||
for l in "$PHOBOS_DIR"/wg-obfuscator*.link; do
|
||||
[ "$(cat "$l" 2>/dev/null)" = "$CLIENT_ID" ] && own="${l%.link}.conf"
|
||||
done
|
||||
[ -z "$own" ] && [ -z "$PHOBOS_ACC" ] && own="$PHOBOS_DIR/wg-obfuscator*.conf"
|
||||
for OBF in $own; do
|
||||
[ -f "$OBF" ] || continue
|
||||
cur=$(grep "^masking = " "$OBF" 2>/dev/null | sed 's/masking = //')
|
||||
[ "$cur" != "$M" ] && { sed -i "s/^masking = .*/masking = $M/" "$OBF"; changed=1; }
|
||||
if [ -n "$D" ]; then
|
||||
cur=$(grep "^max-dummy = " "$OBF" 2>/dev/null | sed 's/max-dummy = //')
|
||||
[ "$cur" != "$D" ] && { sed -i "s/^max-dummy = .*/max-dummy = $D/" "$OBF"; changed=1; }
|
||||
fi
|
||||
if [ -n "$I" ]; then
|
||||
cur=$(grep "^idle-timeout = " "$OBF" 2>/dev/null | sed 's/idle-timeout = //')
|
||||
[ "$cur" != "$I" ] && { sed -i "s/^idle-timeout = .*/idle-timeout = $I/" "$OBF"; changed=1; }
|
||||
fi
|
||||
done
|
||||
[ "$changed" = 0 ] && return 0
|
||||
log "PULL: obf params applied (masking=$M dummy=${D:-} idle=${I:-}), restart obfuscator"
|
||||
if ls /opt/etc/init.d/S*obfuscator* >/dev/null 2>&1; then
|
||||
for sc in /opt/etc/init.d/S*obfuscator*; do
|
||||
for OBF in $own; do grep -q "/$(basename "$OBF")\"\$" "$sc" && [ -x "$sc" ] && "$sc" restart >/dev/null 2>&1; done
|
||||
done
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart 'wg-obfuscator*' >/dev/null 2>&1 || true
|
||||
else
|
||||
killall wg-obfuscator 2>/dev/null; sleep 1
|
||||
for OBF in "$PHOBOS_DIR"/wg-obfuscator*.conf; do [ -f "$OBF" ] && wg-obfuscator --config "$OBF" >/dev/null 2>&1 & done
|
||||
fi
|
||||
}
|
||||
|
||||
# Inner loop: cron fires this every 60s, but we poll ~4x per minute so a
|
||||
|
||||
54
server/phobos-v6relay.py
Normal file
54
server/phobos-v6relay.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv6 -> local IPv4 UDP relay for the obfuscator (it speaks IPv4 only).
|
||||
|
||||
Usage: phobos-v6relay.py LISTEN_PORT [TARGET_PORT] [TARGET_HOST]
|
||||
One upstream socket per IPv6 client, so the obfuscator sees a stable
|
||||
client address for the whole session. Idle clients are dropped after 300 s.
|
||||
"""
|
||||
import selectors
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
IDLE = 300
|
||||
|
||||
listen_port = int(sys.argv[1])
|
||||
target = (sys.argv[3] if len(sys.argv) > 3 else "127.0.0.1",
|
||||
int(sys.argv[2]) if len(sys.argv) > 2 else listen_port)
|
||||
|
||||
down = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
down.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
down.bind(("::", listen_port))
|
||||
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(down, selectors.EVENT_READ, None)
|
||||
clients = {} # v6 addr -> [upstream socket, last seen]
|
||||
|
||||
while True:
|
||||
for key, _ in sel.select(timeout=30):
|
||||
if key.data is None:
|
||||
data, addr = down.recvfrom(65535)
|
||||
c = clients.get(addr)
|
||||
if c is None:
|
||||
up = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
up.connect(target)
|
||||
sel.register(up, selectors.EVENT_READ, addr)
|
||||
c = clients[addr] = [up, 0]
|
||||
c[1] = time.monotonic()
|
||||
try:
|
||||
c[0].send(data)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
addr = key.data
|
||||
try:
|
||||
down.sendto(key.fileobj.recv(65535), addr)
|
||||
clients[addr][1] = time.monotonic()
|
||||
except OSError:
|
||||
pass
|
||||
now = time.monotonic()
|
||||
for addr, (up, seen) in list(clients.items()):
|
||||
if now - seen > IDLE:
|
||||
sel.unregister(up)
|
||||
up.close()
|
||||
del clients[addr]
|
||||
Reference in New Issue
Block a user