#!/bin/sh # rProxy — Менеджер обратного прокси для роутеров Keenetic # Публикация локальных сервисов через SSH-туннели + nginx на VPS # http://5.104.75.50:3000/Petro1990/rProxy VERSION="1.3.6" 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" 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 "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" } enable_router_basic_auth() { msg "Настраиваю роутер на использование Basic Auth для совместимости..." # Для новых версий KeeneticOS ndmq -p "web-server auth basic" >/dev/null 2>&1 # Для старых версий или альтернативных API ndmq -p "ip http auth basic" >/dev/null 2>&1 # Принудительно задаем realm ndmq -p "web-server realm Keenetic" >/dev/null 2>&1 # ОЧЕНЬ ВАЖНО: Сохраняем конфигурацию ndmq -p "system configuration save" >/dev/null 2>&1 } 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 ' ' '_' | 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..." 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" case "$REPLY" in д|Д|y|Y|да|yes) use_ndm_auth="yes" enable_router_basic_auth ;; esac draw_separator prompt "Всё верно? Добавить сервис? (д/н) [д]: " [ "${REPLY:-д}" != "д" ] && [ "${REPLY:-д}" != "y" ] && { msg "Отменено"; pause; return; } msg "Добавляю сервис '$name'..." # Стелс-режим: прикидываемся локальным запросом local stealth_host="$t_host" [ "$t_port" != "80" ] && stealth_host="$t_host:$t_port" # Конфигурация авторизации local auth_config="" local router_ip="127.0.0.1" if [ "$use_ndm_auth" = "yes" ]; then router_ip=$(get_router_ip) local auth_port=$((tunnel_port + 1)) auth_config=" location /rproxy_auth { internal; proxy_pass http://127.0.0.1:$auth_port/rci/system/hostname; proxy_pass_request_body off; proxy_set_header Content-Length \"\"; proxy_set_header Authorization \$http_authorization; # Стелс-режим для авторизации (используем LAN IP роутера) proxy_set_header Host \"$router_ip\"; proxy_set_header Origin \"http://$router_ip\"; proxy_set_header Referer \"http://$router_ip/\"; proxy_set_header X-NDM-Realm \"Keenetic\"; } location @auth_required { add_header WWW-Authenticate 'Basic realm=\"Keenetic\"' always; return 401; } " fi # Генерация конфига nginx local tmp="/tmp/rproxy_$name.conf" if [ -n "$domain" ]; then cat > "$tmp" << NGINXEOF server { listen 80; server_name "$domain"; recursive_error_pages on; proxy_buffering off; proxy_request_buffering off; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; location / { $( [ "$use_ndm_auth" = "yes" ] && echo "auth_request /rproxy_auth;" ) $( [ "$use_ndm_auth" = "yes" ] && echo "error_page 401 403 = @auth_required;" ) 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_set_header X-Forwarded-Host \$http_host; proxy_set_header X-Forwarded-Port \$server_port; # Трансляция куки: меняем локальный IP обратно на домен в браузере proxy_cookie_domain "$t_host" "\$host"; proxy_cookie_path / "/; SameSite=Lax"; proxy_hide_header X-Frame-Options; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } $auth_config } NGINXEOF else cat > "$tmp" << NGINXEOF server { listen $ext_port; recursive_error_pages on; proxy_buffering off; proxy_request_buffering off; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; location / { $( [ "$use_ndm_auth" = "yes" ] && echo "auth_request /rproxy_auth;" ) $( [ "$use_ndm_auth" = "yes" ] && echo "error_page 401 403 = @auth_required;" ) 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_set_header X-Forwarded-Host \$http_host; proxy_set_header X-Forwarded-Port \$server_port; # Трансляция куки: меняем локальный IP обратно на домен в браузере proxy_cookie_domain "$t_host" "\$host"; proxy_cookie_path / "/; SameSite=Lax"; proxy_hide_header X-Frame-Options; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } $auth_config } NGINXEOF fi # Деплой scp_cmd "$tmp" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_$name.conf" || { err "Ошибка деплоя nginx"; rm -f "$tmp"; 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" 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 && nginx -t && systemctl reload nginx" >/dev/null 2>&1 || { warn "Не удалось удалить конфиг nginx на 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" [ "$SVC_NDM_AUTH" = "yes" ] && ssh_cmd "fuser -k $((SVC_TUNNEL_PORT+1))/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" [ "$SVC_NDM_AUTH" = "yes" ] && tunnel_args="$tunnel_args -R 0.0.0.0:$((SVC_TUNNEL_PORT+1)):${SVC_ROUTER_IP:-127.0.0.1}:80" 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' остановлен" } 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" 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" ;; [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 # Оптимизация 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 # Поддержка прямых команд для 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