diff --git a/docs/deploy.md b/docs/deploy.md index bbe3020..9a5dcac 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,88 +1,86 @@ # Развёртывание сервера -Для работы сервера 24/7 его необходимо настроить как службу. У вас есть два пути: **Docker** (рекомендуется) или классический **systemd**. - -> [!TIP] -> **Автоматическая установка** -> Проще всего развернуть сервер с помощью интерактивного скрипта (выполнит установку Docker/systemd, настройку файрвола и обфускации): -> ```bash -> curl -fsSL https://raw.githubusercontent.com/samosvalishe/free-turn-proxy/master/scripts/install-server.sh -o install-server.sh -> sudo bash install-server.sh -> ``` -> Скрипт идемпотентен: **повторный запуск** обнаружит существующую установку и предложит меню — *обновить версию* (конфиг и ключ сохраняются), *переконфигурировать* или *удалить*. При установке можно выбрать конкретную версию релиза или `latest`. -> -> Если вы хотите всё настроить руками — следуйте инструкциям ниже. +Для работы сервера 24/7 его необходимо настроить как службу. Мы поддерживаем установку через скрипт (интерактивно или автоматически), а также ручную настройку через **Docker** или **systemd**. --- -## Подготовка: Ключ обфускации +## Автоматическая установка (Рекомендуется) -Если вы настраиваете всё вручную, сгенерируйте общий 64-символьный hex-ключ. Он должен совпадать на сервере и клиенте: +Проще всего использовать официальный скрипт. Он установит зависимости, сгенерирует ключи, настроит файрвол и запустит службу. +**Интерактивный режим** (задаст вопросы в терминале): +```bash +curl -fsSL https://raw.githubusercontent.com/samosvalishe/free-turn-proxy/master/scripts/install-server.sh | sudo bash +``` +> Скрипт идемпотентен: при повторном запуске он предложит обновить, переконфигурировать или удалить сервер. + +**Неинтерактивный режим** (для автоматизации): +```bash +# Установка через Docker (UDP, порт бэкенда 51820) +curl -fsSL https://raw.githubusercontent.com/samosvalishe/free-turn-proxy/master/scripts/install-server.sh | \ + sudo bash -s -- -y --method docker --mode udp --backend-port 51820 + +# Обновление до конкретной версии +sudo bash install-server.sh -y --update --version v1.2.3 + +# Полное удаление +sudo bash install-server.sh -y --uninstall --purge +``` +*Все доступные флаги:* `sudo bash install-server.sh --help` + +--- + +## Ручная установка + +Если вы предпочитаете контролировать каждый шаг, используйте инструкции ниже. +Сначала сгенерируйте 64-символьный hex-ключ обфускации (он должен совпадать на сервере и клиенте): ```bash openssl rand -hex 32 ``` -*Скопируйте ключ — далее в примерах он обозначен как `<ВАШ_КЛЮЧ>`.* +*В примерах ниже он обозначен как `<ВАШ_КЛЮЧ>`.* ---- +### Способ 1: Docker Compose -## Способ 1: Docker Compose (Рекомендуется) - -1. Установите Docker, если его еще нет: +1. Установите Docker: ```bash - curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh + curl -fsSL https://get.docker.com | sudo sh ``` - -2. Создайте директорию проекта и файл `docker-compose.yml`: +2. Создайте директорию и `docker-compose.yml`: ```bash mkdir -p /opt/free-turn-proxy && cd /opt/free-turn-proxy nano docker-compose.yml ``` - -3. Вставьте конфиг (замените порты и `<ВАШ_КЛЮЧ>` на свои): +3. Вставьте конфигурацию: ```yaml services: free-turn-proxy: image: ghcr.io/samosvalishe/free-turn-proxy:latest container_name: free-turn-proxy - network_mode: "host" # Важно для прямого доступа к локальному WireGuard (127.0.0.1:51820) + network_mode: "host" # Важно для доступа к локальному VPN (127.0.0.1) restart: unless-stopped environment: - CONNECT_ADDR=127.0.0.1:51820 # Порт ВАШЕГО VPN (WG/AmneziaWG/Xray) - - LISTEN_ADDR=0.0.0.0:56000 # Внешний порт, к которому будет подключаться клиент - - MODE=udp # udp для WG/Amnezia, tcp для Xray/VLESS + - LISTEN_ADDR=0.0.0.0:56000 # Внешний порт + - MODE=udp # udp (WG/Amnezia) или tcp (Xray/VLESS) - OBF_PROFILE=rtpopus # Обязательная маскировка - - OBF_KEY=<ВАШ_КЛЮЧ> # Ваш сгенерированный 64-hex ключ - # Раскомментируйте ниже, чтобы включить авторизацию по Client ID - # - CLIENTS_FILE=/opt/free-turn-proxy/clients.json + - OBF_KEY=<ВАШ_КЛЮЧ> # Ваш ключ + # - CLIENTS_FILE=/opt/free-turn-proxy/clients.json # Для авторизации # volumes: # - /opt/free-turn-proxy/clients.json:/opt/free-turn-proxy/clients.json ``` +4. Запустите: `docker compose up -d` -4. Запустите контейнер: - ```bash - docker compose up -d - ``` - *Посмотреть логи: `docker compose logs -f`* +### Способ 2: systemd (Без Docker) ---- - -## Способ 2: systemd (Без Docker) - -1. Скачайте бинарник в `/opt/free-turn-proxy`: +1. Скачайте бинарник: ```bash sudo mkdir -p /opt/free-turn-proxy sudo curl -L -o /opt/free-turn-proxy/server https://github.com/samosvalishe/free-turn-proxy/releases/latest/download/server-linux-amd64 sudo chmod +x /opt/free-turn-proxy/server ``` - *(Для ARM-сервера замените `-amd64` на `-arm64`)* - -2. Создайте файл службы: - ```bash - sudo nano /etc/systemd/system/free-turn-proxy.service - ``` - -3. Вставьте конфиг (замените порты и `<ВАШ_КЛЮЧ>` на свои): + *(Для ARM замените `-amd64` на `-arm64`)* +2. Создайте службу: `sudo nano /etc/systemd/system/free-turn-proxy.service` +3. Вставьте конфигурацию: ```ini [Unit] Description=Free TURN Proxy Server @@ -90,8 +88,6 @@ openssl rand -hex 32 [Service] Type=simple - # Укажите ваши порты и ключ обфускации. - # Для авторизации по Client ID добавьте: -clients-file /opt/free-turn-proxy/clients.json ExecStart=/opt/free-turn-proxy/server -listen 0.0.0.0:56000 -connect 127.0.0.1:51820 -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> Restart=always RestartSec=5 @@ -101,60 +97,42 @@ openssl rand -hex 32 [Install] WantedBy=multi-user.target ``` - *(Примечание: `User=nobody` не может биндить порты < 1024 — используйте внешний порт > 1024)* - -4. Запустите службу и добавьте в автозагрузку: - ```bash - sudo systemctl daemon-reload - sudo systemctl enable --now free-turn-proxy.service - ``` - *Статус: `sudo systemctl status free-turn-proxy.service`* +4. Запустите: `sudo systemctl daemon-reload && sudo systemctl enable --now free-turn-proxy.service` --- ## Настройка Файрвола -Откройте внешний порт. Сервер слушает по **UDP** (DTLS-over-UDP), даже если вы используете `MODE=tcp` для Xray: - +Откройте внешний порт (сервер слушает по **UDP**, даже если `MODE=tcp`): ```bash -# Для UFW (Ubuntu/Debian) sudo ufw allow 56000/udp - -# Либо через iptables напрямую -sudo iptables -I INPUT -p udp --dport 56000 -j ACCEPT -sudo netfilter-persistent save # если установлен +# Или iptables: sudo iptables -I INPUT -p udp --dport 56000 -j ACCEPT ``` --- ## Авторизация по Client ID (Опционально) -По умолчанию любой клиент с верным `-obf-key` может релеить трафик. Чтобы ограничить доступ конкретным списком клиентов, включите авторизацию. +По умолчанию доступ открыт всем, кто знает `-obf-key`. Чтобы ограничить доступ: -1. Создайте пустой список на сервере: - ```bash - echo "[]" | sudo tee /opt/free-turn-proxy/clients.json - ``` +1. Создайте пустой список: `echo "[]" | sudo tee /opt/free-turn-proxy/clients.json` 2. Включите авторизацию: - - **В Docker:** раскомментируйте переменные `CLIENTS_FILE` и `volumes` в `docker-compose.yml`, затем выполните `docker compose up -d`. - - **В systemd:** добавьте флаг `-clients-file /opt/free-turn-proxy/clients.json` в `ExecStart`, затем `systemctl daemon-reload && systemctl restart free-turn-proxy`. -3. Управляйте клиентами (см. раздел [Управление Client ID в flags.md](flags.md#управление-client-id-команды-сервера)). -4. На клиенте задайте `-client-id ` (либо используйте автогенерируемый) и добавьте этот ID в `clients.json` на сервере. Клиент отправляет ID автоматически. + - **Docker:** раскомментируйте `CLIENTS_FILE` и `volumes` в `docker-compose.yml`, затем `docker compose up -d`. + - **systemd:** добавьте флаг `-clients-file /opt/free-turn-proxy/clients.json` в `ExecStart` и перезапустите службу. +3. Добавляйте ID в `clients.json` (см. [flags.md](flags.md#управление-client-id-команды-сервера)). На клиенте используйте флаг `-client-id `. --- -## Справка: Переменные окружения для Docker - -Если вы запускаете через голый `docker run`, вам пригодятся эти переменные (аналогичны флагам бинарника): +## Переменные окружения Docker | Переменная | По умолчанию | Описание | | --- | --- | --- | | `CONNECT_ADDR` | **обязательна** | IP и порт вашего VPN (бэкенда) | | `LISTEN_ADDR` | `0.0.0.0:56000` | Внешний адрес прослушивания | -| `MODE` | `udp` | Режим туннеля: `udp` (для WG/Amnezia) \| `tcp` (для Xray/VLESS) | -| `OBF_PROFILE` | `none` | Значение `-obf-profile`: `none` \| `rtpopus` | -| `OBF_KEY` | пусто | Значение `-obf-key` (обязателен при `OBF_PROFILE != none`) | -| `CLIENTS_FILE`| пусто | Путь к JSON-файлу (`/opt/free-turn-proxy/clients.json`) для авторизации | -| `DEBUG` | `false` | Включает debug-логи (`true` / `false`) | +| `MODE` | `udp` | Режим туннеля: `udp` \| `tcp` | +| `OBF_PROFILE` | `none` | Маскировка: `none` \| `rtpopus` | +| `OBF_KEY` | пусто | Ключ маскировки | +| `CLIENTS_FILE`| пусто | Путь к JSON-файлу авторизации | +| `DEBUG` | `false` | Включить debug-логи | -> **Внимание при Bridge Mode:** Если вы уберете `network_mode: "host"` и пробросите порты через `-p 56000:56000/udp`, то `CONNECT_ADDR=127.0.0.1:51820` будет указывать **внутрь** контейнера Docker. В таком случае прокси не найдет ваш WireGuard. Используйте IP хоста в `CONNECT_ADDR` или оставляйте `network_mode: "host"`. +> **Внимание при Bridge Mode:** Если вы уберете `network_mode: "host"` и пробросите порты (`-p 56000:56000/udp`), `CONNECT_ADDR=127.0.0.1:51820` будет указывать внутрь контейнера. В этом случае прокси не найдет VPN. Используйте IP хоста или оставляйте `network_mode: "host"`. diff --git a/docs/quickstart.md b/docs/quickstart.md index 1b7a17c..118560e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,33 +1,25 @@ # Быстрый Старт -Для стабильной работы мы сразу настроим **обфускацию трафика**. - -## Что Нужно - -- VPS с публичным IP, VPN-сервер (например, WireGuard или AmneziaWG) слушает локальный порт (например, `127.0.0.1:51820/udp`). -- Активная ссылка VK Calls: `https://vk.com/call/join/...` — создайте сами (звонок завершать нельзя). -- VPN-клиент на вашем устройстве: `Endpoint = 127.0.0.1:9000` (порт `9000` взят для примера), `MTU = 1280` (крайне важно для компенсации накладных расходов сети). +## Требования +- **VPS с публичным IP**, на котором установлен VPN-сервер (например, WireGuard). VPN должен слушать локальный порт (например, `127.0.0.1:51820/udp`). +- **Активная ссылка VK Calls**: `https://vk.com/call/join/...` (создайте сами, звонок не завершайте). --- -## Шаг 1: Запуск Сервера на VPS (Автоматически) +## Шаг 1: Запуск Сервера (VPS) -Интерактивный скрипт сам поставит Docker (или настроит systemd), сгенерирует ключ обфускации, откроет порт в файрволе и запустит сервер. Запускать от root: +Интерактивный скрипт установит Docker или systemd, настроит файрвол и запустит сервер. Запускать от root: ```bash -curl -fsSL https://raw.githubusercontent.com/samosvalishe/free-turn-proxy/master/scripts/install-server.sh -o install-server.sh -sudo bash install-server.sh +curl -fsSL https://raw.githubusercontent.com/samosvalishe/free-turn-proxy/master/scripts/install-server.sh | sudo bash ``` - -> Скрипт **интерактивный** — задаёт вопросы в терминале, поэтому скачивайте файл и запускайте через `sudo bash`, а не `curl | bash` (пайп ломает чтение ответов). Скопируйте параметры клиента, которые скрипт выдаст в конце. - -*Предпочитаете ручную установку шаг за шагом? См. [Развёртывание (deploy.md)](deploy.md).* +> Скопируйте параметры клиента, которые скрипт выдаст в конце. Для ручной установки или запуска скрипта без вопросов (non-interactive) см. [Развёртывание (deploy.md)](deploy.md). --- -## Шаг 2: Запуск Клиента и Маршруты (ПК) +## Шаг 2: Запуск Клиента (ПК) -Туннель замыкается сам на себя, если VPN (WireGuard) перехватывает весь трафик. Чтобы прокси `free-turn-proxy` мог общаться с TURN-серверами напрямую, нужен скрипт добавления маршрутов-исключений. +Туннель замыкается сам на себя, если VPN перехватывает весь трафик. Чтобы прокси мог общаться с TURN-серверами напрямую, скрипт добавит маршруты-исключения. **Linux:** ```bash @@ -44,30 +36,27 @@ Invoke-WebRequest -Uri https://github.com/samosvalishe/free-turn-proxy/releases/ **macOS:** ```bash -# Для Apple Silicon (M1/M2) скачайте client-darwin-arm64, для Intel — client-darwin-amd64 +# Apple Silicon (M1/M2): client-darwin-arm64 | Intel: client-darwin-amd64 ./client -listen 127.0.0.1:9000 -peer :56000 -link "" -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> -debug 2>&1 | ./scripts/routes-macos.sh ``` -*Когда скрипт напечатает `Ensuring route to ...` — включайте ваш VPN-клиент.* +> **Важно:** В настройках вашего VPN-клиента (например, WireGuard) укажите `Endpoint = 127.0.0.1:9000` и `MTU = 1280`. Включайте VPN *только после того*, как скрипт выведет `Ensuring route to ...`. > [!TIP] -> **Упрощение с помощью Share-ссылок и Подписок** -> Вместо длинного перечисления флагов вы можете передать клиенту одну компактную ссылку `freeturn://` первым аргументом, или использовать флаг `-sub` для скачивания настроек. Подробнее читайте в [uri.md](uri.md) и [sub.md](sub.md). +> **Упрощение:** Вместо длинных флагов можно использовать ссылку `freeturn://` или подписку (`-sub`). Подробнее в [uri.md](uri.md) и [sub.md](sub.md). --- -## Шаг 3: Мобильные Устройства +## Шаг 3: Мобильные Устройства (Termux) -На мобильных сетях есть жесткая специфика: -1. **Маршруты не нужны**. Но приложение с клиентом (Termux) **обязательно** нужно добавить в исключения VPN-клиента. -2. **Перехват DNS**. Мобильные операторы блокируют сторонние DNS, поэтому обязательно указывайте DNS оператора связи через `-dns-servers` (узнать его можно в настройках APN телефона). +На мобильных сетях маршруты не нужны, но есть своя специфика (блокировка DNS, добавление в исключения VPN). ```bash termux-wake-lock # Скачивание: curl -L -o client https://github.com/samosvalishe/free-turn-proxy/releases/latest/download/client-android-arm64 && chmod +x client -# Обязательно укажите ваш ключ и DNS -./client -listen 127.0.0.1:9000 -peer :56000 -link "" -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> -dns-servers +# Обязательно укажите ваш ключ и DNS оператора (можно узнать в настройках APN) +./client -listen 127.0.0.1:9000 -peer :56000 -link "" -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> -dns-servers ``` -> См. подробности в разделе [Мобильные устройства (mobile.md)](mobile.md). +> Обязательно добавьте приложение Termux в исключения вашего VPN-клиента. Подробнее в [mobile.md](mobile.md). diff --git a/scripts/install-server.sh b/scripts/install-server.sh index e67d6f5..dc0fcb6 100644 --- a/scripts/install-server.sh +++ b/scripts/install-server.sh @@ -1,13 +1,10 @@ #!/usr/bin/env bash -# Free Turn Proxy Server — интерактивный установщик. -# -# Идемпотентен: повторный запуск обнаруживает существующую установку и -# предлагает обновить версию (сохраняя конфиг), переконфигурировать или удалить. -# Конфигурация установки хранится в $CONF_FILE и переиспользуется при обновлении. +# Free Turn Proxy Server — установщик. Интерактивный TUI + non-interactive (-y). +# Подробности: bash install-server.sh --help -set -e +set -Eeuo pipefail +trap 'rc=$?; printf "\n\033[0;31m[x]\033[0m строка %s (код %s): %s\n" "${LINENO}" "${rc}" "${BASH_COMMAND}" >&2; exit "${rc}"' ERR -# --- Константы --- REPO="samosvalishe/free-turn-proxy" IMAGE="ghcr.io/${REPO}" APP_DIR="/opt/free-turn-proxy" @@ -15,22 +12,17 @@ CONF_FILE="${APP_DIR}/install.conf" SERVICE="free-turn-proxy.service" UNIT_FILE="/etc/systemd/system/${SERVICE}" COMPOSE_FILE="${APP_DIR}/docker-compose.yml" +CONTAINER="free-turn-proxy" +WT_BACK="Free Turn Proxy • установщик сервера" -# --- Цвета --- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -# --- Логирование --- -info() { echo -e "${CYAN}[*]${NC} $1"; } +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +info() { echo -e "${CYAN}[*]${NC} $1"; } success() { echo -e "${GREEN}[+]${NC} $1"; } -warn() { echo -e "${YELLOW}[!]${NC} $1"; } -error() { echo -e "${RED}[x]${NC} $1"; exit 1; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +error() { echo -e "${RED}[x]${NC} $1" >&2; exit 1; } -# --- Переменные конфигурации (дефолты; перекрываются load_config / мастером) --- -INSTALL_METHOD="1" # 1=docker, 2=systemd +# Конфиг (перекрывается load_config / мастером / CLI) +INSTALL_METHOD="1" # 1=docker 2=systemd VERSION="latest" PROXY_MODE="udp" BACKEND_PORT="" @@ -39,108 +31,221 @@ OBF_PROFILE="rtpopus" OBF_KEY="" CLIENTS_FILE_CONF="" -# ============================================================================ -# Хелперы -# ============================================================================ +GOARCH="" +WG_PORT="" +HAS_WHIPTAIL=0 +NONINTERACTIVE=0 +OPEN_FIREWALL="" # ""=спросить 1=да 0=нет +PURGE=0 +ACTION="" +OVERRIDES=() -# ask VAR "Текст вопроса" "дефолт" — читает ответ в переменную VAR. -ask() { - local __var="$1" __prompt="$2" __def="$3" __ans - if [ -n "$__def" ]; then - read -p "$__prompt [$__def]: " __ans +valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; } +valid_hex64() { [[ "$1" =~ ^[0-9a-fA-F]{64}$ ]]; } + +# --------------------------------------------------------------------------- +# UI: whiptail | plain (/dev/tty — работает через `curl | bash`) +# --------------------------------------------------------------------------- +wt() { whiptail --backtitle "$WT_BACK" "$@"; } +ui_abort() { info "Отменено."; exit 0; } + +ui_msg() { # TITLE TEXT + if [ "$HAS_WHIPTAIL" = 1 ]; then wt --title "$1" --msgbox "$2" 12 72; else echo; warn "$1: $2"; fi +} + +ui_input() { # VAR PROMPT DEFAULT + local __v="$1" __p="$2" __d="${3:-}" __a + if [ "$HAS_WHIPTAIL" = 1 ]; then + __a=$(wt --title "Free Turn Proxy" --inputbox "$__p" 10 72 "$__d" 3>&1 1>&2 2>&3) || ui_abort else - read -p "$__prompt: " __ans + if [ -n "$__d" ]; then read -r -p "$__p [$__d]: " __a 0/1 + local __p="$1" __def="${2:-Y}" __a + if [ "$HAS_WHIPTAIL" = 1 ]; then + if [ "$__def" = "Y" ]; then wt --title "Free Turn Proxy" --yesno "$__p" 10 72 + else wt --title "Free Turn Proxy" --defaultno --yesno "$__p" 10 72; fi + return $? + fi + local hint; [ "$__def" = "Y" ] && hint="Y/n" || hint="y/N" + read -r -p "$__p [$hint]: " __a &1 1>&2 2>&3) || ui_abort + printf -v "$__v" '%s' "$sel" + else + local i sel + echo; info "$__p" + for i in "${!tags[@]}"; do + [ "${tags[$i]}" = "$__def" ] && echo -e " ${GREEN}${tags[$i]}${NC}) ${descs[$i]}" \ + || echo " ${tags[$i]}) ${descs[$i]}" + done + while :; do + read -r -p "Выбор [${__def}]: " sel "$log" 2>&1 ) & local pid=$! + { + local p=0 + while kill -0 "$pid" 2>/dev/null; do + echo "$p"; p=$((p+2)); if [ "$p" -gt 95 ]; then p=95; fi; sleep 0.25 + done + echo 100 + } | wt --gauge "$title" 8 72 0 + wait "$pid" && rc=0 || rc=$? + [ "$rc" -ne 0 ] && wt --title "Ошибка" --scrolltext --msgbox "$(tail -n 40 "$log")" 20 76 + elif [ -t 1 ]; then + ( trap - ERR; "$@" >"$log" 2>&1 ) & local pid=$! ch='|/-\' i=0 + while kill -0 "$pid" 2>/dev/null; do + i=$(((i+1)%4)); printf "\r${CYAN}[*]${NC} %s %s" "$title" "${ch:$i:1}"; sleep 0.2 + done + wait "$pid" && rc=0 || rc=$? + if [ "$rc" -eq 0 ]; then printf "\r${GREEN}[+]${NC} %s\033[K\n" "$title" + else printf "\r${RED}[x]${NC} %s\033[K\n" "$title"; tail -n 40 "$log" >&2; fi + else + info "$title..." + "$@" >"$log" 2>&1 && rc=0 || rc=$? + [ "$rc" -ne 0 ] && tail -n 40 "$log" >&2 + fi + rm -f "$log" + return "$rc" +} + +ui_info_start() { [ "$HAS_WHIPTAIL" = 1 ] && wt --title "Подождите" --infobox "$1" 7 60 || info "$1"; } + +# --------------------------------------------------------------------------- +# Система +# --------------------------------------------------------------------------- detect_arch() { - local arch - arch=$(uname -m) - case "$arch" in - x86_64|amd64) GOARCH="amd64" ;; + case "$(uname -m)" in + x86_64|amd64) GOARCH="amd64" ;; aarch64|arm64) GOARCH="arm64" ;; - *) error "Неподдерживаемая архитектура: $arch" ;; + *) error "Неподдерживаемая архитектура: $(uname -m)" ;; esac } +pkg_install() { + if command -v apt-get >/dev/null 2>&1; then + apt-get update -y -qq >/dev/null 2>&1 || true + apt-get install -y -qq "$@" >/dev/null 2>&1 || true + elif command -v dnf >/dev/null 2>&1; then dnf install -y -q "$@" >/dev/null 2>&1 || true + elif command -v yum >/dev/null 2>&1; then yum install -y -q "$@" >/dev/null 2>&1 || true + fi +} + ensure_base_deps() { - info "Проверка базовых утилит (curl, jq, openssl)..." - if command -v apt-get >/dev/null; then - apt-get update -y -qq >/dev/null - apt-get install -y -qq curl jq openssl >/dev/null - elif command -v yum >/dev/null; then - yum install -y -q curl jq openssl >/dev/null - else - warn "Пакетный менеджер не определён (apt/yum). Убедитесь, что curl, jq, openssl установлены." + local missing=() b + for b in curl jq openssl; do command -v "$b" >/dev/null 2>&1 || missing+=("$b"); done + if [ "${#missing[@]}" -ne 0 ]; then + with_progress "Установка зависимостей: ${missing[*]}" pkg_install "${missing[@]}" + missing=() + for b in curl jq openssl; do command -v "$b" >/dev/null 2>&1 || missing+=("$b"); done + [ "${#missing[@]}" -ne 0 ] && error "Не удалось установить: ${missing[*]}. Поставьте вручную." fi } -# --- GitHub API --- -gh_latest_version() { - curl -s "https://api.github.com/repos/${REPO}/releases/latest" | jq -r '.tag_name // empty' +ensure_tui() { + command -v whiptail >/dev/null 2>&1 && { HAS_WHIPTAIL=1; return; } + if command -v apt-get >/dev/null 2>&1; then pkg_install whiptail; else pkg_install newt; fi + command -v whiptail >/dev/null 2>&1 && HAS_WHIPTAIL=1 || { HAS_WHIPTAIL=0; warn "whiptail недоступен — текстовый режим."; } } -gh_recent_versions() { - curl -s "https://api.github.com/repos/${REPO}/releases?per_page=6" | jq -r '.[].tag_name' -} - -# resolve_asset_url — URL ассета релиза (latest или vX.Y.Z). -resolve_asset_url() { - local ver="$1" asset="$2" api - if [ "$ver" = "latest" ]; then - api="https://api.github.com/repos/${REPO}/releases/latest" - else - api="https://api.github.com/repos/${REPO}/releases/tags/${ver}" +detect_wg_port() { + WG_PORT="" + command -v wg >/dev/null 2>&1 || return 0 + if wg show all listen-port >/dev/null 2>&1; then + WG_PORT="$(wg show all listen-port 2>/dev/null | head -n1 | awk '{print $2}' || true)" + fi + if [ -z "$WG_PORT" ] && ls /etc/wireguard/*.conf >/dev/null 2>&1; then + WG_PORT="$(grep -i ListenPort /etc/wireguard/*.conf 2>/dev/null | head -n1 | awk -F= '{print $2}' | tr -d ' ' || true)" fi - curl -s "$api" | jq -r --arg n "$asset" '.assets[] | select(.name==$n) | .browser_download_url' } -# Тег Docker-образа: latest -> latest, иначе сам тег. -image_tag() { - [ "$VERSION" = "latest" ] && echo "latest" || echo "$VERSION" +# --------------------------------------------------------------------------- +# GitHub API +# --------------------------------------------------------------------------- +gh_latest_version() { curl -s --max-time 10 "https://api.github.com/repos/${REPO}/releases/latest" | jq -r '.tag_name // empty'; } +gh_recent_versions() { curl -s --max-time 10 "https://api.github.com/repos/${REPO}/releases?per_page=6" | jq -r '.[].tag_name'; } + +resolve_asset_url() { # VERSION ASSET + local api + [ "$1" = "latest" ] && api="https://api.github.com/repos/${REPO}/releases/latest" \ + || api="https://api.github.com/repos/${REPO}/releases/tags/$1" + curl -s --max-time 15 "$api" | jq -r --arg n "$2" '.assets[] | select(.name==$n) | .browser_download_url' } -# Интерактивный выбор версии в переменную VERSION. +image_tag() { [ "$VERSION" = "latest" ] && echo "latest" || echo "$VERSION"; } + choose_version() { - info "Получение списка версий с GitHub..." - local latest others def="${VERSION:-latest}" - latest=$(gh_latest_version || true) - echo "" - if [ -n "$latest" ]; then - echo -e " ${GREEN}latest${NC} (= $latest)" - others=$(gh_recent_versions 2>/dev/null | grep -vx "$latest" | head -4 || true) - if [ -n "$others" ]; then - while IFS= read -r t; do echo " $t"; done <<< "$others" - fi + ui_info_start "Получение списка версий с GitHub..." + local latest others t def="${VERSION:-latest}" + latest="$(gh_latest_version || true)" + others="$([ -n "$latest" ] && gh_recent_versions 2>/dev/null | grep -vx "$latest" | head -4 || true)" + + if [ "$HAS_WHIPTAIL" = 1 ] && [ -n "$latest" ]; then + local args=("latest" "последний стабильный (= $latest)") sel + while IFS= read -r t; do [ -n "$t" ] && args+=("$t" "релиз $t"); done <<< "$others" + args+=("custom" "ввести тег вручную") + sel=$(wt --title "Версия" --default-item "$def" --menu "Выберите версию" 17 72 8 "${args[@]}" 3>&1 1>&2 2>&3) || ui_abort + [ "$sel" = "custom" ] && ui_input VERSION "Тег (latest или vX.Y.Z)" "$def" || VERSION="$sel" else - warn "Не удалось получить список релизов. Введите тег вручную (или latest)." + echo + if [ -n "$latest" ]; then + echo -e " ${GREEN}latest${NC} (= $latest)" + [ -n "$others" ] && while IFS= read -r t; do echo " $t"; done <<< "$others" + else + warn "Список релизов недоступен — введите тег вручную." + fi + echo + ui_input VERSION "Версия (latest или vX.Y.Z)" "$def" fi - echo "" - ask VERSION "Версия (latest или vX.Y.Z)" "$def" } -# --- Конфигурация установки --- -is_installed() { - [ -f "$CONF_FILE" ] || [ -f "$COMPOSE_FILE" ] || [ -f "$UNIT_FILE" ] -} - -load_config() { - [ -f "$CONF_FILE" ] && . "$CONF_FILE" -} +# --------------------------------------------------------------------------- +# Конфиг установки +# --------------------------------------------------------------------------- +is_installed() { [ -f "$CONF_FILE" ] || [ -f "$COMPOSE_FILE" ] || [ -f "$UNIT_FILE" ]; } +load_config() { [ -f "$CONF_FILE" ] && . "$CONF_FILE" || true; } save_config() { mkdir -p "$APP_DIR" cat > "$CONF_FILE" </dev/null; then - warn "WireGuard не найден." - if yesno "Установить пакет WireGuard? (только пакет, настройка — вручную)" "Y"; then - if command -v apt-get >/dev/null; then - apt-get install -y -qq wireguard >/dev/null - elif command -v yum >/dev/null; then - yum install -y -q epel-release >/dev/null - yum install -y -q wireguard-tools >/dev/null - fi - success "WireGuard установлен." - fi - else - success "WireGuard уже установлен." - if wg show all listen-port >/dev/null 2>&1; then - wg_port=$(wg show all listen-port | head -n 1 | awk '{print $2}') - fi - if [ -z "$wg_port" ] && [ -n "$(ls -A /etc/wireguard/*.conf 2>/dev/null)" ]; then - wg_port=$(grep -i ListenPort /etc/wireguard/*.conf | head -n 1 | awk -F '=' '{print $2}' | tr -d ' ') - fi - fi + ui_menu PROXY_MODE "Режим туннеля:" "${PROXY_MODE:-udp}" \ + udp "UDP-relay — WireGuard / AmneziaWG" \ + tcp "TCP-forward — Xray / sing-box" - echo "" - info "Конфигурация Free Turn Proxy" - - ask PROXY_MODE "Режим туннеля (udp — WireGuard/AmneziaWG, tcp — Xray)" "${PROXY_MODE:-udp}" - - # Порт бэкенда: приоритет — текущий конфиг, затем найденный WG, затем дефолт по режиму. + detect_wg_port local def_backend="$BACKEND_PORT" if [ -z "$def_backend" ]; then - if [ -n "$wg_port" ]; then - def_backend="$wg_port" - elif [ "$PROXY_MODE" = "udp" ]; then - def_backend="51820" - else - def_backend="443" - fi + if [ -n "$WG_PORT" ]; then def_backend="$WG_PORT"; info "Найден WireGuard на порту $WG_PORT." + elif [ "$PROXY_MODE" = "udp" ]; then def_backend="51820"; else def_backend="443"; fi fi - ask BACKEND_PORT "Порт вашего VPN / бэкенда" "$def_backend" + ask_port BACKEND_PORT "Порт вашего VPN / бэкенда" "$def_backend" + ask_port LISTEN_PORT "Внешний порт (приём Free Turn Proxy)" "${LISTEN_PORT:-56000}" - ask LISTEN_PORT "Внешний порт (приём соединений Free Turn Proxy)" "${LISTEN_PORT:-56000}" - - # Обфускация (дефолт ответа = текущее состояние) - local obf_def="Y" - [ "$OBF_PROFILE" = "none" ] && obf_def="N" - if yesno "Включить маскировку (rtpopus)?" "$obf_def"; then + local obf_def="Y"; [ "$OBF_PROFILE" = "none" ] && obf_def="N" + if ui_yesno "Включить маскировку rtpopus? (рекомендуется)" "$obf_def"; then OBF_PROFILE="rtpopus" if [ -n "$OBF_KEY" ]; then - info "Текущий ключ обфускации сохранён (оставьте пустым, чтобы не менять)." - ask OBF_KEY "Новый 64-hex ключ (Enter — оставить текущий)" "$OBF_KEY" - elif yesno "Сгенерировать случайный ключ обфускации?" "Y"; then - OBF_KEY=$(openssl rand -hex 32) - success "Ключ сгенерирован: $OBF_KEY" + ui_yesno "Сгенерировать НОВЫЙ ключ? (нет — оставить текущий)" "N" && { OBF_KEY="$(openssl rand -hex 32)"; } + elif ui_yesno "Сгенерировать случайный ключ?" "Y"; then + OBF_KEY="$(openssl rand -hex 32)" else - ask OBF_KEY "Введите ваш 64-символьный hex ключ" "" - [ -z "$OBF_KEY" ] && error "Ключ не может быть пустым." + while :; do + ui_input OBF_KEY "64-hex ключ обфускации" "" + valid_hex64 "$OBF_KEY" && break + ui_msg "Ошибка" "Ключ — ровно 64 hex-символа." + done fi else - OBF_PROFILE="none" - OBF_KEY="" + OBF_PROFILE="none"; OBF_KEY="" fi - # Авторизация по Client ID - local auth_def="N" - [ -n "$CLIENTS_FILE_CONF" ] && auth_def="Y" - if yesno "Включить авторизацию по Client ID?" "$auth_def"; then - CLIENTS_FILE_CONF="${APP_DIR}/clients.json" - else - CLIENTS_FILE_CONF="" - fi + local auth_def="N"; [ -n "$CLIENTS_FILE_CONF" ] && auth_def="Y" + ui_yesno "Включить авторизацию по Client ID?" "$auth_def" \ + && CLIENTS_FILE_CONF="${APP_DIR}/clients.json" || CLIENTS_FILE_CONF="" - # Версия - echo "" choose_version - # Firewall - echo "" - info "Настройка брандмауэра..." - if yesno "Открыть порт ${LISTEN_PORT}/udp в UFW/iptables?" "Y"; then - firewall_open - fi + ui_yesno "Открыть порт ${LISTEN_PORT}/udp в UFW/iptables?" "Y" && OPEN_FIREWALL=1 || OPEN_FIREWALL=0 } firewall_open() { - if command -v ufw >/dev/null && ufw status | grep -q "Status: active"; then - ufw allow "${LISTEN_PORT}/udp" >/dev/null # ufw сам дедуплицирует правила - success "Порт открыт через UFW." - elif command -v iptables >/dev/null; then - # -C проверяет наличие правила → не плодим дубли при повторном запуске. + if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then + ufw allow "${LISTEN_PORT}/udp" >/dev/null 2>&1 || warn "ufw allow не удался." + success "Порт ${LISTEN_PORT}/udp открыт (UFW)." + elif command -v iptables >/dev/null 2>&1; then if ! iptables -C INPUT -p udp --dport "$LISTEN_PORT" -j ACCEPT 2>/dev/null; then - iptables -I INPUT -p udp --dport "$LISTEN_PORT" -j ACCEPT - command -v netfilter-persistent >/dev/null && netfilter-persistent save >/dev/null 2>&1 || true + iptables -I INPUT -p udp --dport "$LISTEN_PORT" -j ACCEPT 2>/dev/null || warn "iptables -I не удался." + command -v netfilter-persistent >/dev/null 2>&1 && netfilter-persistent save >/dev/null 2>&1 || true fi - success "Порт открыт через iptables." + success "Порт ${LISTEN_PORT}/udp открыт (iptables)." else - warn "Брандмауэр (ufw/iptables) не найден — откройте порт ${LISTEN_PORT}/udp вручную." + warn "Брандмауэр не найден — откройте ${LISTEN_PORT}/udp вручную." fi } -# ============================================================================ -# Применение конфигурации -# ============================================================================ +# --------------------------------------------------------------------------- +# Применение +# --------------------------------------------------------------------------- init_clients_file() { - if [ -n "$CLIENTS_FILE_CONF" ] && [ ! -f "$CLIENTS_FILE_CONF" ]; then - echo "[]" > "$CLIENTS_FILE_CONF" - success "Создан пустой список клиентов $CLIENTS_FILE_CONF" - fi + [ -n "$CLIENTS_FILE_CONF" ] && [ ! -f "$CLIENTS_FILE_CONF" ] && echo "[]" > "$CLIENTS_FILE_CONF" || true } ensure_docker() { - if command -v docker >/dev/null; then - return - fi - warn "Docker не найден." - if yesno "Установить Docker сейчас?" "Y"; then - info "Установка Docker..." - curl -fsSL https://get.docker.com -o get-docker.sh - sh get-docker.sh >/dev/null 2>&1 - rm -f get-docker.sh - success "Docker успешно установлен." - else - error "Для этого метода требуется Docker. Отмена." + command -v docker >/dev/null 2>&1 && return 0 + if [ "$NONINTERACTIVE" != 1 ] && ! ui_yesno "Docker не найден. Установить?" "Y"; then + error "Для метода docker нужен Docker." fi + with_progress "Установка Docker" sh -c 'curl -fsSL https://get.docker.com | sh' \ + || error "Установка Docker не удалась." + command -v docker >/dev/null 2>&1 || error "Docker не появился в PATH." +} + +healthcheck_docker() { + sleep 2 + [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo false)" = "true" ] && { success "Контейнер работает."; return; } + warn "Контейнер не запущен. Логи:" + docker logs --tail 40 "$CONTAINER" 2>&1 || true + error "$CONTAINER не поднялся." } apply_docker() { ensure_docker - info "Генерация docker-compose.yml..." { echo "services:" echo " free-turn-proxy:" echo " image: ${IMAGE}:$(image_tag)" - echo " container_name: free-turn-proxy" + echo " container_name: ${CONTAINER}" echo " network_mode: \"host\"" echo " restart: unless-stopped" echo " environment:" @@ -317,34 +394,34 @@ apply_docker() { } > "$COMPOSE_FILE" cd "$APP_DIR" - info "Загрузка образа ${IMAGE}:$(image_tag)..." - docker compose pull - info "Запуск контейнера..." - docker compose up -d + with_progress "Загрузка образа ${IMAGE}:$(image_tag)" docker compose pull || error "docker compose pull не удался." + with_progress "Запуск контейнера" docker compose up -d || error "docker compose up не удался." + healthcheck_docker } download_binary() { - info "Поиск бинарника server-linux-${GOARCH} (версия: ${VERSION})..." local url - url=$(resolve_asset_url "$VERSION" "server-linux-${GOARCH}") - [ -z "$url" ] && error "Не удалось найти server-linux-${GOARCH} в релизе ${VERSION}." - info "Скачивание..." - # Скачиваем во временный файл и атомарно подменяем — рабочий процесс не падает. - if ! curl -fL -o "${APP_DIR}/server.new" "$url" >/dev/null 2>&1; then - error "Не удалось скачать бинарник по адресу $url" - fi + url="$(resolve_asset_url "$VERSION" "server-linux-${GOARCH}" || true)" + [ -z "$url" ] && error "Не найден server-linux-${GOARCH} в релизе ${VERSION}." + with_progress "Скачивание server-linux-${GOARCH} (${VERSION})" \ + curl -fL -o "${APP_DIR}/server.new" "$url" || error "Не удалось скачать $url" chmod +x "${APP_DIR}/server.new" mv -f "${APP_DIR}/server.new" "${APP_DIR}/server" } +healthcheck_systemd() { + sleep 1 + systemctl is-active --quiet "$SERVICE" && { success "Служба активна."; return; } + warn "Служба не активна. Логи:" + journalctl -u "$SERVICE" --no-pager -n 40 2>&1 || true + error "$SERVICE не запустилась." +} + apply_systemd() { download_binary - - info "Генерация systemd-сервиса..." local args="-listen 0.0.0.0:${LISTEN_PORT} -connect $(connect_addr) -mode ${PROXY_MODE}" [ "$OBF_PROFILE" != "none" ] && args="$args -obf-profile ${OBF_PROFILE} -obf-key ${OBF_KEY}" [ -n "$CLIENTS_FILE_CONF" ] && args="$args -clients-file ${CLIENTS_FILE_CONF}" - cat > "$UNIT_FILE" </dev/null 2>&1 || true - systemctl restart "$SERVICE" # restart идемпотентен: стартует если стоит, перезапускает если работает + systemctl restart "$SERVICE" || error "systemctl restart не удался." + healthcheck_systemd } apply() { mkdir -p "$APP_DIR" init_clients_file - if [ "$INSTALL_METHOD" = "1" ]; then - apply_docker - else - apply_systemd - fi + if [ "$INSTALL_METHOD" = "1" ]; then apply_docker; else apply_systemd; fi + [ "$OPEN_FIREWALL" = "1" ] && firewall_open save_config } -# ============================================================================ -# Итоговая сводка -# ============================================================================ print_summary() { - echo "" - success "Готово!" - echo "--------------------------------------------------------" - echo -e "${CYAN}Конфигурация для подключения (Client):${NC}" - local ext_ip - ext_ip=$(curl -s ifconfig.me || echo "IP_ВАШЕГО_СЕРВЕРА") - echo " Сервер (peer): ${ext_ip}:${LISTEN_PORT}" - if [ "$OBF_PROFILE" != "none" ]; then - echo " Профиль обфускации: $OBF_PROFILE" - echo " Ключ обфускации: $OBF_KEY" - fi - echo " Режим: $PROXY_MODE" - echo " Версия: $VERSION" - + local ext_ip; ext_ip="$(curl -s --max-time 5 ifconfig.me || echo "IP_СЕРВЕРА")" + local body="Сервер (peer): ${ext_ip}:${LISTEN_PORT}\nРежим: ${PROXY_MODE}\nВерсия: ${VERSION}\nМетод: $(method_label)" + [ "$OBF_PROFILE" != "none" ] && body="${body}\nОбфускация: ${OBF_PROFILE}\nКлюч: ${OBF_KEY}" if [ -n "$CLIENTS_FILE_CONF" ]; then - echo "" - echo -e "${YELLOW}Внимание: включена авторизация по Client ID.${NC}" - echo "Добавьте клиента на сервере, иначе он не подключится:" if [ "$INSTALL_METHOD" = "1" ]; then - echo " Добавить: docker exec -it free-turn-proxy /app/server clients add " - echo " Список: docker exec -it free-turn-proxy /app/server clients list" + body="${body}\n\nClient ID auth ВКЛ. Добавить клиента:\n docker exec -it ${CONTAINER} /app/server clients add " else - echo " Добавить: ${APP_DIR}/server -clients-file ${CLIENTS_FILE_CONF} clients add " - echo " Список: ${APP_DIR}/server -clients-file ${CLIENTS_FILE_CONF} clients list" + body="${body}\n\nClient ID auth ВКЛ. Добавить клиента:\n ${APP_DIR}/server -clients-file ${CLIENTS_FILE_CONF} clients add " fi fi - echo "--------------------------------------------------------" - echo "Повторный запуск скрипта: обновление версии / переконфигурация / удаление." + if [ "$HAS_WHIPTAIL" = 1 ]; then + wt --title "Готово ✓" --msgbox "$(echo -e "$body")" 18 74 + else + echo ""; success "Готово!" + echo "--------------------------------------------------------" + echo -e "$body" + echo "--------------------------------------------------------" + fi echo -e "Документация: https://github.com/${REPO}" } -# ============================================================================ +# --------------------------------------------------------------------------- # Сценарии -# ============================================================================ -flow_install() { - info "Новая установка Free Turn Proxy Server." - wizard - echo "" - info "Начинаем установку..." - apply - print_summary -} +# --------------------------------------------------------------------------- +flow_install() { wizard; validate_config; apply; print_summary; } +flow_reconfigure() { wizard; validate_config; apply; print_summary; } flow_update() { - if [ ! -f "$CONF_FILE" ]; then - warn "Файл конфигурации не найден ($CONF_FILE) — нужна переконфигурация." - flow_reconfigure - return - fi - info "Обновление. Текущая версия: ${VERSION}, метод: $([ "$INSTALL_METHOD" = "1" ] && echo docker || echo systemd)." + [ -f "$CONF_FILE" ] || { warn "Конфиг не найден — переконфигурация."; flow_reconfigure; return; } choose_version - echo "" - info "Применяем версию ${VERSION}..." - apply - print_summary + [ -z "$OPEN_FIREWALL" ] && OPEN_FIREWALL=0 + validate_config; apply; print_summary } -flow_reconfigure() { - info "Переконфигурация (текущие значения подставлены как дефолты)." - wizard - echo "" - info "Применяем изменения..." - apply - print_summary +do_uninstall() { # REMOVE_DIR(0|1) + if [ "$INSTALL_METHOD" = "1" ]; then + [ -f "$COMPOSE_FILE" ] && ( cd "$APP_DIR" && docker compose down ) || warn "docker compose down — пропуск/ошибка." + else + systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true + rm -f "$UNIT_FILE"; systemctl daemon-reload || true + fi + success "Служба остановлена и удалена." + [ "$1" = "1" ] && { rm -rf "$APP_DIR"; success "Каталог $APP_DIR удалён."; } || info "Каталог сохранён: $APP_DIR" } flow_uninstall() { - echo "" - warn "Удаление Free Turn Proxy Server." - if ! yesno "Продолжить удаление?" "N"; then - info "Отменено." - return - fi - if [ "$INSTALL_METHOD" = "1" ]; then - if [ -f "$COMPOSE_FILE" ]; then - ( cd "$APP_DIR" && docker compose down ) || warn "docker compose down завершился с ошибкой." - fi - else - systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true - rm -f "$UNIT_FILE" - systemctl daemon-reload - fi - success "Служба остановлена и удалена." - if yesno "Удалить каталог ${APP_DIR} (ключи, clients.json, конфиг)?" "N"; then - rm -rf "$APP_DIR" - success "Каталог удалён." - else - info "Каталог сохранён: $APP_DIR" - fi + ui_yesno "Удалить Free Turn Proxy Server?" "N" || { info "Отменено."; return; } + local rm_dir=0 + ui_yesno "Удалить каталог ${APP_DIR} (ключи, clients.json, конфиг)?" "N" && rm_dir=1 + do_uninstall "$rm_dir" } menu_existing() { - local method_label - method_label=$([ "$INSTALL_METHOD" = "1" ] && echo docker || echo systemd) - echo "" - info "Обнаружена установка (метод: ${method_label}, версия: ${VERSION:-?})." - echo " 1) Обновить версию (конфиг сохранён)" - echo " 2) Переконфигурировать" - echo " 3) Удалить" - echo " 4) Выход" local choice - ask choice "Ваш выбор [1-4]" "1" + ui_menu choice "Установка найдена (метод: $(method_label), версия: ${VERSION:-?}). Действие:" "update" \ + update "Обновить версию (конфиг сохранён)" \ + reconfigure "Переконфигурировать" \ + uninstall "Удалить" \ + exit "Выход" case "$choice" in - 1) flow_update ;; - 2) flow_reconfigure ;; - 3) flow_uninstall ;; - 4) info "Выход."; exit 0 ;; - *) error "Неизвестный выбор: $choice" ;; + update) flow_update ;; reconfigure) flow_reconfigure ;; + uninstall) flow_uninstall ;; exit) info "Выход."; exit 0 ;; esac } -# ============================================================================ -# main -# ============================================================================ -main() { - [ "$EUID" -ne 0 ] && error "Запустите скрипт от root (sudo bash install-server.sh)" +run_noninteractive() { + is_installed && load_config || true + apply_overrides + if [ "${ACTION:-install}" = "uninstall" ]; then do_uninstall "$PURGE"; return; fi + validate_config + [ -z "$OPEN_FIREWALL" ] && OPEN_FIREWALL=1 + info "Применяю конфигурацию (метод: $(method_label))..." + apply; print_summary +} +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- +usage() { + cat <