rProxy/rproxy

1368 lines
55 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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" <<EOF
VPS_HOST="$VPS_HOST"
VPS_PORT="$VPS_PORT"
VPS_USER="$VPS_USER"
VPS_AUTH="$VPS_AUTH"
VPS_PASS="$VPS_PASS"
EOF
# Очищаем rproxy.conf от старых системных переменных
cat > "$CONF_FILE.tmp" <<EOF
CERTBOT_EMAIL="$CERTBOT_EMAIL"
EOF
mv "$CONF_FILE.tmp" "$CONF_FILE"
fi
[ -f "$VPS_DIR/default.conf" ] && chmod 600 "$VPS_DIR/default.conf"
fi
# Если профиль default остался битым и восстановить нечего — удаляем его,
# чтобы check_conf сработал и предложил настройку заново
if [ -f "$VPS_DIR/default.conf" ] && ! grep -q "VPS_HOST=\"[0-9]" "$VPS_DIR/default.conf"; then
rm -f "$VPS_DIR/default.conf"
fi
}
ssh_cmd() {
local ssh_exec="/opt/bin/ssh"
[ ! -f "$ssh_exec" ] && ssh_exec="ssh"
if [ "$VPS_AUTH" = "password" ]; then
if ! command -v sshpass >/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" <<EOF
SVC_NAME="$name"
SVC_VPS="$vps_id"
SVC_TARGET_HOST="$t_host"
SVC_TARGET_PORT="$t_port"
SVC_TUNNEL_PORT="$tunnel_port"
SVC_EXT_PORT="$ext_port"
SVC_DOMAIN="$domain"
SVC_SSL="$use_ssl"
SVC_NDM_AUTH="$use_ndm_auth"
SVC_ROUTER_IP="$router_ip"
SVC_ENABLED="yes"
EOF
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"
msg "Настройки обновлены. Перезапускаю туннель..."
if is_running "$name"; then
do_stop_service "$name"
sleep 1
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 && 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" <<EOF
VPS_HOST="$v_host"
VPS_PORT="$v_port"
VPS_USER="$v_user"
VPS_AUTH="$v_auth"
VPS_PASS="$v_pass"
EOF
chmod 600 "$vps_file"
msg "Профиль '$vps_id' сохранён."
# Настройка Nginx на новом VPS
msg "Настройка окружения на VPS..."
VPS_HOST="$v_host" VPS_PORT="$v_port" VPS_USER="$v_user" VPS_AUTH="$v_auth" VPS_PASS="$v_pass"
ssh_cmd "
if ! command -v nginx >/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