mirror of
https://github.com/andrey271192/PCA_Phobos.git
synced 2026-09-20 11:55:32 +00:00
419 lines
14 KiB
Bash
Executable File
419 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
source "$(dirname "${BASH_SOURCE[0]}")/lib-core.sh"
|
||
|
||
check_root
|
||
load_env
|
||
ensure_dirs
|
||
|
||
# server.env stores OBFUSCATOR_PORTS (plural, comma list). Client obfuscator
|
||
# must target an ACTUAL listening port. lib-core load_env defaults OBFUSCATOR_PORT
|
||
# to 51821 (NOT listened on) -> handshake failed. Override unconditionally from
|
||
# the real port list when present.
|
||
[ -n "${OBFUSCATOR_PORTS:-}" ] && export OBFUSCATOR_PORT="$(echo "$OBFUSCATOR_PORTS" | cut -d',' -f1)"
|
||
|
||
CMD="${1:-help}"
|
||
CLIENT_ARG="${2:-}"
|
||
EXTRA_ARG="${3:-}"
|
||
|
||
resolve_client() {
|
||
local name="$1"
|
||
local id=$(echo "$name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
|
||
|
||
if [[ -d "$CLIENTS_DIR/$id" ]]; then
|
||
echo "$id"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
action_add() {
|
||
local name="$CLIENT_ARG"
|
||
local manual_ip="$EXTRA_ARG"
|
||
|
||
if [[ -z "$name" ]]; then die "Использование: $0 add <client_name> [ip]"; fi
|
||
|
||
local id=$(echo "$name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
|
||
local dir="$CLIENTS_DIR/$id"
|
||
|
||
if [[ -d "$dir" ]]; then die "Клиент $id уже существует."; fi
|
||
|
||
if [[ -z "$SERVER_WG_PUBLIC_KEY" ]]; then
|
||
die "Публичный ключ сервера не найден в server.env. Запустите установку."
|
||
fi
|
||
|
||
local server_pub="$SERVER_WG_PUBLIC_KEY"
|
||
local server_ip_v4="${SERVER_PUBLIC_IP_V4:-}"
|
||
local server_ip_v6="${SERVER_PUBLIC_IP_V6:-}"
|
||
|
||
local client_ip_v4="$manual_ip"
|
||
local ipv4_prefix_main=$(echo "${SERVER_WG_IPV4_NETWORK:-10.25.0.0/16}" | cut -d'/' -f1 | cut -d'.' -f1-2)
|
||
local ipv6_prefix_main=$(echo "${SERVER_WG_IPV6_NETWORK:-fd00:10:25::/48}" | cut -d'/' -f1 | sed 's/::.*//')
|
||
|
||
if [[ -z "$client_ip_v4" ]]; then
|
||
log_info "Поиск свободного IP..."
|
||
declare -A used_ips
|
||
|
||
for d in "$CLIENTS_DIR"/*; do
|
||
if [[ -d "$d" ]] && [[ -f "$d/metadata.json" ]]; then
|
||
local ip=$(jq -r '.tunnel_ip_v4 // empty' "$d/metadata.json" 2>/dev/null)
|
||
[[ -n "$ip" ]] && used_ips["$ip"]=1
|
||
fi
|
||
done
|
||
|
||
used_ips["${ipv4_prefix_main}.0.1"]=1
|
||
|
||
local found=false
|
||
for oct3 in {0..255}; do
|
||
local start_oct4=2
|
||
for oct4 in $(seq $start_oct4 254); do
|
||
local candidate="${ipv4_prefix_main}.${oct3}.${oct4}"
|
||
if [[ -z "${used_ips[$candidate]:-}" ]]; then
|
||
client_ip_v4="$candidate"
|
||
found=true
|
||
break 2
|
||
fi
|
||
done
|
||
done
|
||
|
||
[[ "$found" == "false" ]] && die "Нет свободных IP в подсети."
|
||
fi
|
||
|
||
local oct3=$(echo "$client_ip_v4" | cut -d. -f3)
|
||
local oct4=$(echo "$client_ip_v4" | cut -d. -f4)
|
||
local hex_part=$(printf "%x:%x" "$oct3" "$oct4")
|
||
local client_ip_v6=""
|
||
[[ -n "$server_ip_v6" ]] && client_ip_v6="${ipv6_prefix_main}::${hex_part}"
|
||
|
||
log_info "Назначен IP: $client_ip_v4 $([[ -n $client_ip_v6 ]] && echo "/ $client_ip_v6")"
|
||
|
||
mkdir -p "$dir"
|
||
umask 077
|
||
wg genkey > "$dir/client_private.key"
|
||
wg pubkey < "$dir/client_private.key" > "$dir/client_public.key"
|
||
local priv_key=$(cat "$dir/client_private.key")
|
||
local pub_key=$(cat "$dir/client_public.key")
|
||
|
||
local allowed_ips="0.0.0.0/0"
|
||
local addr_str="$client_ip_v4/32"
|
||
if [[ -n "$client_ip_v6" ]]; then
|
||
allowed_ips="0.0.0.0/0, ::/0"
|
||
addr_str="$client_ip_v4/32, $client_ip_v6/128"
|
||
fi
|
||
|
||
cat > "$dir/${id}.conf" <<EOF
|
||
[Interface]
|
||
PrivateKey = $priv_key
|
||
Address = $addr_str
|
||
DNS = 1.1.1.1, 8.8.8.8
|
||
MTU = 1420
|
||
|
||
[Peer]
|
||
PublicKey = $server_pub
|
||
Endpoint = 127.0.0.1:${CLIENT_WG_PORT:-13255}
|
||
AllowedIPs = $allowed_ips
|
||
PersistentKeepalive = 25
|
||
EOF
|
||
chmod 600 "$dir/${id}.conf"
|
||
|
||
cat > "$dir/wg-obfuscator.conf" <<EOF
|
||
[instance]
|
||
source-if = 127.0.0.1
|
||
source-lport = ${CLIENT_WG_PORT:-13255}
|
||
target = $SERVER_PUBLIC_IP_V4:${OBFUSCATOR_PORT:-51821}
|
||
key = ${OBFUSCATOR_KEY:-KEY}
|
||
masking = ${OBFUSCATOR_MASKING:-AUTO}
|
||
verbose = INFO
|
||
idle-timeout = ${OBFUSCATOR_IDLE:-300}
|
||
max-dummy = ${OBFUSCATOR_DUMMY:-4}
|
||
EOF
|
||
chmod 600 "$dir/wg-obfuscator.conf"
|
||
|
||
cat > "$dir/metadata.json" <<EOF
|
||
{
|
||
"client_id": "$id",
|
||
"client_name": "$name",
|
||
"tunnel_ip_v4": "$client_ip_v4",
|
||
"tunnel_ip_v6": "$client_ip_v6",
|
||
"public_key": "$pub_key",
|
||
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||
"obfuscator_key": "${OBFUSCATOR_KEY:-}",
|
||
"obfuscator_dummy": "${OBFUSCATOR_DUMMY:-4}",
|
||
"obfuscator_idle": "${OBFUSCATOR_IDLE:-300}",
|
||
"server_ip_v4": "$SERVER_PUBLIC_IP_V4",
|
||
"server_port": "${OBFUSCATOR_PORT:-}"
|
||
}
|
||
EOF
|
||
chmod 600 "$dir/metadata.json"
|
||
|
||
local peer_ips="$client_ip_v4/32"
|
||
[[ -n "$client_ip_v6" ]] && peer_ips="$peer_ips, $client_ip_v6/128"
|
||
|
||
cat >> "$WG_CONFIG" <<EOF
|
||
|
||
[Peer]
|
||
PublicKey = $pub_key
|
||
AllowedIPs = $peer_ips
|
||
EOF
|
||
|
||
wg syncconf wg0 <(wg-quick strip wg0 2>/dev/null) 2>/dev/null
|
||
log_success "Клиент $name создан."
|
||
|
||
CLIENT_ARG="$id"
|
||
action_package
|
||
action_link
|
||
}
|
||
|
||
action_remove() {
|
||
local id=$(resolve_client "$CLIENT_ARG")
|
||
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
|
||
local dir="$CLIENTS_DIR/$id"
|
||
|
||
log_info "Удаление клиента $id..."
|
||
|
||
if [[ -f "$dir/client_public.key" ]]; then
|
||
local pub=$(cat "$dir/client_public.key")
|
||
if grep -qF "$pub" "$WG_CONFIG"; then
|
||
awk -v key="$pub" '
|
||
BEGIN {RS=""; ORS="\n\n"}
|
||
index($0, key) == 0 {print $0}
|
||
' "$WG_CONFIG" > "$WG_CONFIG.tmp" && mv "$WG_CONFIG.tmp" "$WG_CONFIG"
|
||
|
||
sed -i '/^$/N;/^\n$/D' "$WG_CONFIG"
|
||
|
||
wg syncconf wg0 <(wg-quick strip wg0 2>/dev/null) 2>/dev/null
|
||
log_success "Peer удален из конфигурации."
|
||
fi
|
||
fi
|
||
|
||
rm -rf "$dir"
|
||
rm -f "$PACKAGES_DIR/phobos-$id.tar.gz"
|
||
|
||
if [[ -f "$TOKENS_FILE" ]] && command -v jq >/dev/null; then
|
||
local tokens=$(jq -r ".[] | select(.client == \"$id\") | .token" "$TOKENS_FILE")
|
||
for t in $tokens; do
|
||
rm -f "$WWW_DIR/init/$t.sh"
|
||
rm -rf "$WWW_DIR/packages/$t"
|
||
done
|
||
jq "map(select(.client != \"$id\"))" "$TOKENS_FILE" > "$TOKENS_FILE.tmp" && mv "$TOKENS_FILE.tmp" "$TOKENS_FILE"
|
||
fi
|
||
|
||
log_success "Клиент $id полностью удален."
|
||
}
|
||
|
||
action_package() {
|
||
local id=$(resolve_client "$CLIENT_ARG")
|
||
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
|
||
|
||
log_info "Сборка пакета для $id..."
|
||
local dir="$CLIENTS_DIR/$id"
|
||
local tmp=$(mktemp -d)
|
||
local pkg_root="$tmp/phobos-$id"
|
||
|
||
mkdir -p "$pkg_root/bin"
|
||
|
||
cp "$dir/${id}.conf" "$pkg_root/${id}.conf"
|
||
cp "$dir/wg-obfuscator.conf" "$pkg_root/wg-obfuscator.conf"
|
||
|
||
# Fetch missing router-arch binaries from ClusterM on-demand
|
||
local _tag; _tag=$(curl -fsSL -m10 https://api.github.com/repos/ClusterM/wg-obfuscator/releases/latest 2>/dev/null | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\(.*\)".*//')
|
||
local _cb="https://github.com/ClusterM/wg-obfuscator/releases/download/${_tag}"
|
||
declare -A _am=([aarch64]="linux-arm64" [mipsel]="linux-mipsel-mips32" [mips]="linux-mips-mips32" [armv7]="linux-armv7-hf" [x86_64]="linux-x64")
|
||
for _arch in aarch64 mipsel mips armv7 x86_64; do
|
||
if [[ ! -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && [[ -n "$_tag" ]]; then
|
||
local _t; _t=$(mktemp -d)
|
||
if curl -fsSL -m30 "${_cb}/wg-obfuscator-${_tag}-${_am[$_arch]}.tar.gz" -o "$_t/o.tgz" 2>/dev/null; then
|
||
tar -xzf "$_t/o.tgz" -C "$_t" 2>/dev/null || true
|
||
local _b; _b=$(find "$_t" -name "wg-obfuscator" -type f | head -1)
|
||
[[ -n "$_b" ]] && cp "$_b" "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && chmod +x "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" && log_info "+wg-obfuscator-$_arch (ClusterM ${_tag})"
|
||
fi
|
||
rm -rf "$_t"
|
||
fi
|
||
[[ -f "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" ]] && cp "$PHOBOS_DIR/bin/wg-obfuscator-$_arch" "$pkg_root/bin/"
|
||
done
|
||
|
||
local tpl_dir="$REPO_DIR/client/templates"
|
||
|
||
if [[ -d "$tpl_dir" ]]; then
|
||
cp "$tpl_dir/install-router.sh.template" "$pkg_root/install-router.sh"
|
||
sed -i "s|{{CLIENT_NAME}}|${id}|g" "$pkg_root/install-router.sh"
|
||
chmod +x "$pkg_root/install-router.sh"
|
||
[[ -f "$tpl_dir/lib-client.sh" ]] && cp "$tpl_dir/lib-client.sh" "$pkg_root/lib-client.sh"
|
||
[[ -f "$tpl_dir/install-obfuscator.sh" ]] && cp "$tpl_dir/install-obfuscator.sh" "$pkg_root/install-obfuscator.sh"
|
||
[[ -f "$tpl_dir/install-wireguard.sh" ]] && cp "$tpl_dir/install-wireguard.sh" "$pkg_root/install-wireguard.sh"
|
||
for f in router-configure-wireguard router-configure-wireguard-openwrt phobos-uninstall 3xui; do
|
||
[[ -f "$tpl_dir/$f.sh" ]] && cp "$tpl_dir/$f.sh" "$pkg_root/$f.sh" && chmod +x "$pkg_root/$f.sh"
|
||
done
|
||
else
|
||
log_warn "Шаблоны не найдены в $tpl_dir"
|
||
fi
|
||
|
||
echo "Phobos Client Package for $id" > "$pkg_root/README.txt"
|
||
echo "Date: $(date)" >> "$pkg_root/README.txt"
|
||
|
||
# Health monitor + failover
|
||
if [[ -f "$PHOBOS_DIR/server/phobos-health.sh" ]]; then
|
||
cp "$PHOBOS_DIR/server/phobos-health.sh" "$pkg_root/phobos-health.sh"
|
||
chmod +x "$pkg_root/phobos-health.sh"
|
||
fi
|
||
# Pull agent: panel -> router config sync (NAT-friendly). Token = panel
|
||
# server_api_key (the /api/router-config endpoint accepts it as a fallback).
|
||
for src in "$tpl_dir/phobos-pull.sh" "$PHOBOS_DIR/server/phobos-pull.sh"; do
|
||
if [[ -f "$src" ]]; then cp "$src" "$pkg_root/phobos-pull.sh"; chmod +x "$pkg_root/phobos-pull.sh"; break; fi
|
||
done
|
||
local PULL_TOKEN=""
|
||
command -v jq >/dev/null && PULL_TOKEN=$(jq -r '.server_api_key // empty' /opt/phobos-panel/settings.json 2>/dev/null)
|
||
[[ -n "$PULL_TOKEN" ]] && echo "$PULL_TOKEN" > "$pkg_root/pull_token"
|
||
# Generate failover.conf with current server data
|
||
source "$PHOBOS_DIR/server/server.env"
|
||
cat > "$pkg_root/failover.conf" <<FAILEOF
|
||
# Phobos Failover Configuration
|
||
SERVER_1=${SERVER_PUBLIC_IP_V4}:${OBFUSCATOR_PORTS:-51821,51822,51823}
|
||
KEY_1=${OBFUSCATOR_KEY}
|
||
FAILEOF
|
||
|
||
find "$pkg_root" -type f ! -path "*/bin/*" -exec sed -i 's/\r$//' {} \;
|
||
|
||
tar -C "$tmp" -czf "$PACKAGES_DIR/phobos-$id.tar.gz" "phobos-$id"
|
||
rm -rf "$tmp"
|
||
|
||
log_success "Пакет создан: $PACKAGES_DIR/phobos-$id.tar.gz"
|
||
}
|
||
|
||
action_check() {
|
||
local id=$(resolve_client "$CLIENT_ARG")
|
||
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
|
||
|
||
local dir="$CLIENTS_DIR/$id"
|
||
local changes=()
|
||
|
||
if [[ ! -f "$dir/metadata.json" ]]; then
|
||
echo "metadata_missing"
|
||
return 1
|
||
fi
|
||
|
||
local client_server_ip=$(jq -r '.server_ip_v4 // ""' "$dir/metadata.json")
|
||
local client_obf_key=$(jq -r '.obfuscator_key // ""' "$dir/metadata.json")
|
||
local client_obf_port=$(jq -r '.server_port // ""' "$dir/metadata.json")
|
||
local client_obf_dummy=$(jq -r '.obfuscator_dummy // "4"' "$dir/metadata.json")
|
||
local client_obf_idle=$(jq -r '.obfuscator_idle // "300"' "$dir/metadata.json")
|
||
|
||
local client_wg_pubkey=""
|
||
if [[ -f "$dir/${id}.conf" ]]; then
|
||
client_wg_pubkey=$(grep "^PublicKey" "$dir/${id}.conf" | cut -d'=' -f2- | tr -d ' ')
|
||
fi
|
||
|
||
[[ "$client_server_ip" != "$SERVER_PUBLIC_IP_V4" ]] && changes+=("IP сервера: $client_server_ip -> $SERVER_PUBLIC_IP_V4")
|
||
[[ "$client_obf_key" != "$OBFUSCATOR_KEY" ]] && changes+=("Ключ обфускатора: изменен")
|
||
[[ "$client_obf_port" != "$OBFUSCATOR_PORT" ]] && changes+=("Порт обфускатора: $client_obf_port -> $OBFUSCATOR_PORT")
|
||
[[ "$client_obf_dummy" != "$OBFUSCATOR_DUMMY" ]] && changes+=("Max dummy: изменен")
|
||
[[ "$client_obf_idle" != "$OBFUSCATOR_IDLE" ]] && changes+=("Idle таймаут: изменен")
|
||
[[ -n "$client_wg_pubkey" && "$client_wg_pubkey" != "$SERVER_WG_PUBLIC_KEY" ]] && changes+=("Публичный ключ WG: изменен")
|
||
|
||
if [[ ${#changes[@]} -gt 0 ]]; then
|
||
echo "ИЗМЕНЕНИЯ КОНФИГУРАЦИИ:"
|
||
for c in "${changes[@]}"; do
|
||
echo " - $c"
|
||
done
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
action_link() {
|
||
local id=$(resolve_client "$CLIENT_ARG")
|
||
if [[ -z "$id" ]]; then die "Клиент не найден."; fi
|
||
local ttl="${EXTRA_ARG:-$TOKEN_TTL}"
|
||
|
||
if ! command -v jq >/dev/null; then die "jq не установлен. Установите: apt-get install jq"; fi
|
||
|
||
local token=$(head -c 16 /dev/urandom | md5sum | cut -d' ' -f1)
|
||
local exp=$(($(date +%s) + ttl))
|
||
|
||
if [[ ! -f "$TOKENS_FILE" ]]; then
|
||
echo "[]" > "$TOKENS_FILE"
|
||
fi
|
||
|
||
local clean_json=$(jq "map(select(.client != \"$id\"))" "$TOKENS_FILE")
|
||
echo "$clean_json" | jq ". + [{\"client\": \"$id\", \"token\": \"$token\", \"expires\": $exp}]" > "$TOKENS_FILE.tmp" && mv "$TOKENS_FILE.tmp" "$TOKENS_FILE"
|
||
|
||
local link_dir="$WWW_DIR/packages/$token"
|
||
rm -rf "$link_dir"
|
||
mkdir -p "$link_dir"
|
||
ln -s "$PACKAGES_DIR/phobos-$id.tar.gz" "$link_dir/phobos-$id.tar.gz"
|
||
|
||
mkdir -p "$WWW_DIR/init"
|
||
local script_url="http://${SERVER_PUBLIC_IP_V4}:${HTTP_PORT:-80}/packages/$token/phobos-$id.tar.gz"
|
||
|
||
cat > "$WWW_DIR/init/$token.sh" <<EOF
|
||
#!/bin/sh
|
||
url="$script_url"
|
||
dir="/tmp/phobos_install_\$\$"
|
||
mkdir -p "\$dir"
|
||
echo "Downloading..."
|
||
if command -v curl >/dev/null; then
|
||
curl -L -s -o "\$dir/package.tar.gz" "\$url"
|
||
else
|
||
wget -q -O "\$dir/package.tar.gz" "\$url"
|
||
fi
|
||
if [ ! -f "\$dir/package.tar.gz" ]; then echo "Download failed"; exit 1; fi
|
||
cd "\$dir"
|
||
tar xzf package.tar.gz
|
||
cd "phobos-$id"
|
||
chmod +x install-router.sh
|
||
./install-router.sh
|
||
EOF
|
||
|
||
# nginx (www-data) must read these; the builder runs with a strict root umask
|
||
# (files 600 / dirs 700) which makes the install URL return 403. Fix perms.
|
||
chmod 755 "$WWW_DIR" "$WWW_DIR/init" "$WWW_DIR/packages" "$link_dir" 2>/dev/null
|
||
chmod 644 "$WWW_DIR/init/$token.sh" 2>/dev/null
|
||
chmod 755 "$PACKAGES_DIR" 2>/dev/null
|
||
chmod 644 "$PACKAGES_DIR/phobos-$id.tar.gz" 2>/dev/null
|
||
|
||
local cmd="curl -s http://${SERVER_PUBLIC_IP_V4}:${HTTP_PORT:-80}/init/$token.sh | sh"
|
||
|
||
echo ""
|
||
echo "=================================================="
|
||
echo "КОМАНДА ДЛЯ УСТАНОВКИ (Действительна $(($ttl / 3600))ч)"
|
||
echo "=================================================="
|
||
echo "$cmd"
|
||
echo "=================================================="
|
||
echo ""
|
||
}
|
||
|
||
action_list() {
|
||
printf "% -20s % -20s % -20s\n" "CLIENT ID" "IPv4" "CREATED"
|
||
echo "------------------------------------------------------------"
|
||
for d in "$CLIENTS_DIR"/*; do
|
||
if [[ -d "$d" ]]; then
|
||
local id=$(basename "$d")
|
||
local ip="N/A"
|
||
local date="N/A"
|
||
if [[ -f "$d/metadata.json" ]]; then
|
||
ip=$(jq -r '.tunnel_ip_v4 // "N/A"' "$d/metadata.json")
|
||
date=$(jq -r '.created_at // "N/A"' "$d/metadata.json" | cut -d'T' -f1)
|
||
fi
|
||
printf "% -20s % -20s % -20s\n" "$id" "$ip" "$date"
|
||
fi
|
||
done
|
||
}
|
||
|
||
case "$CMD" in
|
||
add) action_add ;;
|
||
remove) action_remove ;;
|
||
package) action_package ;;
|
||
link) action_link ;;
|
||
check) action_check ;;
|
||
list) action_list ;;
|
||
rebuild)
|
||
action_remove
|
||
action_add
|
||
;;
|
||
*)
|
||
echo "Usage: $0 {add|remove|list|package|link|check|rebuild}"
|
||
exit 1
|
||
;;
|
||
esac
|