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