fix(install): переписать установщик сервера — TUI, надёжность, non-interactive

Чинит тихий выход после выбора метода (set -e + установка WireGuard
в мастере убивали скрипт без диагностики) и неудобный выбор.

- trap ERR с номером строки и командой вместо тихой смерти
- WireGuard только детектится (порт бэкенда), без авто-установки
- whiptail-TUI: gauge-прелоадеры (pull/up/download/deps), меню с
  --default-item, backtitle, валидация портов/hex-ключа; fallback на
  текст со спиннером, чтение из /dev/tty (работает через curl | bash)
- non-interactive режим: -y + флаги (--method/--mode/--obf-key/
  --version/--update/--uninstall/--purge), CLI бьёт install.conf
- health-check после старта (docker inspect / systemctl is-active):
  падение даёт ошибку с логами, а не ложное «Готово»
- docs: curl | bash one-liner + примеры non-interactive
This commit is contained in:
samosvalishe
2026-05-26 00:52:46 +03:00
parent 98e1d7cc8e
commit 1b154a2e84
3 changed files with 490 additions and 432 deletions
+61 -83
View File
@@ -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>` (либо используйте автогенерируемый) и добавьте этот 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 <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"`.
+17 -28
View File
@@ -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 <vps_ip>:56000 -link "<vk-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 <vps_ip>:56000 -link "<vk-link>" -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> -dns-servers <ip_dns_мобильного_оператора>
# Обязательно укажите ваш ключ и DNS оператора (можно узнать в настройках APN)
./client -listen 127.0.0.1:9000 -peer <vps_ip>:56000 -link "<vk-link>" -obf-profile rtpopus -obf-key <ВАШ_КЛЮЧ> -dns-servers <ip_dns_оператора>
```
> См. подробности в разделе [Мобильные устройства (mobile.md)](mobile.md).
> Обязательно добавьте приложение Termux в исключения вашего VPN-клиента. Подробнее в [mobile.md](mobile.md).
+412 -321
View File
@@ -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 </dev/tty
else read -r -p "$__p: " __a </dev/tty; fi
__a="${__a:-$__d}"
fi
printf -v "$__var" '%s' "${__ans:-$__def}"
printf -v "$__v" '%s' "$__a"
}
# yesno "Текст" "Y|N" — true если пользователь согласился.
yesno() {
local __ans
read -p "$1 [$([ "$2" = "Y" ] && echo "Y/n" || echo "y/N")]: " __ans
__ans=${__ans:-$2}
[[ "$__ans" =~ ^[Yy]$ ]]
ui_yesno() { # PROMPT DEFAULT(Y|N) -> 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 </dev/tty
[[ "${__a:-$__def}" =~ ^[Yy]$ ]]
}
ui_menu() { # VAR PROMPT DEFAULT_TAG tag desc [tag desc...]
local __v="$1" __p="$2" __def="$3"; shift 3
local tags=() descs=()
while [ $# -gt 0 ]; do tags+=("$1"); descs+=("$2"); shift 2; done
if [ "$HAS_WHIPTAIL" = 1 ]; then
local args=() i sel
for i in "${!tags[@]}"; do args+=("${tags[$i]}" "${descs[$i]}"); done
sel=$(wt --title "Free Turn Proxy" --default-item "$__def" \
--menu "$__p" 17 74 "${#tags[@]}" "${args[@]}" 3>&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 </dev/tty
sel="${sel:-$__def}"
for i in "${!tags[@]}"; do
[ "$sel" = "${tags[$i]}" ] && { printf -v "$__v" '%s' "$sel"; return; }
done
warn "Неверный выбор: $sel"
done
fi
}
ask_port() { # VAR PROMPT DEFAULT
local __v="$1" __p="$2" __d="$3"
while :; do
ui_input "$__v" "$__p" "$__d"
valid_port "${!__v}" && break
ui_msg "Ошибка" "Порт — число 1–65535. Получено: '${!__v}'"
done
}
# Прелоадер: gauge (whiptail) | спиннер (tty) | строка (pipe/NI). Возвращает код задачи.
with_progress() { # TITLE CMD ARGS...
local title="$1"; shift
local log; log="$(mktemp)"
local rc=0
if [ "$HAS_WHIPTAIL" = 1 ]; then
( trap - ERR; "$@" >"$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 <version> <asset-name> — 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" <<EOF
# Сгенерировано install-server.sh — переиспользуется при обновлении. Не редактируйте вручную.
INSTALL_METHOD="$INSTALL_METHOD"
VERSION="$VERSION"
PROXY_MODE="$PROXY_MODE"
@@ -150,157 +255,129 @@ OBF_PROFILE="$OBF_PROFILE"
OBF_KEY="$OBF_KEY"
CLIENTS_FILE_CONF="$CLIENTS_FILE_CONF"
EOF
chmod 600 "$CONF_FILE" # содержит OBF_KEY
chmod 600 "$CONF_FILE"
}
apply_overrides() { # CLI бьёт install.conf
local kv k v
for kv in "${OVERRIDES[@]+"${OVERRIDES[@]}"}"; do k="${kv%%=*}"; v="${kv#*=}"; printf -v "$k" '%s' "$v"; done
}
connect_addr() { echo "127.0.0.1:${BACKEND_PORT}"; }
method_label() { [ "$INSTALL_METHOD" = "1" ] && echo "docker" || echo "systemd"; }
# ============================================================================
# Мастер настройки (install / reconfigure)
# ============================================================================
validate_config() {
case "$INSTALL_METHOD" in 1|2) ;; *) error "method: docker|systemd (1|2), а не '$INSTALL_METHOD'" ;; esac
case "$PROXY_MODE" in udp|tcp) ;; *) error "mode: udp|tcp, а не '$PROXY_MODE'" ;; esac
valid_port "$LISTEN_PORT" || error "listen-port невалиден: '$LISTEN_PORT'"
[ -z "$BACKEND_PORT" ] && { [ "$PROXY_MODE" = "udp" ] && BACKEND_PORT="51820" || BACKEND_PORT="443"; }
valid_port "$BACKEND_PORT" || error "backend-port невалиден: '$BACKEND_PORT'"
case "$OBF_PROFILE" in
rtpopus)
[ -z "$OBF_KEY" ] && { OBF_KEY="$(openssl rand -hex 32)"; info "Сгенерирован ключ: $OBF_KEY"; }
valid_hex64 "$OBF_KEY" || error "obf-key — ровно 64 hex-символа" ;;
none) OBF_KEY="" ;;
*) error "obf: rtpopus|none, а не '$OBF_PROFILE'" ;;
esac
}
# ---------------------------------------------------------------------------
# Мастер
# ---------------------------------------------------------------------------
wizard() {
echo ""
info "Выберите метод установки сервера:"
echo -e " ${GREEN}1) Docker Compose (Рекомендуется)${NC}"
echo -e " 2) Systemd (Прямой запуск бинарника)"
ask INSTALL_METHOD "Ваш выбор [1/2]" "${INSTALL_METHOD:-1}"
local m def_method; [ "$INSTALL_METHOD" = "2" ] && def_method="systemd" || def_method="docker"
ui_menu m "Метод установки сервера:" "$def_method" \
docker "Docker Compose (рекомендуется)" \
systemd "Systemd (бинарь напрямую)"
[ "$m" = "docker" ] && INSTALL_METHOD="1" || INSTALL_METHOD="2"
# WireGuard (только подсказка дефолтного порта бэкенда)
echo ""
info "Проверка WireGuard..."
local wg_port=""
if ! command -v wg >/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" <<EOF
[Unit]
Description=Free TURN Proxy Server
@@ -361,144 +438,157 @@ Group=nogroup
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE" >/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 <id_клиента>"
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 <id>"
else
echo " Добавить: ${APP_DIR}/server -clients-file ${CLIENTS_FILE_CONF} clients add <id_клиента>"
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 <id>"
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 <<EOF
Free Turn Proxy Server — установщик.
sudo bash install-server.sh интерактивно (whiptail/текст)
sudo bash install-server.sh -y [опции] non-interactive
Опции:
-y, --yes, --non-interactive без вопросов
--method docker|systemd метод (default docker)
--mode udp|tcp режим (default udp)
--backend-port N порт бэкенда (default udp→51820 / tcp→443)
--listen-port N внешний порт (default 56000)
--obf rtpopus|none обфускация (default rtpopus)
--obf-key HEX64 ключ (нет → сгенерируется)
--clients-auth / --no-clients-auth авторизация Client ID (default off)
--version latest|vX.Y.Z версия (default latest)
--firewall / --no-firewall открывать порт (NI default: да)
--update | --reconfigure обновить / переустановить
--uninstall [--purge] удалить (--purge — снести каталог)
-h, --help
Примеры:
sudo bash install-server.sh -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
EOF
}
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes|--non-interactive) NONINTERACTIVE=1 ;;
--method)
case "${2:-}" in docker|1) OVERRIDES+=("INSTALL_METHOD=1");; systemd|2) OVERRIDES+=("INSTALL_METHOD=2");; *) error "--method: docker|systemd";; esac; shift ;;
--mode) OVERRIDES+=("PROXY_MODE=${2:-}"); shift ;;
--backend-port) OVERRIDES+=("BACKEND_PORT=${2:-}"); shift ;;
--listen-port) OVERRIDES+=("LISTEN_PORT=${2:-}"); shift ;;
--obf) OVERRIDES+=("OBF_PROFILE=${2:-}"); shift ;;
--obf-key) OVERRIDES+=("OBF_KEY=${2:-}"); shift ;;
--clients-auth) OVERRIDES+=("CLIENTS_FILE_CONF=${APP_DIR}/clients.json") ;;
--no-clients-auth) OVERRIDES+=("CLIENTS_FILE_CONF=") ;;
--version) OVERRIDES+=("VERSION=${2:-}"); shift ;;
--firewall) OPEN_FIREWALL=1 ;;
--no-firewall) OPEN_FIREWALL=0 ;;
--update) ACTION="update"; NONINTERACTIVE=1 ;;
--reconfigure) ACTION="reconfigure"; NONINTERACTIVE=1 ;;
--uninstall) ACTION="uninstall"; NONINTERACTIVE=1 ;;
--purge) PURGE=1 ;;
-h|--help) usage; exit 0 ;;
*) error "Неизвестный аргумент: $1 (см. --help)" ;;
esac
shift
done
}
banner() {
[ "$HAS_WHIPTAIL" = 1 ] && return 0
echo -e "${CYAN}"
cat << "EOF"
______ _____ ____
@@ -509,16 +599,17 @@ main() {
/____/
EOF
echo -e "${NC}"
}
main() {
parse_args "$@"
[ "$(id -u)" -ne 0 ] && error "Запустите от root (sudo)."
ensure_base_deps
detect_arch
if is_installed; then
load_config
menu_existing
else
flow_install
fi
if [ "$NONINTERACTIVE" = 1 ]; then run_noninteractive; return; fi
ensure_tui
banner
if is_installed; then load_config; menu_existing; else flow_install; fi
}
main "$@"