mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
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:
+61
-83
@@ -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
@@ -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
@@ -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 "$@"
|
||||
|
||||
Reference in New Issue
Block a user