#!/bin/sh # rProxy — Менеджер обратного прокси для роутеров Keenetic # Публикация локальных сервисов через SSH-туннели + nginx на VPS # http://5.104.75.50:3000/Petro1990/rProxy VERSION="1.1.2" 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 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" printf "${CYAN}╔══════════════════════════════════════════════╗${NC}\n" printf "${CYAN}║${NC} ${BOLD}%-44s${NC}${CYAN}║${NC}\n" "$title" printf "${CYAN}╚══════════════════════════════════════════════╝${NC}\n" } 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 return 1 fi . "$CONF_FILE" # Строгая проверка: ищем хотя бы один файл в vps/, в котором заполнена переменная VPS_HOST for f in "$VPS_DIR"/*.conf; do [ -f "$f" ] || continue if grep -q "VPS_HOST=\"[0-9]" "$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" # Проверка на "битый" (пустой) конфиг после неудачной миграции (1.0.7-1.1.0) local broken=0 [ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=\"[0-9]" "$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=\"[0-9]" "$CONF_FILE"; then msg "Восстановление конфигурации VPS..." . "$CONF_FILE" cat > "$VPS_DIR/default.conf" < "$CONF_FILE.tmp" </dev/null 2>&1; then err "Утилита sshpass не найдена. Авторизация по паролю невозможна." err "Пожалуйста, перенастройте rProxy на использование SSH-ключа (rproxy setup)." return 1 fi sshpass -p "$VPS_PASS" $ssh_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@" else $ssh_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@" fi } scp_cmd() { local scp_exec="/opt/bin/scp" [ ! -f "$scp_exec" ] && scp_exec="scp" if [ "$VPS_AUTH" = "password" ]; then if ! command -v sshpass >/dev/null 2>&1; then err "Утилита sshpass не найдена. Копирование по паролю невозможно." return 1 fi sshpass -p "$VPS_PASS" $scp_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -P "$VPS_PORT" "$@" else $scp_exec -q -o StrictHostKeyChecking=no -o LogLevel=ERROR -i "$SSH_KEY" -P "$VPS_PORT" "$@" fi } 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 "TUNNEL_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" draw_separator printf " ${BOLD}4)${NC} ▶️ Запустить туннель\n" printf " ${BOLD}5)${NC} ⏹️ Остановить туннель\n" printf " ${BOLD}6)${NC} 🔄 Перезапустить туннель\n" draw_separator printf " ${BOLD}7)${NC} 🔒 Получить/Обновить SSL (Certbot)\n" printf " ${BOLD}8)${NC} ⚙️ Настройки VPS\n" printf " ${BOLD}9)${NC} 🚀 Обновить rProxy\n" printf " ${BOLD}10)${NC} 🏥 Проверка VPS (Health)\n" printf " ${BOLD}0)${NC} 🚪 Выход\n" prompt "Выберите действие: " case "$REPLY" in 1) show_status ;; 2) do_add_interactive ;; 3) do_remove_interactive ;; 4) do_start_interactive ;; 5) do_stop_interactive ;; 6) do_restart_interactive ;; 7) do_ssl_interactive ;; 8) do_setup ;; 9) do_self_update ;; 10) 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" # Пытаемся получить 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 basename "$f" .conf return 0 fi done return 1 } select_vps_interactive() { 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" ) vps_list="$vps_list $(basename "$f" .conf)" done local next_idx=$((idx + 1)) printf " ${BOLD}%d)${NC} ➕ Добавить новый VPS\n" "$next_idx" prompt "Выберите номер: " if [ "$REPLY" = "$next_idx" ]; then do_add_vps # Возвращаем последний созданный (упрощение) ls -t "$VPS_DIR"/*.conf | head -n 1 | xargs basename | sed 's/\.conf//' return 0 fi local sel=$(echo "$vps_list" | cut -d' ' -f"$REPLY") echo "$sel" } # ══════════════════════════════════════════════════════════════════════ # ДОБАВИТЬ СЕРВИС (интерактивно) # ══════════════════════════════════════════════════════════════════════ do_add_interactive() { clear_screen draw_box "Добавить новый сервис" printf "\n" prompt "Название сервиса (латиницей, без пробелов): " local name=$(echo "$REPLY" | 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="$REPLY" [ -z "$domain" ] && { warn "Домен не указан"; pause; return; } 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..." vps_id=$(find_vps_by_domain "$domain") if [ -n "$vps_id" ]; then msg "Домен $domain указывает на ваш VPS '$vps_id'. Выбираю его." else warn "Не удалось автоматически определить VPS для $domain" fi else ext_port=$(next_free_port) prompt "Внешний порт [$ext_port]: " ext_port="${REPLY:-$ext_port}" fi # Если VPS не определен автоматически — выбираем вручную if [ -z "$vps_id" ]; then vps_id=$(select_vps_interactive) [ -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" draw_separator prompt "Всё верно? Добавить сервис? (д/н) [д]: " [ "${REPLY:-д}" != "д" ] && [ "${REPLY:-д}" != "y" ] && { msg "Отменено"; pause; return; } msg "Добавляю сервис '$name'..." # Конфиг nginx (упрощенная генерация) local nginx_conf if [ -n "$domain" ]; then nginx_conf="server { listen 80; server_name $domain; location / { proxy_pass http://127.0.0.1:$tunnel_port; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; } }" else nginx_conf="server { listen $ext_port; location / { proxy_pass http://127.0.0.1:$tunnel_port; proxy_set_header Host \$host; } }" fi # Деплой local tmp="/tmp/rproxy_$name.conf" echo "$nginx_conf" > "$tmp" scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_$name.conf" || { err "Ошибка деплоя nginx"; pause; return; } 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 # Сохранение конфига сервиса cat > "$SERVICES_DIR/$name.conf" <> "$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" local services="" local idx=0 for f in "$SERVICES_DIR"/*.conf; do [ -f "$f" ] || continue idx=$((idx + 1)) . "$f" local st="○" is_running "$SVC_NAME" && st="●" printf " ${BOLD}%d)${NC} %s %s (%s:%s)\n" "$idx" "$st" "$SVC_NAME" "$SVC_TARGET_HOST" "$SVC_TARGET_PORT" [ -z "$services" ] && services="$SVC_NAME" || services="$services $SVC_NAME" done if [ "$idx" -eq 0 ]; then printf " ${YELLOW}Нет сервисов для удаления.${NC}\n" pause return fi printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите сервис для удаления: " [ "$REPLY" = "0" ] && return [ -z "$REPLY" ] && return local sel_name sel_name=$(echo "$services" | cut -d' ' -f"$REPLY") [ -z "$sel_name" ] && { warn "Неверный выбор"; pause; return; } prompt "Удалить сервис '$sel_name'? (д/н) [н]: " local confirm="${REPLY:-н}" case "$confirm" in д|Д|y|Y|да|yes) ;; *) msg "Отменено"; pause; return ;; esac load_service "$sel_name" || { pause; return; } is_running "$sel_name" && do_stop_service "$sel_name" msg "Удаляю конфиг nginx на VPS..." ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$sel_name.conf && nginx -t && systemctl reload nginx" || { warn "Не удалось удалить конфиг nginx на VPS" } rm -f "$SERVICES_DIR/$sel_name.conf" rm -f "$(get_pid_file "$sel_name")" msg "Сервис '$sel_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 local all_idx=$((idx + 1)) printf " ${BOLD}%d)${NC} Все сервисы\n" "$all_idx" printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите сервис: " [ "$REPLY" = "0" ] && { SELECTED_SERVICE=""; return 1; } if [ "$REPLY" = "$all_idx" ]; then SELECTED_SERVICE="__ALL__" return 0 fi # Извлечение имени сервиса по индексу (1-based) SELECTED_SERVICE=$(echo "$services" | cut -d' ' -f"$REPLY") [ -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") local ssh_opts="-o StrictHostKeyChecking=no -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes" if [ "$VPS_AUTH" = "password" ]; then AUTOSSH_GATETIME=0 sshpass -p "$VPS_PASS" autossh -M 0 -f -N \ $ssh_opts \ -p "$VPS_PORT" \ -R "0.0.0.0:$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" \ "$VPS_USER@$VPS_HOST" & else AUTOSSH_GATETIME=0 autossh -M 0 -f -N \ $ssh_opts \ -i "$SSH_KEY" \ -p "$VPS_PORT" \ -R "0.0.0.0:$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" \ "$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' остановлен" } 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 local n_idx=$((idx + 1)) local d_idx=$((idx + 2)) draw_separator printf " ${BOLD}%d)${NC} ➕ Добавить новый VPS\n" "$n_idx" [ "$idx" -gt 0 ] && printf " ${BOLD}%d)${NC} ❌ Удалить VPS\n" "$d_idx" printf " ${BOLD}0)${NC} Назад\n" prompt "Выберите действие: " case "$REPLY" in 0) return ;; "$n_idx") do_add_vps ;; "$d_idx") [ "$idx" -gt 0 ] && do_delete_vps_interactive "$vps_list" ;; [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_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" v_auth="key" v_pass="" if [ -f "$vps_file" ]; then . "$vps_file" v_host="$VPS_HOST"; v_port="$VPS_PORT"; v_user="$VPS_USER"; v_auth="$VPS_AUTH"; v_pass="$VPS_PASS" 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}" printf "\n Метод авторизации:\n" printf " ${BOLD}1)${NC} SSH-ключ (рекомендуется)\n" printf " ${BOLD}2)${NC} Пароль\n" prompt "Выберите [${v_auth/key/1}${v_auth/password/2}]: " local choice="${REPLY:-${v_auth/key/1}${v_auth/password/2}}" if [ "$choice" = "2" ]; then v_auth="password" prompt "SSH пароль: " v_pass="$REPLY" else v_auth="key" # Проверка ключа (код ниже требует наличия SSH_KEY) ensure_ssh_key fi msg "Проверяю подключение к $v_host..." local ssh_exec="/opt/bin/ssh" [ ! -f "$ssh_exec" ] && ssh_exec="ssh" if [ "$v_auth" = "password" ]; then if ! command -v sshpass >/dev/null 2>&1; then err "Утилита sshpass не найдена." pause; return fi if ! sshpass -p "$v_pass" $ssh_exec -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p "$v_port" "$v_user@$v_host" "echo ok" >/dev/null 2>&1; then err "Ошибка подключения." pause; return fi else # Копирование ключа local copy_id="/opt/bin/ssh-copy-id" [ ! -f "$copy_id" ] && copy_id="ssh-copy-id" printf " Введите пароль VPS для копирования ключа:\n" $copy_id -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 fi # Сохраняем cat > "$vps_file" </dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq nginx || yum install -y nginx 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 " 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 # Поддержка прямых команд для 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) ( load_service "$name" >/dev/null 2>&1 local state="остановлен" is_running "$SVC_NAME" && state="работает" local info="$SVC_NAME $SVC_TARGET_HOST:$SVC_TARGET_PORT → VPS($CUR_VPS_ID):$SVC_TUNNEL_PORT" [ -n "$SVC_DOMAIN" ] && info="$info ($SVC_DOMAIN)" echo "$info [$state]" ) done ;; -v|--version) echo "rProxy v$VERSION" ;; "") main_menu ;; *) main_menu ;; esac