#!/bin/sh # rProxy — Менеджер обратного прокси для роутеров Keenetic # Публикация локальных сервисов через SSH-туннели + nginx на VPS # http://5.104.75.50:3000/Petro1990/rProxy VERSION="1.6.4" export PATH="/opt/bin:/opt/sbin:$PATH" CONF_DIR="/opt/etc/rproxy" CONF_FILE="$CONF_DIR/rproxy.conf" SERVICES_DIR="$CONF_DIR/services" VPS_DIR="$CONF_DIR/vps" PID_DIR="/opt/var/run/rproxy" SSH_KEY="$CONF_DIR/id_ed25519" REMOTE_NGINX_DIR="/etc/nginx/sites-enabled" BASE_TUNNEL_PORT=10000 BASE_EXT_PORT=26000 CERTBOT_EMAIL="" # Будет заполнено из конфига # ─── Цвета ─────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' msg() { printf "${GREEN}▸${NC} %s\n" "$*"; } warn() { printf "${YELLOW}⚠${NC} %s\n" "$*"; } err() { printf "${RED}✖${NC} %s\n" "$*" >&2; } header() { printf "\n${CYAN}${BOLD}%s${NC}\n" "$*"; } # ─── Утилиты ───────────────────────────────────────────────────────── clear_screen() { printf "\033[2J\033[H"; } draw_box() { local title="$1" header "$title" } draw_separator() { printf "${DIM}──────────────────────────────────────────────────${NC}\n" } prompt() { printf "\n${CYAN}▸${NC} $1" read -r REPLY } pause() { printf "\n${DIM}Нажмите Enter для продолжения...${NC}" read -r _ } check_conf() { if [ ! -f "$CONF_FILE" ]; then mkdir -p "$CONF_DIR" touch "$CONF_FILE" fi . "$CONF_FILE" # Строгая проверка: ищем хотя бы один файл в vps/, в котором заполнена переменная VPS_HOST for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue if grep -qE "VPS_HOST=['\"]?[^'\" ]+" "$f"; then return 0 fi done return 1 } load_service() { local svc_file="$SERVICES_DIR/$1.conf" if [ ! -f "$svc_file" ]; then err "Сервис '$1' не найден" return 1 fi . "$svc_file" # При загрузке сервиса также загружаем его VPS if [ -n "$SVC_VPS" ]; then load_vps "$SVC_VPS" || return 1 else # Обратная совместимость: если VPS не указан, пробуем default load_vps "default" || return 1 fi } load_vps() { local vps_id="$1" local vps_file="$VPS_DIR/$vps_id.conf" if [ ! -f "$vps_file" ]; then return 1 fi # Сброс старых переменных перед загрузкой VPS_HOST="" VPS_PORT="" VPS_USER="" VPS_AUTH="" VPS_PASS="" . "$vps_file" CUR_VPS_ID="$vps_id" return 0 } migrate_config() { mkdir -p "$VPS_DIR" "$SERVICES_DIR" "$PID_DIR" [ ! -f "$CONF_FILE" ] && touch "$CONF_FILE" # Проверка на "битый" (пустой) конфиг local broken=0 [ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=" "$VPS_DIR/default.conf" && broken=1 if [ -f "$CONF_FILE" ] && { [ ! -f "$VPS_DIR/default.conf" ] || [ "$broken" -eq 1 ]; }; then # Если rproxy.conf ещё содержит данные серверов (не пустой) if grep -q "VPS_HOST=" "$CONF_FILE"; then msg "Восстановление конфигурации VPS..." . "$CONF_FILE" cat > "$VPS_DIR/default.conf" < "$tmp_conf" [ -n "$DEFAULT_AUTH_USER" ] && printf "DEFAULT_AUTH_USER='%s'\n" "$DEFAULT_AUTH_USER" >> "$tmp_conf" [ -n "$DEFAULT_AUTH_PASS" ] && printf "DEFAULT_AUTH_PASS='%s'\n" "$DEFAULT_AUTH_PASS" >> "$tmp_conf" mv "$tmp_conf" "$CONF_FILE" fi [ -f "$VPS_DIR/default.conf" ] && chmod 600 "$VPS_DIR/default.conf" fi # Если профиль default остался битым и восстановить нечего — удаляем его if [ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=" "$VPS_DIR/default.conf"; then rm -f "$VPS_DIR/default.conf" fi } ensure_local_deps() { local missing="" _has_cmd() { command -v "$1" >/dev/null 2>&1 || [ -x "/opt/bin/$1" ] || [ -x "/opt/sbin/$1" ] } _has_cmd openssl || missing="$missing openssl-util" _has_cmd curl || missing="$missing curl" if [ -n "$missing" ]; then warn "Обнаружены недостающие пакеты:$missing" msg "Попытка автоматической установки через opkg..." if ! opkg update; then err "Не удалось обновить списки пакетов opkg. Проверьте интернет на роутере." return 1 fi for pkg in $missing; do msg "Установка $pkg..." if opkg install "$pkg"; then msg "Пакет $pkg успешно установлен." else err "Не удалось установить $pkg. Возможно, его нет в репозитории вашей модели." fi done hash -r 2>/dev/null msg "Проверка зависимостей завершена." fi } ssh_cmd() { local ssh_exec="/opt/bin/ssh" [ ! -f "$ssh_exec" ] && ssh_exec="ssh" $ssh_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@" } scp_cmd() { local scp_exec="/opt/bin/scp" [ ! -f "$scp_exec" ] && scp_exec="scp" $scp_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -P "$VPS_PORT" "$@" } next_free_port() { local port=$BASE_TUNNEL_PORT while true; do local used=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue grep -q "SVC_TUNNEL_PORT=\"$port\"" "$f" && used=1 && break done [ "$used" -eq 0 ] && echo "$port" && return port=$((port + 1)) done } get_router_ip() { # Метод 1: Через ndmq (Keenetic Bridge0) local ip=$(ndmq -p "show interface Bridge0" -path "address" 2>/dev/null) [ -n "$ip" ] && [ "$ip" != "0.0.0.0" ] && echo "$ip" && return # Метод 2: Через маршруты (default gateway) ip=$(ip route show | grep default | awk '{print $3}' | head -n 1) [ -n "$ip" ] && echo "$ip" && return # Метод 3: Через ip addr (Entware/Keenetic) ip=$(ip addr show br0 2>/dev/null | grep 'inet ' | awk '{print $2}' | cut -d/ -f1 | head -n1) [ -n "$ip" ] && echo "$ip" && return # Запасной вариант echo "192.168.1.1" } # Метод 2: Через ndmq (Локальный доступ в Entware) # Этот метод не требует пароля и работает мгновенно rci_call() { local command="$1" ndmq -p "$command" 2>/dev/null } enable_router_basic_auth() { log_info "Настройка локального доступа через RCI/ndmq..." local hostname=$(rci_call "show system" | grep "hostname" | awk '{print $NF}') log_info "Хостнейм роутера: $hostname" return 0 } gen_htpasswd() { local user="$1" local pass="$2" # Пытаемся использовать openssl если он есть в Entware if command -v openssl >/dev/null 2>&1; then local hash=$(openssl passwd -apr1 "$pass") echo "$user:$hash" else warn "Утилита 'openssl' не найдена! Пароль будет сохранен в открытом виде (Nginx может его не принять)." warn "Рекомендуется: opkg update && opkg install openssl-util" echo "$user:$pass" fi } next_free_ext_port() { local port=$BASE_EXT_PORT while true; do local used=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue grep -q "SVC_EXT_PORT=\"$port\"" "$f" && used=1 && break done [ "$used" -eq 0 ] && echo "$port" && return port=$((port + 1)) done } get_pid_file() { echo "$PID_DIR/$1.pid"; } is_running() { local pf pf=$(get_pid_file "$1") [ -f "$pf" ] && kill -0 "$(cat "$pf")" 2>/dev/null } count_services() { local count=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] && count=$((count + 1)) done echo $count } # ══════════════════════════════════════════════════════════════════════ # ГЛАВНОЕ МЕНЮ # ══════════════════════════════════════════════════════════════════════ main_menu() { while true; do clear_screen draw_box "rProxy v$VERSION" printf "\n" if ! check_conf; then printf " ${YELLOW}⚠ VPS не настроен${NC}\n\n" printf " ${BOLD}1)${NC} Настроить подключение к VPS\n" printf " ${BOLD}0)${NC} Выход\n" prompt "Выберите действие: " case "$REPLY" in 1) do_setup ;; 0|q) printf "\n"; exit 0 ;; *) ;; esac continue fi # Подсчёт сервисов и статусов по всем VPS local total=0 online=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue total=$((total + 1)) ( . "$f" is_running "$SVC_NAME" && exit 0 exit 1 ) && online=$((online + 1)) done printf " ${DIM}Серверов VPS:${NC} ${BOLD}$(ls -A "$VPS_DIR" | wc -l)${NC} " printf "${DIM}Сервисов:${NC} ${BOLD}$total${NC} " printf "${DIM}Онлайн:${NC} ${GREEN}${BOLD}$online${NC}\n" draw_separator printf "\n" printf " ${BOLD}1)${NC} 📋 Список сервисов и статус\n" printf " ${BOLD}2)${NC} ➕ Добавить сервис\n" printf " ${BOLD}3)${NC} 📝 Редактировать сервис\n" printf " ${BOLD}4)${NC} ❌ Удалить сервис\n" draw_separator printf " ${BOLD}5)${NC} ▶️ Запустить туннель\n" printf " ${BOLD}6)${NC} ⏹️ Остановить туннель\n" printf " ${BOLD}7)${NC} 🔄 Перезапустить туннель\n" draw_separator draw_separator printf " ${BOLD}8)${NC} 🔒 Получить/Обновить SSL (Certbot)\n" printf " ${BOLD}9)${NC} ⚙️ Настройки VPS\n" printf " ${BOLD}10)${NC} 🚀 Обновить rProxy\n" printf " ${BOLD}11)${NC} 🏥 Проверка VPS (Health)\n" printf " ${BOLD}0)${NC} 🚪 Выход\n" prompt "Выберите действие: " case "$REPLY" in 1) show_status ;; 2) do_add_interactive ;; 3) do_edit_interactive ;; 4) do_remove_interactive ;; 5) do_start_interactive ;; 6) do_stop_interactive ;; 7) do_restart_interactive ;; 8) do_ssl_interactive ;; 9) do_setup ;; 10) do_self_update ;; 11) do_health_check ;; 0|q) clear_screen; exit 0 ;; *) ;; esac done } # ══════════════════════════════════════════════════════════════════════ # СТАТУС # ══════════════════════════════════════════════════════════════════════ show_status() { clear_screen draw_box "Список сервисов" printf "\n" local has_services=0 local idx=0 printf " ${BOLD}%-4s %-14s %-22s %-7s %-9s %-18s${NC}\n" "№" "ИМЯ" "ЦЕЛЬ" "ПОРТ" "СТАТУС" "ДОМЕН" draw_separator for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue has_services=1 idx=$((idx + 1)) ( load_service "$(basename "$f" .conf)" >/dev/null 2>&1 local status_text if is_running "$SVC_NAME"; then status_text="${GREEN}● онлайн${NC}" else status_text="${RED}○ офлайн${NC}" fi local domain_text="${SVC_DOMAIN:-—}" local auto_text="" [ "$SVC_ENABLED" = "yes" ] && auto_text=" ${DIM}[авто]${NC}" printf " %-4s %-14s %-22s %-7s ${status_text} %-18s${auto_text}\n" \ "$idx" "$SVC_NAME" "$SVC_TARGET_HOST:$SVC_TARGET_PORT" "$SVC_EXT_PORT" "$domain_text" ) done if [ "$has_services" -eq 0 ]; then printf "\n ${YELLOW}Нет добавленных сервисов.${NC}\n" printf " ${DIM}Выберите «Добавить сервис» в главном меню.${NC}\n" fi printf "\n" pause } # ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════ # ПОМОЩНИКИ VPS # ══════════════════════════════════════════════════════════════════════ find_vps_by_domain() { local dom="$1" FOUND_VPS_ID="" # Пытаемся получить IP через ping (BusyBox стиль) local ip=$(ping -c 1 "$dom" 2>/dev/null | grep -o "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" | head -n 1) # Если не вышло, пробуем nslookup (парсим более надежно через grep -o) [ -z "$ip" ] && ip=$(nslookup "$dom" 2>/dev/null | grep "Address" | tail -n 1 | grep -o "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" | head -n 1) [ -z "$ip" ] && return 1 for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue # Используем grep для извлечения IP из конфига (быстрее и надежнее источника) local v_host=$(grep "VPS_HOST=" "$f" | cut -d'"' -f2) if [ "$v_host" = "$ip" ]; then FOUND_VPS_ID=$(basename "$f" .conf) return 0 fi done return 1 } select_vps_interactive() { SELECTED_VPS_ID="" draw_box "Выберите VPS для сервиса" printf "\n" local vps_list="" local idx=0 for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue idx=$((idx + 1)) ( . "$f" printf " ${BOLD}%d)${NC} %s (${DIM}%s${NC})\n" "$idx" "$(basename "$f" .conf)" "$VPS_HOST" ) [ -z "$vps_list" ] && vps_list="$(basename "$f" .conf)" || vps_list="$vps_list $(basename "$f" .conf)" done draw_separator printf " ${BOLD}901)${NC} ➕ Добавить новый VPS\n" printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите номер: " [ "$REPLY" = "0" ] && { SELECTED_VPS_ID=""; return 0; } if [ "$REPLY" = "901" ]; then do_add_vps # Возвращаем последний созданный (упрощение) SELECTED_VPS_ID=$(ls -t "$VPS_DIR"/*.conf | head -n 1 | xargs basename | sed 's/\.conf//') return 0 fi # Валидация ввода для cut if echo "$REPLY" | grep -qE '^[0-9]+$'; then SELECTED_VPS_ID=$(echo "$vps_list" | cut -d' ' -f"$REPLY") fi return 0 } # ══════════════════════════════════════════════════════════════════════ # ДОБАВИТЬ СЕРВИС (интерактивно) # ══════════════════════════════════════════════════════════════════════ do_add_interactive() { clear_screen draw_box "Добавить новый сервис" printf "\n" prompt "Название сервиса (латиницей, без пробелов): " local name=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | tr -cd '[:alnum:]_-') [ -z "$name" ] && { warn "Название не может быть пустым"; pause; return; } if [ -f "$SERVICES_DIR/$name.conf" ]; then err "Сервис '$name' уже существует" pause; return fi prompt "Адрес локального сервиса (например, 127.0.0.1:8080): " local target="$REPLY" [ -z "$target" ] && { warn "Адрес не может быть пустым"; pause; return; } printf "\n Привязать к домену?\n" printf " ${BOLD}1)${NC} Да — указать доменное имя (порт 443)\n" printf " ${BOLD}2)${NC} Нет — указать внешний порт\n" prompt "Выберите [2]: " local mode="${REPLY:-2}" local domain="" ext_port="" use_ssl="no" vps_id="" if [ "$mode" = "1" ]; then prompt "Доменное имя (например, mysite.example.com): " domain=$(echo "$REPLY" | tr '[:upper:]' '[:lower:]') [ -z "$domain" ] && { warn "Домен не указан"; pause; return; } # Проверка на дубликаты доменов for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue if grep -q "SVC_DOMAIN=\"$domain\"" "$f"; then local conflict=$(basename "$f" .conf) err "Домен '$domain' уже используется сервисом '$conflict'" pause; return fi done use_ssl="yes"; ext_port=443 if [ -z "$CERTBOT_EMAIL" ]; then prompt "Введите Email для Certbot: " CERTBOT_EMAIL="$REPLY" sed -i "/CERTBOT_EMAIL=/d" "$CONF_FILE" echo "CERTBOT_EMAIL=\"$CERTBOT_EMAIL\"" >> "$CONF_FILE" fi # Автоопределение VPS по домену msg "Проверяю куда направлен домен $domain..." if find_vps_by_domain "$domain"; then vps_id="$FOUND_VPS_ID" msg "Домен $domain указывает на ваш VPS '$vps_id'. Выбираю его." else warn "Не удалось автоматически определить VPS для $domain" fi else ext_port=$(next_free_ext_port) prompt "Внешний порт [$ext_port]: " ext_port="${REPLY:-$ext_port}" fi # Если VPS не определен автоматически — выбираем вручную if [ -z "$vps_id" ]; then select_vps_interactive vps_id="$SELECTED_VPS_ID" [ -z "$vps_id" ] && { warn "VPS не выбран"; pause; return; } fi # Загружаем выбранный VPS для деплоя load_vps "$vps_id" || { err "Не удалось загрузить VPS '$vps_id'"; pause; return; } # Разбор адреса local t_host="${target%:*}" local t_port="${target#*:}" [ "$t_host" = "$t_port" ] && { t_port="$t_host"; t_host="127.0.0.1"; } local tunnel_port=$(next_free_port) printf "\n" draw_separator printf " ${BOLD}Сервис:${NC} $name\n" printf " ${BOLD}VPS:${NC} $vps_id ($VPS_HOST)\n" printf " ${BOLD}Цель:${NC} $t_host:$t_port\n" printf " ${BOLD}Туннель:${NC} порт $tunnel_port\n" [ -n "$domain" ] && printf " ${BOLD}Домен:${NC} $domain\n" printf " ${BOLD}Внешний порт:${NC} $ext_port\n" prompt "Защитить сервис паролем? (д/н) [н]: " local use_ndm_auth="no" local htpasswd_line="" case "$REPLY" in д|Д|y|Y|да|yes) use_ndm_auth="yes" local def_user="${DEFAULT_AUTH_USER:-admin}" if [ -n "$DEFAULT_AUTH_PASS" ]; then printf " ${DIM}Будут использованы сохраненные данные: $def_user / *****${NC}\n" prompt "Использовать их? (д/н) [д]: " if [ "${REPLY:-д}" = "д" ] || [ "${REPLY:-д}" = "y" ]; then htpasswd_line=$(gen_htpasswd "$def_user" "$DEFAULT_AUTH_PASS") fi fi if [ -z "$htpasswd_line" ]; then prompt "Введите имя пользователя [$def_user]: " local user="${REPLY:-$def_user}" prompt "Введите пароль для '$user': " local pass="$REPLY" if [ -z "$pass" ]; then warn "Пароль не может быть пустым. Авторизация будет отключена." use_ndm_auth="no" else htpasswd_line=$(gen_htpasswd "$user" "$pass") fi fi ;; esac draw_separator prompt "Всё верно? Добавить сервис? (д/н) [д]: " [ "${REPLY:-д}" != "д" ] && [ "${REPLY:-д}" != "y" ] && { msg "Отменено"; pause; return; } msg "Добавляю сервис '$name'..." # Конфигурация авторизации local auth_config="" if [ "$use_ndm_auth" = "yes" ]; then auth_config=" auth_basic \"Restricted Access\"; auth_basic_user_file /etc/nginx/rproxy_$name.htpasswd; " fi local tmp="/tmp/rproxy_$name.conf" generate_nginx_conf "$name" "$t_host" "$t_port" "$tunnel_port" "$domain" "$ext_port" "$use_ndm_auth" "$tmp" # Деплой # Деплой htpasswd если нужно if [ "$use_ndm_auth" = "yes" ]; then local ht_tmp="/tmp/rproxy_$name.htpasswd" echo "$htpasswd_line" > "$ht_tmp" scp_cmd "$ht_tmp" "$VPS_USER@$VPS_HOST:/etc/nginx/rproxy_$name.htpasswd" rm -f "$ht_tmp" fi # Деплой nginx scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_$name.conf" || { err "Ошибка деплоя nginx"; rm -f "$tmp"; pause; return; } rm -f "$tmp" ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1 # SSL если нужно if [ "$use_ssl" = "yes" ]; then msg "Получаю SSL через Certbot (может занять время)..." ssh_cmd "certbot --nginx -d $domain --non-interactive --agree-tos -m $CERTBOT_EMAIL" || warn "Certbot вернул ошибку" fi # Сохранение конфига сервиса (используем printf для защиты от спецсимволов и одинарные кавычки) { printf 'SVC_NAME="%s"\n' "$name" printf 'SVC_VPS="%s"\n' "$vps_id" printf 'SVC_TARGET_HOST="%s"\n' "$t_host" printf 'SVC_TARGET_PORT="%s"\n' "$t_port" printf 'SVC_TUNNEL_PORT="%s"\n' "$tunnel_port" printf 'SVC_EXT_PORT="%s"\n' "$ext_port" printf 'SVC_DOMAIN="%s"\n' "$domain" printf 'SVC_SSL="%s"\n' "$use_ssl" printf 'SVC_NDM_AUTH="%s"\n' "$use_ndm_auth" printf "SVC_HTPASSWD='%s'\n" "$htpasswd_line" printf 'SVC_ENABLED="yes"\n' } > "$SERVICES_DIR/$name.conf" msg "Сервис '$name' добавлен на VPS '$vps_id'!" do_start_service "$name" pause } # ══════════════════════════════════════════════════════════════════════ # РЕДАКТИРОВАТЬ СЕРВИС # ══════════════════════════════════════════════════════════════════════ do_edit_interactive() { clear_screen draw_box "Редактировать сервис" printf "\n" select_service "Выберите сервис для редактирования" "all" || { pause; return; } local name="$SELECTED_SERVICE" [ "$name" = "__ALL__" ] && { warn "Выберите конкретный сервис"; pause; return; } load_service "$name" || { pause; return; } header "Редактирование: $name" printf " ${DIM}Текущая цель:${NC} $SVC_TARGET_HOST:$SVC_TARGET_PORT\n\n" prompt "Новый IP адрес [$SVC_TARGET_HOST]: " local new_host="${REPLY:-$SVC_TARGET_HOST}" prompt "Новый внутренний порт [$SVC_TARGET_PORT]: " local new_port="${REPLY:-$SVC_TARGET_PORT}" prompt "Сохранить изменения? (д/н) [д]: " [ "${REPLY:-д}" != "д" ] && [ "${REPLY:-д}" != "y" ] && { msg "Отменено"; pause; return; } # Обновляем конфиг sed -i "s/SVC_TARGET_HOST=.*/SVC_TARGET_HOST=\"$new_host\"/" "$SERVICES_DIR/$name.conf" sed -i "s/SVC_TARGET_PORT=.*/SVC_TARGET_PORT=\"$new_port\"/" "$SERVICES_DIR/$name.conf" # Перегенерация и деплой конфига Nginx на VPS msg "Обновляю конфигурацию Nginx на VPS..." load_service "$name" # Перезагружаем переменные local tmp="/tmp/rproxy_edit_$name.conf" generate_nginx_conf "$name" "$SVC_TARGET_HOST" "$SVC_TARGET_PORT" "$SVC_TUNNEL_PORT" "$SVC_DOMAIN" "$SVC_EXT_PORT" "$SVC_NDM_AUTH" "$tmp" scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_$name.conf" rm -f "$tmp" ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1 # SSL если он был настроен - восстанавливаем слушателей 443 if [ "$SVC_SSL" = "yes" ]; then msg "Восстанавливаю SSL через Certbot..." ssh_cmd "certbot --nginx -d $SVC_DOMAIN --non-interactive --agree-tos -m $CERTBOT_EMAIL" >/dev/null 2>&1 fi msg "Настройки обновлены. Перезапускаю туннель..." if is_running "$name"; then do_stop_service "$name" sleep 1 # Обновление htpasswd на VPS если включено if [ "$SVC_NDM_AUTH" = "yes" ]; then ssh_cmd "printf '%s\n' '$SVC_HTPASSWD' > /etc/nginx/rproxy_$name.htpasswd" fi do_start_service "$name" else msg "Сервис был остановлен. Настройки вступят в силу при следующем запуске." fi msg "Готово!" pause } # ══════════════════════════════════════════════════════════════════════ # ПОЛУЧИТЬ SSL (Certbot) # ══════════════════════════════════════════════════════════════════════ do_ssl_interactive() { clear_screen draw_box "Получить SSL сертификат (Certbot)" printf "\n" select_service "Выпуститиь SSL для" "all" || { pause; return; } local name="$SELECTED_SERVICE" [ "$name" = "__ALL__" ] && { warn "Выберите конкретный сервис"; pause; return; } load_service "$name" || { pause; return; } if [ -z "$SVC_DOMAIN" ]; then err "Для получения SSL сервису должен быть назначен домен" pause return fi if [ -z "$CERTBOT_EMAIL" ]; then prompt "Введите Email для регистрации в Certbot: " CERTBOT_EMAIL="$REPLY" sed -i "/CERTBOT_EMAIL=/d" "$CONF_FILE" echo "CERTBOT_EMAIL=\"$CERTBOT_EMAIL\"" >> "$CONF_FILE" fi msg "Проверяю наличие Certbot на VPS..." ssh_cmd "command -v certbot >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq certbot python3-certbot-nginx || yum install -y certbot python3-certbot-nginx)" || { warn "Не удалось автоматически установить Certbot на VPS" } msg "Запрашиваю сертификат для $SVC_DOMAIN..." if ssh_cmd "certbot --nginx -d $SVC_DOMAIN --non-interactive --agree-tos -m $CERTBOT_EMAIL"; then msg "SSL сертификат успешно получен!" # Обновляем конфиг сервиса sed -i "s/SVC_SSL=.*/SVC_SSL=\"yes\"/" "$SERVICES_DIR/$name.conf" sed -i "s/SVC_EXT_PORT=.*/SVC_EXT_PORT=\"443\"/" "$SERVICES_DIR/$name.conf" # Проверка автопродления (добавляем таймер/крон если нет) ssh_cmd "systemctl is-active certbot.timer >/dev/null 2>&1 || (crontab -l 2>/dev/null | grep -q certbot || (crontab -l 2>/dev/null; echo \"0 0,12 * * * certbot renew -q\") | crontab -)" else err "Certbot не смог выпустить сертификат" warn "Убедитесь, что домен $SVC_DOMAIN указывает на этот VPS и порт 80 открыт" fi pause } # ══════════════════════════════════════════════════════════════════════ # ОБНОВЛЕНИЕ СКРИПТА # ══════════════════════════════════════════════════════════════════════ do_self_update() { clear_screen draw_box "Обновление rProxy" printf "\n" msg "Проверяю наличие обновлений..." local url="http://5.104.75.50:3000/Petro1990/rProxy/raw/branch/main/rproxy" local tmp_file="/tmp/rproxy_update" if curl -sSL "$url" -o "$tmp_file"; then if [ -s "$tmp_file" ]; then mv "$tmp_file" "/opt/bin/rproxy" chmod +x "/opt/bin/rproxy" msg "Обновление успешно завершено!" pause exec /opt/bin/rproxy # Перезапуск новой версии else err "Файл обновления не пуст или не найден" fi else err "Не удалось загрузить обновление" fi pause } # ══════════════════════════════════════════════════════════════════════ # ПРОВЕРКА СОСТОЯНИЯ VPS # ══════════════════════════════════════════════════════════════════════ do_health_check() { clear_screen draw_box "Состояние VPS и SSL" printf "\n" for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue local vps_id=$(basename "$f" .conf) load_vps "$vps_id" header "Сервер: $vps_id ($VPS_HOST)" msg "Проверяю Nginx..." if ssh_cmd "systemctl is-active nginx >/dev/null 2>&1"; then printf " Nginx: ${GREEN}${BOLD}Запущен${NC}\n" else printf " Nginx: ${RED}${BOLD}Остановлен${NC}\n" fi msg "Проверяю автопродление SSL (Certbot)..." local timer=$(ssh_cmd "systemctl is-active certbot.timer 2>/dev/null | tr -d '\r'") if [ "$timer" = "active" ]; then printf " SSL Timer: ${GREEN}${BOLD}Активен (Systemd)${NC}\n" local next_run=$(ssh_cmd "systemctl list-timers certbot.timer --no-legend 2>/dev/null | awk '{print \$1, \$2}'" | head -n 1) [ -n "$next_run" ] && printf " Следующее: $next_run\n" else if ssh_cmd "crontab -l 2>/dev/null | grep -q certbot"; then printf " SSL Renewal: ${GREEN}${BOLD}Настроено (Cron)${NC}\n" else printf " SSL Renewal: ${RED}${BOLD}Не обнаружено${NC}\n" fi fi msg "Действующие сертификаты:" local certs=$(ssh_cmd "certbot certificates 2>/dev/null | grep -E 'Expiry Date:|Domains:'") if [ -n "$certs" ]; then echo "$certs" | while read -r line; do printf " %s\n" "$line" done else printf " ${DIM}Сертификаты не найдены${NC}\n" fi done printf "\n" msg "Утилиты на роутере:" [ -x "/opt/bin/ssh" ] && printf " SSH: ${GREEN}Entware OK${NC}\n" || printf " SSH: ${RED}Missing${NC}\n" [ -x "/opt/bin/autossh" ] && printf " AutoSSH: ${GREEN}OK${NC}\n" || printf " AutoSSH: ${RED}Missing${NC}\n" pause } # ══════════════════════════════════════════════════════════════════════ # УДАЛИТЬ СЕРВИС (интерактивно) # ══════════════════════════════════════════════════════════════════════ do_remove_interactive() { clear_screen draw_box "Удалить сервис" printf "\n" select_service "Выберите сервис для удаления" "all" || { pause; return; } local name="$SELECTED_SERVICE" [ "$name" = "__ALL__" ] && { warn "Для удаления выберите конкретный сервис"; pause; return; } prompt "Удалить сервис '$name'? (д/н) [н]: " case "${REPLY:-н}" in д|Д|y|Y|да|yes) ;; *) msg "Отменено"; pause; return ;; esac load_service "$name" || { pause; return; } is_running "$name" && do_stop_service "$name" msg "Удаляю конфиги Nginx и пароли на VPS..." ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$name.conf /etc/nginx/rproxy_$name.htpasswd && nginx -t && systemctl reload nginx" >/dev/null 2>&1 || { warn "Не удалось полностью очистить VPS (возможно файлы уже удалены)" } rm -f "$SERVICES_DIR/$name.conf" rm -f "$(get_pid_file "$name")" msg "Сервис '$name' удалён" pause } # ══════════════════════════════════════════════════════════════════════ # РЕДАКТИРОВАТЬ СЕРВИС (интерактивно) # ══════════════════════════════════════════════════════════════════════ do_edit_interactive() { clear_screen draw_box "Редактировать сервис" printf "\n" select_service "Выберите сервис для редактирования" "all" || { pause; return; } local name="$SELECTED_SERVICE" [ "$name" = "__ALL__" ] && { warn "Для редактирования выберите конкретный сервис"; pause; return; } load_service "$name" || { pause; return; } printf "\n" draw_separator printf " ${DIM}Текущие параметры:${NC}\n" printf " Цель: ${BOLD}$SVC_TARGET_HOST:$SVC_TARGET_PORT${NC}\n" printf " Туннель: порт ${BOLD}$SVC_TUNNEL_PORT${NC}\n" [ -n "$SVC_DOMAIN" ] && printf " Домен: ${BOLD}$SVC_DOMAIN${NC}\n" printf " Внешний порт: ${BOLD}$SVC_EXT_PORT${NC}\n" draw_separator prompt "Новый адрес (IP:порт) [$SVC_TARGET_HOST:$SVC_TARGET_PORT]: " local new_target="${REPLY:-$SVC_TARGET_HOST:$SVC_TARGET_PORT}" local new_host="${new_target%:*}" local new_port="${new_target#*:}" if [ "$new_host" = "$new_port" ]; then new_port="$new_host" new_host="127.0.0.1" fi # Если ничего не изменилось if [ "$new_host" = "$SVC_TARGET_HOST" ] && [ "$new_port" = "$SVC_TARGET_PORT" ]; then msg "Параметры не изменились." pause return fi printf "\n" draw_separator printf " ${DIM}Было:${NC} $SVC_TARGET_HOST:$SVC_TARGET_PORT\n" printf " ${GREEN}Стало:${NC} $new_host:$new_port\n" draw_separator prompt "Применить изменения? (д/н) [д]: " case "${REPLY:-д}" in д|Д|y|Y|да|yes) ;; *) msg "Отменено"; pause; return ;; esac printf "\n" msg "Применяю изменения..." # Остановить туннель если запущен local was_running=0 if is_running "$name"; then was_running=1 do_stop_service "$name" fi # Обновить конфиг сервиса sed -i "s|SVC_TARGET_HOST=\".*\"|SVC_TARGET_HOST=\"$new_host\"|" "$SERVICES_DIR/$name.conf" sed -i "s|SVC_TARGET_PORT=\".*\"|SVC_TARGET_PORT=\"$new_port\"|" "$SERVICES_DIR/$name.conf" msg "Конфигурация обновлена." # Перезапустить туннель если он был запущен if [ "$was_running" -eq 1 ]; then msg "Перезапускаю туннель..." load_service "$name" || { pause; return; } do_start_service "$name" fi msg "Сервис '$name' успешно обновлён!" pause } # ══════════════════════════════════════════════════════════════════════ # ВЫБОР СЕРВИСА (общий) # ══════════════════════════════════════════════════════════════════════ select_service() { local title="$1" local filter="$2" # all, running, stopped local services="" local idx=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue . "$f" local running=0 is_running "$SVC_NAME" && running=1 # Фильтр case "$filter" in running) [ "$running" -eq 0 ] && continue ;; stopped) [ "$running" -eq 1 ] && continue ;; esac idx=$((idx + 1)) local st="${RED}○${NC}" [ "$running" -eq 1 ] && st="${GREEN}●${NC}" printf " ${BOLD}%d)${NC} %b %-14s %s:%s" "$idx" "$st" "$SVC_NAME" "$SVC_TARGET_HOST" "$SVC_TARGET_PORT" [ -n "$SVC_DOMAIN" ] && printf " ${DIM}(%s)${NC}" "$SVC_DOMAIN" printf "\n" [ -z "$services" ] && services="$SVC_NAME" || services="$services $SVC_NAME" done if [ "$idx" -eq 0 ]; then printf " ${YELLOW}Нет подходящих сервисов.${NC}\n" SELECTED_SERVICE="" return 1 fi draw_separator printf " ${BOLD}903)${NC} Все сервисы\n" printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите сервис: " [ "$REPLY" = "0" ] && { SELECTED_SERVICE=""; return 1; } if [ "$REPLY" = "903" ]; then SELECTED_SERVICE="__ALL__" return 0 fi # Извлечение имени сервиса по индексу (1-based) с валидацией if echo "$REPLY" | grep -qE '^[0-9]+$'; then SELECTED_SERVICE=$(echo "$services" | cut -d' ' -f"$REPLY") else SELECTED_SERVICE="" fi [ -z "$SELECTED_SERVICE" ] && { warn "Неверный выбор"; return 1; } return 0 } # ══════════════════════════════════════════════════════════════════════ # ЗАПУСК / ОСТАНОВ / ПЕРЕЗАПУСК (интерактивно) # ══════════════════════════════════════════════════════════════════════ do_start_interactive() { clear_screen draw_box "Запустить туннель" printf "\n" select_service "Запустить" "stopped" || { pause; return; } if [ "$SELECTED_SERVICE" = "__ALL__" ]; then do_start_all else do_start_service "$SELECTED_SERVICE" fi pause } do_stop_interactive() { clear_screen draw_box "Остановить туннель" printf "\n" select_service "Остановить" "running" || { pause; return; } if [ "$SELECTED_SERVICE" = "__ALL__" ]; then do_stop_all else do_stop_service "$SELECTED_SERVICE" fi pause } do_restart_interactive() { clear_screen draw_box "Перезапустить туннель" printf "\n" select_service "Перезапустить" "all" || { pause; return; } if [ "$SELECTED_SERVICE" = "__ALL__" ]; then for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue . "$f" do_stop_service "$SVC_NAME" sleep 1 do_start_service "$SVC_NAME" done else load_service "$SELECTED_SERVICE" do_stop_service "$SELECTED_SERVICE" sleep 1 do_start_service "$SELECTED_SERVICE" fi pause } # ══════════════════════════════════════════════════════════════════════ # ЗАПУСК / ОСТАНОВ (одного сервиса) # ══════════════════════════════════════════════════════════════════════ do_start_service() { local name="$1" load_service "$name" || return if is_running "$name"; then warn "Сервис '$name' уже запущен" return fi msg "Запускаю туннель '$name' ($SVC_TARGET_HOST:$SVC_TARGET_PORT → VPS:$SVC_TUNNEL_PORT)..." local ssh_exec="/opt/bin/ssh" [ ! -f "$ssh_exec" ] && ssh_exec="ssh" export AUTOSSH_PATH="$ssh_exec" mkdir -p "$PID_DIR" local pid_file pid_file=$(get_pid_file "$name") msg "Синхронизация с VPS (очистка портов)..." ssh_cmd "fuser -k $SVC_TUNNEL_PORT/tcp >/dev/null 2>&1 || true" local ssh_opts="-o StrictHostKeyChecking=no -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -o ConnectTimeout=10 -o ExitOnForwardFailure=yes" local tunnel_args="-R 0.0.0.0:$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" if [ "$VPS_AUTH" = "password" ]; then AUTOSSH_GATETIME=0 sshpass -p "$VPS_PASS" autossh -M 0 -f -N \ $ssh_opts \ -p "$VPS_PORT" \ $tunnel_args \ "$VPS_USER@$VPS_HOST" & else AUTOSSH_GATETIME=0 autossh -M 0 -f -N \ $ssh_opts \ -i "$SSH_KEY" \ -p "$VPS_PORT" \ $tunnel_args \ "$VPS_USER@$VPS_HOST" & fi local bg_pid=$! sleep 2 local real_pid real_pid=$(pgrep -f "autossh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1) [ -z "$real_pid" ] && real_pid=$(pgrep -f "ssh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1) [ -z "$real_pid" ] && real_pid=$bg_pid echo "$real_pid" > "$pid_file" msg "Туннель '$name' запущен (PID: $real_pid)" } do_stop_service() { local name="$1" load_service "$name" || return # Загружаем VPS для правильной очистки local pid_file pid_file=$(get_pid_file "$name") if [ ! -f "$pid_file" ]; then warn "Сервис '$name' не запущен" return fi local pid pid=$(cat "$pid_file") kill "$pid" 2>/dev/null pkill -f "ssh.*:$SVC_TUNNEL_PORT:.*$VPS_HOST" 2>/dev/null rm -f "$pid_file" msg "Туннель '$name' остановлен" } _clear_svc_vars() { unset SVC_NAME SVC_VPS SVC_TARGET_HOST SVC_TARGET_PORT SVC_TUNNEL_PORT unset SVC_EXT_PORT SVC_DOMAIN SVC_SSL SVC_NDM_AUTH SVC_HTPASSWD SVC_ENABLED } load_service() { _clear_svc_vars local f="$SERVICES_DIR/$1.conf" [ -f "$f" ] || return 1 . "$f" load_vps "$SVC_VPS" } generate_nginx_conf() { local name="$1" local t_host="$2" local t_port="$3" local tunnel_port="$4" local domain="$5" local ext_port="$6" local use_ndm_auth="$7" local target_file="$8" local stealth_host="$t_host" [ "$t_port" != "80" ] && stealth_host="$t_host:$t_port" local auth_config="" if [ "$use_ndm_auth" = "yes" ]; then auth_config=" auth_basic \"Restricted Access\"; auth_basic_user_file /etc/nginx/rproxy_$name.htpasswd; " fi if [ -n "$domain" ]; then cat > "$target_file" << NGINXEOF server { listen 80; server_name "$domain"; proxy_buffering off; proxy_request_buffering off; location / { $auth_config proxy_pass http://127.0.0.1:$tunnel_port; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host "$stealth_host"; proxy_set_header Origin "http://$stealth_host"; proxy_set_header Referer "http://$stealth_host/"; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_cookie_domain "$t_host" "\$host"; } } NGINXEOF else cat > "$target_file" << NGINXEOF server { listen $ext_port; proxy_buffering off; proxy_request_buffering off; location / { $auth_config proxy_pass http://127.0.0.1:$tunnel_port; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host "$stealth_host"; proxy_set_header Origin "http://$stealth_host"; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_cookie_domain "$t_host" "\$host"; } } NGINXEOF fi } do_start_all() { for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue . "$f" [ "$SVC_ENABLED" = "yes" ] && do_start_service "$SVC_NAME" done } do_stop_all() { for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue . "$f" do_stop_service "$SVC_NAME" done } # ══════════════════════════════════════════════════════════════════════ # НАСТРОЙКА VPS (Менеджер профилей) # ══════════════════════════════════════════════════════════════════════ do_setup() { while true; do clear_screen draw_box "Управление VPS серверами" printf "\n" local vps_list="" local idx=0 for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue idx=$((idx + 1)) VPS_HOST="" # Сброс перед загрузкой . "$f" local v_info="${DIM}${VPS_USER}@${VPS_HOST}:${VPS_PORT}${NC}" [ -z "$VPS_HOST" ] && v_info="${RED}Не настроен${NC}" printf " ${BOLD}%d)${NC} %-14s %b\n" "$idx" "$(basename "$f" .conf)" "$v_info" vps_list="$vps_list $(basename "$f" .conf)" done if [ "$idx" -eq 0 ]; then printf " ${YELLOW}Нет настроенных VPS серверов.${NC}\n" fi draw_separator printf " ${BOLD}901)${NC} ➕ Добавить новый VPS\n" [ "$idx" -gt 0 ] && printf " ${BOLD}902)${NC} ❌ Удалить VPS\n" [ "$idx" -gt 0 ] && printf " ${BOLD}903)${NC} 🧹 Очистить VPS от мусора (фантомные конфиги)\n" printf " ${BOLD}904)${NC} 🔑 Пароль публикации по умолчанию\n" printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите действие: " case "$REPLY" in 0) return ;; 901) do_add_vps ;; 902) [ "$idx" -gt 0 ] && do_delete_vps_interactive "$vps_list" ;; 903) [ "$idx" -gt 0 ] && do_vps_cleanup_menu "$vps_list" ;; 904) do_change_auth_defaults ;; [1-9]*) local sel_vps=$(echo "$vps_list" | cut -d' ' -f"$REPLY") [ -n "$sel_vps" ] && do_add_vps "$sel_vps" ;; esac done } do_delete_vps_interactive() { local list="$1" prompt "Введите номер VPS для удаления: " local sel_vps=$(echo "$list" | cut -d' ' -f"$REPLY") [ -z "$sel_vps" ] && { warn "Неверный выбор"; pause; return; } prompt "Удалить профиль VPS '$sel_vps'? (д/н) [н]: " case "$REPLY" in д|Д|y|Y) rm -f "$VPS_DIR/$sel_vps.conf"; msg "Удалено" ;; *) msg "Отменено" ;; esac pause } do_vps_cleanup_menu() { local list="$1" prompt "Введите номер VPS для очистки: " local sel_vps=$(echo "$list" | cut -d' ' -f"$REPLY") [ -z "$sel_vps" ] && { warn "Неверный выбор"; pause; return; } do_vps_cleanup_interactive "$sel_vps" } do_vps_cleanup_interactive() { local vps_id="$1" load_vps "$vps_id" || return clear_screen draw_box "Очистка VPS: $vps_id ($VPS_HOST)" printf "\n" msg "Сканирую VPS на наличие конфигураций rProxy..." local remote_files remote_files=$(ssh_cmd "ls $REMOTE_NGINX_DIR/rproxy_*.conf /etc/nginx/rproxy_*.htpasswd 2>/dev/null | xargs -n1 basename 2>/dev/null") if [ -z "$remote_files" ]; then msg "На сервере не обнаружено файлов rProxy." pause return fi local to_delete="" local local_found="" header "Обнаруженные файлы на VPS:" echo "$remote_files" | while read -r line; do # Извлекаем имя сервиса из rproxy_NAME.conf или rproxy_NAME.htpasswd local s_name=$(echo "$line" | sed -E 's/^rproxy_(.+)\.(conf|htpasswd)$/\1/') if [ -f "$SERVICES_DIR/$s_name.conf" ]; then printf " ${GREEN}●${NC} %-30s (Активен локально)\n" "$line" else printf " ${RED}○${NC} %-30s (ФАНТОМНЫЙ - нет на роутере)\n" "$line" local_found="1" fi done draw_separator printf " ${BOLD}1)${NC} 🧹 Умная очистка (удалить только фантомные файлы)\n" printf " ${BOLD}2)${NC} 🔥 Полная очистка (удалить ВСЕ конфиги rProxy на этом VPS)\n" printf " ${BOLD}0)${NC} Отмена\n" prompt "Выберите действие: " case "$REPLY" in 1) msg "Выполняю умную очистку..." echo "$remote_files" | while read -r line; do local s_name=$(echo "$line" | sed -E 's/^rproxy_(.+)\.(conf|htpasswd)$/\1/') if [ ! -f "$SERVICES_DIR/$s_name.conf" ]; then msg "Удаляю $line..." ssh_cmd "rm -f $REMOTE_NGINX_DIR/$line /etc/nginx/$line" >/dev/null 2>&1 fi done ssh_cmd "nginx -t && systemctl reload nginx" >/dev/null 2>&1 msg "Готово!" ;; 2) prompt "Это удалит ВСЕ публикации rProxy на этом VPS. Уверены? (y/n): " [ "$REPLY" != "y" ] && { msg "Отменено"; pause; return; } msg "Полная очистка..." ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_*.conf /etc/nginx/rproxy_*.htpasswd && nginx -t && systemctl reload nginx" >/dev/null 2>&1 msg "Все конфигурации rProxy на VPS '$vps_id' удалены." ;; *) msg "Отменено" ;; esac pause } do_change_auth_defaults() { draw_box "Настройки публикации (Basic Auth)" printf "\n" printf " Эти данные будут предлагаться при защите новых сервисов.\n" prompt "Имя пользователя [${DEFAULT_AUTH_USER:-admin}]: " local user="${REPLY:-${DEFAULT_AUTH_USER:-admin}}" prompt "Пароль (оставьте пустым чтобы не менять): " local pass="$REPLY" if [ -z "$pass" ] && [ -n "$DEFAULT_AUTH_PASS" ]; then pass="$DEFAULT_AUTH_PASS" fi [ -z "$pass" ] && { warn "Пароль не может быть пустым"; pause; return; } sed -i "/DEFAULT_AUTH_USER=/d" "$CONF_FILE" sed -i "/DEFAULT_AUTH_PASS=/d" "$CONF_FILE" printf "DEFAULT_AUTH_USER='%s'\n" "$user" >> "$CONF_FILE" printf "DEFAULT_AUTH_PASS='%s'\n" "$pass" >> "$CONF_FILE" msg "Данные успешно сохранены" DEFAULT_AUTH_USER="$user" DEFAULT_AUTH_PASS="$pass" pause } do_add_vps() { local vps_id="${1:-}" local is_new=0 [ -z "$vps_id" ] && is_new=1 if [ "$is_new" -eq 1 ]; then prompt "Название сервера (например, amsterdam): " vps_id=$(echo "$REPLY" | tr -dc 'a-zA-Z0-9_-') [ -z "$vps_id" ] && { warn "Недопустимое название"; pause; return; } fi local vps_file="$VPS_DIR/$vps_id.conf" local v_host="" v_port="22" v_user="root" if [ -f "$vps_file" ]; then . "$vps_file" v_host="$VPS_HOST"; v_port="$VPS_PORT"; v_user="$VPS_USER" fi header "Настройка VPS: $vps_id" prompt "IP-адрес VPS [$v_host]: " v_host="${REPLY:-$v_host}" [ -z "$v_host" ] && { warn "IP обязателен"; pause; return; } prompt "SSH порт [$v_port]: " v_port="${REPLY:-$v_port}" prompt "SSH пользователь [$v_user]: " v_user="${REPLY:-$v_user}" # Настройка ключей ensure_ssh_key msg "Настраиваю доступ по SSH-ключу для $v_host..." # Копирование ключа (пароль запросит сама система) local ssh_exec="/opt/bin/ssh" [ ! -f "$ssh_exec" ] && ssh_exec="ssh" local copy_id="/opt/bin/ssh-copy-id" [ ! -f "$copy_id" ] && copy_id="ssh-copy-id" printf " ${YELLOW}Сейчас будет запрошен пароль VPS для копирования ключа${NC}\n" $copy_id -o StrictHostKeyChecking=no -i "$SSH_KEY.pub" -p "$v_port" "$v_user@$v_host" 2>/dev/null || { cat "$SSH_KEY.pub" | $ssh_exec -o StrictHostKeyChecking=no -p "$v_port" "$v_user@$v_host" \ "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys" } if ! $ssh_exec -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i "$SSH_KEY" -p "$v_port" "$v_user@$v_host" "echo ok" >/dev/null 2>&1; then err "Ошибка подключения по ключу. Убедитесь, что пароль введен верно." pause; return fi # Сохраняем (БЕЗ ПАРОЛЯ) cat > "$vps_file" </dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq nginx psmisc || (yum update -y && yum install -y nginx psmisc) else apt-get update -qq && apt-get install -y -qq psmisc || yum install -y psmisc fi mkdir -p /etc/nginx/sites-enabled grep -q 'sites-enabled' /etc/nginx/nginx.conf || sed -i '/http {/a\ include /etc/nginx/sites-enabled/*.conf;' /etc/nginx/nginx.conf command -v certbot >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq certbot python3-certbot-nginx || yum install -y certbot python3-certbot-nginx) systemctl enable nginx && systemctl start nginx # Оптимизация SSH на стороне сервера для туннелей grep -q 'ClientAliveInterval' /etc/ssh/sshd_config || echo 'ClientAliveInterval 30' >> /etc/ssh/sshd_config grep -q 'ClientAliveCountMax' /etc/ssh/sshd_config || echo 'ClientAliveCountMax 2' >> /etc/ssh/sshd_config systemctl restart ssh " pause } ensure_ssh_key() { if [ ! -f "$SSH_KEY" ]; then msg "Генерирую SSH-ключ (ed25519)..." local keygen="/opt/bin/ssh-keygen" [ ! -f "$keygen" ] && keygen="ssh-keygen" $keygen -t ed25519 -f "$SSH_KEY" -N "" -q [ ! -f "$SSH_KEY.pub" ] && $keygen -y -f "$SSH_KEY" > "$SSH_KEY.pub" chmod 600 "$SSH_KEY" fi } # ══════════════════════════════════════════════════════════════════════ # ТОЧКА ВХОДА # ══════════════════════════════════════════════════════════════════════ migrate_config 2>/dev/null ensure_local_deps # Поддержка прямых команд для init-скрипта и автоматизации case "${1:-}" in start) check_conf && do_start_all ;; stop) check_conf && do_stop_all ;; restart) check_conf && do_stop_all; sleep 1; do_start_all ;; status) check_conf || { err "VPS не настроен"; exit 1; } for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue local name=$(basename "$f" .conf) ( _clear_svc_vars . "$f" local status="${RED}OFFLINE${NC}" is_running "$SVC_NAME" && status="${GREEN}ONLINE${NC}" local type="[PORT]" [ -n "$SVC_DOMAIN" ] && type="[DOM ]" local addr="$SVC_EXT_PORT" [ -n "$SVC_DOMAIN" ] && addr="$SVC_DOMAIN" printf " %-15s %-7s %-20s %s\n" "$SVC_NAME" "$type" "$addr" "$status" ) done ;; -v|--version) echo "rProxy v$VERSION" ;; "") main_menu ;; *) main_menu ;; esac