#!/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 [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" < "$dir/wg-obfuscator.conf" < "$dir/metadata.json" <> "$WG_CONFIG" </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" < $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" </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