From f336aec6d5980912fa75b014716a1b460273921b Mon Sep 17 00:00:00 2001 From: Petro1990 Date: Fri, 13 Mar 2026 14:20:39 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20rProxy=20=E2=80=94=20=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=B4=D0=B6=D0=B5=D1=80=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20Keenetic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 147 ++++++++++++ S98rproxy | 40 ++++ install.sh | 100 +++++++++ rproxy | 647 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 934 insertions(+) create mode 100644 README.md create mode 100644 S98rproxy create mode 100644 install.sh create mode 100644 rproxy diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdf3ffa --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# 🔄 rProxy + +**Менеджер обратного прокси для роутеров Keenetic** + +Публикация локальных сервисов в интернет через VPS с nginx в качестве reverse proxy. + +``` +[LAN сервис] ← [Keenetic] ══SSH туннель══▶ [VPS:nginx] → Интернет +``` + +## ⚡ Быстрый старт + +### 1. Установка (на роутере Keenetic) + +> Требуется [Entware](https://help.keenetic.com/hc/ru/articles/360021214160) + +```bash +curl -sSL http://5.104.75.50:3000/Petro1990/rProxy/raw/branch/main/install.sh | sh +``` + +### 2. Настройка VPS + +```bash +rproxy setup +``` + +Скрипт запросит: +- **IP-адрес VPS** — адрес вашего сервера +- **SSH порт** — порт SSH (по умолчанию 22) +- **SSH пользователь** — логин (по умолчанию root) +- **Метод авторизации** — SSH-ключ или пароль + +При выборе SSH-ключа скрипт автоматически сгенерирует ключ и скопирует его на VPS. Также будет автоматически установлен и настроен nginx. + +### 3. Публикация сервиса + +**С доменом** (порт 80): +```bash +rproxy add nas 192.168.1.100:5000 --domain nas.example.com +``` + +**Без домена** (произвольный порт): +```bash +rproxy add camera 192.168.1.50:8080 --port 9090 +``` + +**С автоподбором порта:** +```bash +rproxy add homeassistant 192.168.1.10:8123 +``` + +## 📋 Команды + +| Команда | Описание | +|---------|----------| +| `rproxy setup` | Настроить подключение к VPS | +| `rproxy add <имя> <хост:порт> [опции]` | Добавить сервис | +| `rproxy remove <имя>` | Удалить сервис | +| `rproxy start [имя]` | Запустить туннель(и) | +| `rproxy stop [имя]` | Остановить туннель(и) | +| `rproxy restart [имя]` | Перезапустить туннель(и) | +| `rproxy status` | Статус всех сервисов | +| `rproxy list` | Краткий список | +| `rproxy enable <имя>` | Включить автозапуск | +| `rproxy disable <имя>` | Выключить автозапуск | +| `rproxy logs <имя>` | Логи nginx на VPS | + +### Опции для `add` + +| Опция | Описание | +|-------|----------| +| `--domain`, `-d` | Привязать к домену (nginx слушает порт 80) | +| `--port`, `-p` | Указать внешний порт (без домена) | + +## 🏗 Архитектура + +### На роутере (Keenetic + Entware) +- **autossh** — устанавливает устойчивые reverse SSH-туннели до VPS +- Каждый сервис = отдельный туннель +- Конфигурация: `/opt/etc/rproxy/` +- PID-файлы: `/opt/var/run/rproxy/` + +### На VPS +- **nginx** — принимает входящие запросы и проксирует в SSH-туннели +- Конфиги сервисов: `/etc/nginx/sites-enabled/rproxy_*.conf` + +### Файлы конфигурации + +``` +/opt/etc/rproxy/ +├── rproxy.conf # Параметры подключения к VPS +└── services/ + ├── nas.conf # Конфиг сервиса "nas" + ├── camera.conf # Конфиг сервиса "camera" + └── ... +``` + +## 🔒 Безопасность + +- SSH-ключ хранится в `/opt/etc/rproxy/id_rsa` (права 600) +- Пароль VPS хранится в `/opt/etc/rproxy/rproxy.conf` (права 600) +- Все соединения зашифрованы через SSH + +> **Рекомендация:** используйте авторизацию по SSH-ключу для лучшей безопасности. + +## 🔧 Решение проблем + +### Туннель не подключается +```bash +# Проверьте статус +rproxy status + +# Перезапустите туннель +rproxy restart <имя> + +# Проверьте SSH-подключение вручную +ssh -i /opt/etc/rproxy/id_rsa -p 22 root@ +``` + +### Сервис не доступен по домену +- Убедитесь, что DNS-запись домена указывает на IP VPS +- Проверьте логи: `rproxy logs <имя>` +- Проверьте nginx на VPS: `ssh root@VPS 'nginx -t'` + +### Автозапуск не работает +```bash +# Проверьте init-скрипт +ls -la /opt/etc/init.d/S98rproxy + +# Проверьте, что сервис enabled +rproxy list +``` + +## 📦 Требования + +### Роутер +- Keenetic с установленным [Entware](https://help.keenetic.com/hc/ru/articles/360021214160) +- Пакеты: `openssh-client`, `autossh`, `curl`, `sshpass` + +### VPS +- Linux (Debian/Ubuntu/CentOS) +- nginx (устанавливается автоматически при `setup`) +- SSH-доступ + +## 📝 Лицензия + +MIT diff --git a/S98rproxy b/S98rproxy new file mode 100644 index 0000000..64cae86 --- /dev/null +++ b/S98rproxy @@ -0,0 +1,40 @@ +#!/bin/sh +# rProxy init script for Entware (Keenetic) +# Autostart/stop SSH tunnels on boot/shutdown + +ENABLED=yes +PROCS="rproxy" +DESC="rProxy reverse proxy tunnels" + +start() { + echo "Starting $DESC..." + /opt/bin/rproxy start +} + +stop() { + echo "Stopping $DESC..." + /opt/bin/rproxy stop +} + +restart() { + stop + sleep 2 + start +} + +status() { + /opt/bin/rproxy status +} + +case "$1" in + start) start ;; + stop) stop ;; + restart) restart ;; + status) status ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..b4f0b18 --- /dev/null +++ b/install.sh @@ -0,0 +1,100 @@ +#!/bin/sh +# rProxy Installer for Keenetic Routers (Entware) +# Usage: curl -sSL http://5.104.75.50:3000/Petro1990/rProxy/raw/branch/main/install.sh | sh + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +REPO_URL="http://5.104.75.50:3000/Petro1990/rProxy/raw/branch/main" +INSTALL_DIR="/opt/bin" +CONF_DIR="/opt/etc/rproxy" +INIT_DIR="/opt/etc/init.d" + +msg() { printf "${GREEN}▸${NC} %s\n" "$*"; } +err() { printf "${RED}✖${NC} %s\n" "$*" >&2; } + +header() { + printf "\n" + printf "${CYAN}${BOLD}╔══════════════════════════════════════════╗${NC}\n" + printf "${CYAN}${BOLD}║ rProxy — Installer for Keenetic ║${NC}\n" + printf "${CYAN}${BOLD}╚══════════════════════════════════════════╝${NC}\n" + printf "\n" +} + +header + +# ─── Check Entware ─────────────────────────────────────────────────── +if [ ! -d "/opt/bin" ] || ! command -v opkg >/dev/null 2>&1; then + err "Entware не найден!" + err "Установите Entware на роутер: https://help.keenetic.com/hc/ru/articles/360021214160" + exit 1 +fi + +msg "Entware обнаружен ✓" + +# ─── Install dependencies ─────────────────────────────────────────── +msg "Проверяю зависимости..." + +install_pkg() { + local pkg="$1" + if ! opkg list-installed 2>/dev/null | grep -q "^$pkg "; then + msg "Устанавливаю $pkg..." + opkg update >/dev/null 2>&1 + opkg install "$pkg" || { + err "Не удалось установить $pkg" + exit 1 + } + else + msg "$pkg уже установлен ✓" + fi +} + +install_pkg "openssh-client" +install_pkg "autossh" +install_pkg "curl" +install_pkg "sshpass" + +# ─── Download rproxy script ───────────────────────────────────────── +msg "Скачиваю rproxy..." +curl -sSL "$REPO_URL/rproxy" -o "$INSTALL_DIR/rproxy" || { + err "Не удалось скачать rproxy" + exit 1 +} +chmod +x "$INSTALL_DIR/rproxy" +msg "rproxy установлен в $INSTALL_DIR/rproxy ✓" + +# ─── Download init script ─────────────────────────────────────────── +msg "Устанавливаю init-скрипт для автозапуска..." +curl -sSL "$REPO_URL/S98rproxy" -o "$INIT_DIR/S98rproxy" || { + err "Не удалось скачать init-скрипт" + exit 1 +} +chmod +x "$INIT_DIR/S98rproxy" +msg "Init-скрипт установлен ✓" + +# ─── Create directories ───────────────────────────────────────────── +mkdir -p "$CONF_DIR/services" +mkdir -p "/opt/var/run/rproxy" + +# ─── Done ──────────────────────────────────────────────────────────── +printf "\n" +printf "${GREEN}${BOLD}══════════════════════════════════════════${NC}\n" +printf "${GREEN}${BOLD} rProxy успешно установлен!${NC}\n" +printf "${GREEN}${BOLD}══════════════════════════════════════════${NC}\n" +printf "\n" +printf " Следующий шаг — настройте подключение к VPS:\n" +printf "\n" +printf " ${CYAN}rproxy setup${NC}\n" +printf "\n" +printf " Затем добавьте сервис:\n" +printf "\n" +printf " ${CYAN}rproxy add myservice 192.168.1.100:8080 --domain my.domain.com${NC}\n" +printf " ${CYAN}rproxy add camera 192.168.1.50:554 --port 9554${NC}\n" +printf "\n" +printf " Полная справка: ${CYAN}rproxy --help${NC}\n" +printf "\n" diff --git a/rproxy b/rproxy new file mode 100644 index 0000000..716aa64 --- /dev/null +++ b/rproxy @@ -0,0 +1,647 @@ +#!/bin/sh +# rProxy — Менеджер обратного прокси для роутеров Keenetic +# Публикация локальных сервисов через SSH-туннели + nginx на VPS +# http://5.104.75.50:3000/Petro1990/rProxy + +VERSION="1.0.0" +CONF_DIR="/opt/etc/rproxy" +CONF_FILE="$CONF_DIR/rproxy.conf" +SERVICES_DIR="$CONF_DIR/services" +PID_DIR="/opt/var/run/rproxy" +SSH_KEY="$CONF_DIR/id_rsa" +REMOTE_NGINX_DIR="/etc/nginx/sites-enabled" +REMOTE_NGINX_AVAILABLE="/etc/nginx/sites-available" +BASE_TUNNEL_PORT=10000 + +# ─── Colors ─────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +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" "$*"; } + +# ─── Helpers ────────────────────────────────────────────────────────── +check_conf() { + if [ ! -f "$CONF_FILE" ]; then + err "Конфигурация не найдена. Запустите: rproxy setup" + exit 1 + fi + . "$CONF_FILE" +} + +load_service() { + local svc_file="$SERVICES_DIR/$1.conf" + if [ ! -f "$svc_file" ]; then + err "Сервис '$1' не найден" + exit 1 + fi + . "$svc_file" +} + +ssh_cmd() { + if [ "$VPS_AUTH" = "password" ]; then + sshpass -p "$VPS_PASS" ssh -o StrictHostKeyChecking=no -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@" + else + ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "$@" + fi +} + +scp_cmd() { + if [ "$VPS_AUTH" = "password" ]; then + sshpass -p "$VPS_PASS" scp -o StrictHostKeyChecking=no -P "$VPS_PORT" "$@" + else + scp -o StrictHostKeyChecking=no -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 2>/dev/null; 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 +} + +# ─── SETUP ──────────────────────────────────────────────────────────── +do_setup() { + header "rProxy — Настройка подключения к VPS" + + printf "IP-адрес VPS: " + read -r vps_host + printf "SSH порт [22]: " + read -r vps_port + vps_port=${vps_port:-22} + printf "SSH пользователь [root]: " + read -r vps_user + vps_user=${vps_user:-root} + + printf "\nМетод авторизации:\n" + printf " 1) SSH-ключ (рекомендуется)\n" + printf " 2) Пароль\n" + printf "Выберите [1]: " + read -r auth_choice + auth_choice=${auth_choice:-1} + + local vps_auth="key" + local vps_pass="" + + if [ "$auth_choice" = "2" ]; then + vps_auth="password" + printf "SSH пароль: " + stty -echo 2>/dev/null + read -r vps_pass + stty echo 2>/dev/null + printf "\n" + + # Check sshpass + if ! command -v sshpass >/dev/null 2>&1; then + msg "Устанавливаю sshpass..." + opkg install sshpass 2>/dev/null || { + err "Не удалось установить sshpass. Установите вручную: opkg install sshpass" + exit 1 + } + fi + else + # Generate SSH key if not exists + if [ ! -f "$SSH_KEY" ]; then + msg "Генерирую SSH-ключ..." + mkdir -p "$CONF_DIR" + ssh-keygen -t rsa -b 4096 -f "$SSH_KEY" -N "" -q + msg "Ключ создан: $SSH_KEY" + fi + fi + + # Test connection + msg "Проверяю подключение к $vps_host..." + if [ "$vps_auth" = "password" ]; then + if ! sshpass -p "$vps_pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + -p "$vps_port" "$vps_user@$vps_host" "echo ok" >/dev/null 2>&1; then + err "Не удалось подключиться к VPS. Проверьте параметры." + exit 1 + fi + else + # Copy key to VPS + msg "Копирую SSH-ключ на VPS..." + printf "Введите пароль VPS для копирования ключа:\n" + ssh-copy-id -i "$SSH_KEY.pub" -p "$vps_port" "$vps_user@$vps_host" 2>/dev/null || { + # Fallback: manual copy + cat "$SSH_KEY.pub" | ssh -p "$vps_port" "$vps_user@$vps_host" \ + "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" + } + + if ! ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + -i "$SSH_KEY" -p "$vps_port" "$vps_user@$vps_host" "echo ok" >/dev/null 2>&1; then + err "Не удалось подключиться к VPS по ключу." + exit 1 + fi + fi + msg "Подключение успешно!" + + # Setup nginx on VPS + msg "Настраиваю nginx на VPS..." + ssh_cmd_raw() { + if [ "$vps_auth" = "password" ]; then + sshpass -p "$vps_pass" ssh -o StrictHostKeyChecking=no -p "$vps_port" "$vps_user@$vps_host" "$@" + else + ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" -p "$vps_port" "$vps_user@$vps_host" "$@" + fi + } + + ssh_cmd_raw " + if ! command -v nginx >/dev/null 2>&1; then + echo 'Устанавливаю nginx...' + apt-get update -qq && apt-get install -y -qq nginx >/dev/null 2>&1 || \ + yum install -y nginx >/dev/null 2>&1 || { + echo 'FAIL: не удалось установить nginx' + exit 1 + } + fi + mkdir -p $REMOTE_NGINX_DIR + mkdir -p /etc/nginx/rproxy + # Ensure sites-enabled is included + if ! grep -q 'include.*sites-enabled' /etc/nginx/nginx.conf 2>/dev/null; then + sed -i '/http {/a\\ include /etc/nginx/sites-enabled/*.conf;' /etc/nginx/nginx.conf 2>/dev/null + fi + # Enable and start nginx + systemctl enable nginx 2>/dev/null + systemctl start nginx 2>/dev/null + echo 'OK' + " || { + warn "Не удалось автоматически настроить nginx. Убедитесь, что nginx установлен на VPS." + } + + # Save config + mkdir -p "$CONF_DIR" "$SERVICES_DIR" "$PID_DIR" + cat > "$CONF_FILE" < <хост:порт>" +} + +# ─── ADD ────────────────────────────────────────────────────────────── +do_add() { + local name="$1" + local target="$2" + shift 2 + + if [ -z "$name" ] || [ -z "$target" ]; then + err "Использование: rproxy add <имя> <хост:порт> [--domain example.com] [--port 8080]" + exit 1 + fi + + check_conf + + if [ -f "$SERVICES_DIR/$name.conf" ]; then + err "Сервис '$name' уже существует. Удалите его сначала: rproxy remove $name" + exit 1 + fi + + local domain="" + local ext_port="" + + while [ $# -gt 0 ]; do + case "$1" in + --domain|-d) + domain="$2" + shift 2 + ;; + --port|-p) + ext_port="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + + local tunnel_port + tunnel_port=$(next_free_port) + + # Determine external port + if [ -n "$domain" ]; then + ext_port=80 + elif [ -z "$ext_port" ]; then + ext_port=$tunnel_port + fi + + local target_host="${target%:*}" + local target_port="${target#*:}" + + # Validate target + if [ "$target_host" = "$target_port" ]; then + # No colon — assume localhost + target_port="$target_host" + target_host="127.0.0.1" + fi + + msg "Добавляю сервис '$name'..." + msg " Цель: $target_host:$target_port" + msg " Туннель порт: $tunnel_port" + [ -n "$domain" ] && msg " Домен: $domain" + msg " Внешний порт: $ext_port" + + # Generate nginx config + local nginx_conf="" + if [ -n "$domain" ]; then + nginx_conf="# rProxy: $name +server { + listen 80; + server_name $domain; + + location / { + 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 \$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_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} +" + else + nginx_conf="# rProxy: $name +server { + listen $ext_port; + + location / { + 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 \$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_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } +} +" + fi + + # Deploy nginx config to VPS + local tmp_file="/tmp/rproxy_nginx_$name.conf" + printf '%s' "$nginx_conf" > "$tmp_file" + scp_cmd "$tmp_file" "$VPS_USER@$VPS_HOST:$REMOTE_NGINX_DIR/rproxy_$name.conf" || { + err "Не удалось загрузить конфиг nginx на VPS" + rm -f "$tmp_file" + exit 1 + } + rm -f "$tmp_file" + + # Reload nginx + ssh_cmd "nginx -t && systemctl reload nginx" || { + err "Ошибка конфигурации nginx на VPS" + ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$name.conf" + exit 1 + } + + # Save service config + cat > "$SERVICES_DIR/$name.conf" <"; exit 1; } + + check_conf + load_service "$name" + + # Stop tunnel if running + is_running "$name" && do_stop "$name" + + # Remove nginx config from VPS + msg "Удаляю конфиг nginx на VPS..." + ssh_cmd "rm -f $REMOTE_NGINX_DIR/rproxy_$name.conf && nginx -t && systemctl reload nginx" || { + warn "Не удалось удалить конфиг nginx на VPS" + } + + # Remove local config + rm -f "$SERVICES_DIR/$name.conf" + rm -f "$(get_pid_file "$name")" + + msg "Сервис '$name' удалён" +} + +# ─── START ──────────────────────────────────────────────────────────── +do_start() { + local name="$1" + + check_conf + + if [ -z "$name" ]; then + # Start all enabled services + local count=0 + for f in "$SERVICES_DIR"/*.conf; do + [ -f "$f" ] || continue + local svc_name + svc_name=$(basename "$f" .conf) + . "$f" + if [ "$SVC_ENABLED" = "yes" ]; then + do_start "$svc_name" + count=$((count + 1)) + fi + done + [ "$count" -eq 0 ] && warn "Нет сервисов для запуска" + return + fi + + load_service "$name" + + if is_running "$name"; then + warn "Сервис '$name' уже запущен" + return + fi + + msg "Запускаю туннель '$name' ($SVC_TARGET_HOST:$SVC_TARGET_PORT → VPS:$SVC_TUNNEL_PORT)..." + + 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 + # With sshpass + 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 + # With key + 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 + + # Find the actual autossh/ssh process + local real_pid + real_pid=$(pgrep -f "autossh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1) + if [ -z "$real_pid" ]; then + real_pid=$(pgrep -f "ssh.*$SVC_TUNNEL_PORT:$SVC_TARGET_HOST:$SVC_TARGET_PORT" 2>/dev/null | head -1) + fi + + if [ -n "$real_pid" ]; then + echo "$real_pid" > "$pid_file" + msg "Туннель '$name' запущен (PID: $real_pid)" + else + echo "$bg_pid" > "$pid_file" + msg "Туннель '$name' запущен (PID: $bg_pid)" + fi +} + +# ─── STOP ───────────────────────────────────────────────────────────── +do_stop() { + local name="$1" + + check_conf + + if [ -z "$name" ]; then + # Stop all + for f in "$SERVICES_DIR"/*.conf; do + [ -f "$f" ] || continue + do_stop "$(basename "$f" .conf)" + done + return + fi + + 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 autossh and child ssh processes + kill "$pid" 2>/dev/null + # Also kill any related ssh tunnels + pkill -f "ssh.*$SVC_TUNNEL_PORT:.*:$SVC_TARGET_PORT" 2>/dev/null + + rm -f "$pid_file" + msg "Туннель '$name' остановлен" +} + +# ─── RESTART ────────────────────────────────────────────────────────── +do_restart() { + local name="$1" + check_conf + + if [ -z "$name" ]; then + for f in "$SERVICES_DIR"/*.conf; do + [ -f "$f" ] || continue + do_restart "$(basename "$f" .conf)" + done + return + fi + + load_service "$name" + do_stop "$name" + sleep 1 + do_start "$name" +} + +# ─── STATUS ─────────────────────────────────────────────────────────── +do_status() { + check_conf + + header "rProxy v$VERSION — Статус сервисов" + printf "\n" + printf "${BOLD}%-15s %-25s %-8s %-10s %-20s${NC}\n" "ИМЯ" "ЦЕЛЬ" "ПОРТ" "СТАТУС" "ДОМЕН" + printf "%-15s %-25s %-8s %-10s %-20s\n" "───────────────" "─────────────────────────" "────────" "──────────" "────────────────────" + + local has_services=0 + for f in "$SERVICES_DIR"/*.conf; do + [ -f "$f" ] || continue + has_services=1 + . "$f" + + local status_text + if is_running "$SVC_NAME"; then + status_text="${GREEN}▲ онлайн${NC}" + else + status_text="${RED}▼ офлайн${NC}" + fi + + local domain_text="${SVC_DOMAIN:-—}" + + printf "%-15s %-25s %-8s ${status_text} %-20s\n" \ + "$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 " Добавьте сервис: ${CYAN}rproxy add <имя> <хост:порт>${NC}\n" + fi + + printf "\n${BOLD}VPS:${NC} $VPS_HOST:$VPS_PORT (${VPS_AUTH})\n\n" +} + +# ─── LIST ───────────────────────────────────────────────────────────── +do_list() { + check_conf + + for f in "$SERVICES_DIR"/*.conf; do + [ -f "$f" ] || continue + . "$f" + local state="остановлен" + is_running "$SVC_NAME" && state="работает" + + local info="$SVC_NAME $SVC_TARGET_HOST:$SVC_TARGET_PORT → :$SVC_EXT_PORT" + [ -n "$SVC_DOMAIN" ] && info="$info ($SVC_DOMAIN)" + info="$info [$state]" + echo "$info" + done +} + +# ─── ENABLE / DISABLE ──────────────────────────────────────────────── +do_enable() { + local name="$1" + [ -z "$name" ] && { err "Использование: rproxy enable <имя>"; exit 1; } + + check_conf + load_service "$name" + + sed -i "s/SVC_ENABLED=.*/SVC_ENABLED=\"yes\"/" "$SERVICES_DIR/$name.conf" + msg "Сервис '$name' добавлен в автозапуск" +} + +do_disable() { + local name="$1" + [ -z "$name" ] && { err "Использование: rproxy disable <имя>"; exit 1; } + + check_conf + load_service "$name" + + sed -i "s/SVC_ENABLED=.*/SVC_ENABLED=\"no\"/" "$SERVICES_DIR/$name.conf" + msg "Сервис '$name' убран из автозапуска" +} + +# ─── LOGS ───────────────────────────────────────────────────────────── +do_logs() { + local name="$1" + [ -z "$name" ] && { err "Использование: rproxy logs <имя>"; exit 1; } + + check_conf + + msg "Логи nginx на VPS для '$name':" + ssh_cmd "grep -i 'rproxy_$name\\|$name' /var/log/nginx/access.log /var/log/nginx/error.log 2>/dev/null | tail -50" +} + +# ─── USAGE ──────────────────────────────────────────────────────────── +usage() { + cat < [аргументы] + +${BOLD}Команды:${NC} + ${GREEN}setup${NC} Настроить подключение к VPS + ${GREEN}add${NC} <имя> <хост:порт> [опции] Добавить сервис + ${GREEN}remove${NC} <имя> Удалить сервис + ${GREEN}start${NC} [имя] Запустить туннель (все/один) + ${GREEN}stop${NC} [имя] Остановить туннель (все/один) + ${GREEN}restart${NC} [имя] Перезапустить туннель + ${GREEN}status${NC} Показать статус сервисов + ${GREEN}list${NC} Список сервисов (краткий) + ${GREEN}enable${NC} <имя> Включить автозапуск + ${GREEN}disable${NC} <имя> Выключить автозапуск + ${GREEN}logs${NC} <имя> Показать логи nginx + +${BOLD}Опции для add:${NC} + --domain, -d <домен> Привязать к домену (порт 80) + --port, -p <порт> Внешний порт (без домена) + +${BOLD}Примеры:${NC} + rproxy setup + rproxy add nas 192.168.1.100:5000 --domain nas.example.com + rproxy add camera 192.168.1.50:8080 --port 9090 + rproxy add homeassistant 192.168.1.10:8123 + rproxy remove nas + rproxy status + +EOF +} + +# ─── MAIN ───────────────────────────────────────────────────────────── +mkdir -p "$CONF_DIR" "$SERVICES_DIR" "$PID_DIR" 2>/dev/null + +case "${1:-}" in + setup) do_setup ;; + add) shift; do_add "$@" ;; + remove|rm|del) shift; do_remove "$@" ;; + start) shift; do_start "$@" ;; + stop) shift; do_stop "$@" ;; + restart) shift; do_restart "$@" ;; + status|st) do_status ;; + list|ls) do_list ;; + enable) shift; do_enable "$@" ;; + disable) shift; do_disable "$@" ;; + logs|log) shift; do_logs "$@" ;; + -v|--version) echo "rProxy v$VERSION" ;; + -h|--help|"") usage ;; + *) err "Неизвестная команда: $1"; usage; exit 1 ;; +esac