rProxy/rproxy

1060 lines
42 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.0.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
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 профиль
[ "$(ls -A "$VPS_DIR" 2>/dev/null)" ] || return 1
return 0
}
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"
if [ -f "$CONF_FILE" ] && [ ! -f "$VPS_DIR/default.conf" ]; then
msg "Миграция конфигурации в новый формат..."
# Копируем параметры в профиль default
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
# В основном конфиге оставляем только глобальные настройки
cat > "$CONF_FILE.tmp" <<EOF
CERTBOT_EMAIL="$CERTBOT_EMAIL"
EOF
mv "$CONF_FILE.tmp" "$CONF_FILE"
chmod 600 "$VPS_DIR/default.conf" "$CONF_FILE"
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 "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"
msg "Отладка: Резолвлю $dom..." >&2
# Пытаемся получить 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)
msg "Отладка: Получен IP: '$ip'" >&2
[ -z "$ip" ] && return 1
for f in "$VPS_DIR"/*.conf; do
[ -f "$f" ] || continue
(
VPS_HOST=""
. "$f"
msg "Отладка: Сравниваю с VPS '$(basename "$f" .conf)' (HOST: $VPS_HOST)" >&2
if [ "$VPS_HOST" = "$ip" ]; then
basename "$f" .conf
exit 0
fi
exit 1
) && { basename "$f" .conf; return 0; }
done
return 1
}
select_vps_interactive() {
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))
(
. "$f"
printf " ${BOLD}%d)${NC} %s (${DIM}%s${NC})\n" "$idx" "$(basename "$f" .conf)" "$VPS_HOST"
)
vps_list="$vps_list $(basename "$f" .conf)"
done
printf " ${BOLD}n)${NC} Добавить новый VPS\n"
prompt "Выберите номер или 'n': "
if [ "$REPLY" = "n" ] || [ "$REPLY" = "N" ]; 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" <<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_ENABLED="yes"
EOF
msg "Сервис '$name' добавлен на VPS '$vps_id'!"
do_start_service "$name"
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"
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
printf " ${BOLD}a)${NC} Все сервисы\n"
printf " ${BOLD}0)${NC} Назад\n"
prompt "Выберите сервис: "
[ "$REPLY" = "0" ] && { SELECTED_SERVICE=""; return 1; }
if [ "$REPLY" = "a" ] || [ "$REPLY" = "A" ] || [ "$REPLY" = "а" ] || [ "$REPLY" = "А" ]; 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"
printf " ${BOLD}%d)${NC} %s (${DIM}%s@%s:%s${NC})\n" "$idx" "$(basename "$f" .conf)" "$VPS_USER" "$VPS_HOST" "$VPS_PORT"
vps_list="$vps_list $(basename "$f" .conf)"
done
if [ "$idx" -eq 0 ]; then
printf " ${YELLOW}Нет настроенных VPS серверов.${NC}\n"
fi
draw_separator
printf " ${BOLD}n)${NC} Добавить новый VPS\n"
[ "$idx" -gt 0 ] && printf " ${BOLD}d)${NC} ❌ Удалить VPS\n"
printf " ${BOLD}0)${NC} Назад\n"
prompt "Выберите действие: "
case "$REPLY" in
0) return ;;
n|N) do_add_vps ;;
d|D) [ "$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
"
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