From 5edd23bd2b876837ac5c77bcf9be3c4c743d7d1a Mon Sep 17 00:00:00 2001 From: PinkmanXXX <56165502+PinkmanXXX@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:12:37 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D0=BE=D0=B8=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D0=B8=D0=BF=D1=82=D1=8B=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B8=203X-UI=20+=20REALITY=20=D0=B8=20Hysteria2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/3x-ui.sh: официальная 3X-UI v3.8.5 её установщиком, сертификат Let's Encrypt на IP для панели, подключение VLESS REALITY через API, подбор сайта для маскировки, ufw, ссылка и QR-код - scripts/hysteria2.sh: официальный Hysteria 2.12.3 с проверкой sha256, Let's Encrypt или самоподписанный сертификат с pinSHA256, сайт-заглушка, ufw, команда hy2 для управления пользователями - Инструкции переписаны под скрипты, старые (x-ui-pro, Blitz) удалены Проверено: сквозные тесты в контейнерах с systemd и подключение через интернет к тестовому серверу (VLESS REALITY и Hysteria2). --- README.md | 37 +- manuals/3x-ui.md | 89 +++++ manuals/assets/script-3x-ui.svg | 1 + manuals/assets/script-hy2-add.svg | 1 + manuals/assets/script-hysteria2.svg | 1 + manuals/hysteria2.md | 76 +++++ manuals/xkeen-keenetic.md | 2 +- scripts/3x-ui.sh | 296 ++++++++++++++++ scripts/hysteria2.sh | 501 ++++++++++++++++++++++++++++ 9 files changed, 988 insertions(+), 16 deletions(-) create mode 100644 manuals/3x-ui.md create mode 100644 manuals/assets/script-3x-ui.svg create mode 100644 manuals/assets/script-hy2-add.svg create mode 100644 manuals/assets/script-hysteria2.svg create mode 100644 manuals/hysteria2.md create mode 100755 scripts/3x-ui.sh create mode 100755 scripts/hysteria2.sh diff --git a/README.md b/README.md index 5a53ff0..1236636 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Пошаговые инструкции: свой VPN-сервер за 15 минут и прокси на роутере Keenetic** -[![Инструкций](https://img.shields.io/badge/инструкций-4-3fb950)](#инструкции) +[![Инструкций](https://img.shields.io/badge/инструкций-3-3fb950)](#инструкции) [![Обновлено](https://img.shields.io/github/last-commit/PinkmanXXX/pinkman_manuals?label=обновлено&color=3fb950)](https://github.com/PinkmanXXX/pinkman_manuals/commits) [Инструкции](#инструкции) · [Что понадобится](#что-понадобится) · [Полезное](#полезное) @@ -13,18 +13,21 @@ --- -Здесь собраны короткие проверенные инструкции: как поднять VPN-сервер на -3X-UI или Hysteria2 и направить трафик через прокси прямо на роутере -Keenetic с XKeen. Каждая инструкция — -одна страница: команды, которые нужно скопировать, и схемы нужных настроек. +Свой VPN-сервер — одной командой. Скрипты из этого репозитория ставят +3X-UI с REALITY или Hysteria2 на чистый VPS, сами настраивают сертификаты +и файрвол и выдают готовую ссылку с QR-кодом. А инструкция по XKeen поможет +пустить трафик через прокси прямо на роутере Keenetic. + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/3x-ui.sh) +``` ## Инструкции | | Инструкция | Что получится | Сложность | |---|---|---|---| -| 🛡 | [3X-UI Pro с REALITY](manuals/3x-ui-pro-reality.md) | Панель, VLESS REALITY и подписки на 443 порту за nginx | ●●○ | -| ⚡ | [3X-UI с 4 инбаундами](manuals/3x-ui-4-inbounds.md) | То же одной командой, без своих доменов | ●○○ | -| 🚀 | [Hysteria2 (Blitz)](manuals/hysteria2-blitz.md) | Быстрый протокол на UDP с веб-панелью | ●○○ | +| 🛡 | [3X-UI + REALITY](manuals/3x-ui.md) | Панель и подключение VLESS REALITY одной командой, без доменов | ●○○ | +| 🚀 | [Hysteria2](manuals/hysteria2.md) | Быстрый протокол на UDP, друзья добавляются командой `hy2 add` | ●○○ | | 📶 | [XKeen на Keenetic](manuals/xkeen-keenetic.md) | Прокси для выбранных устройств прямо на роутере | ●●○ | > [!WARNING] @@ -33,9 +36,8 @@ Keenetic с XKeen. Каждая инструкция — ## Что понадобится -- VPS с **Ubuntu 24.04** или **Debian 12** и доступом по SSH -- Для 3X-UI Pro — **два домена или сабдомена**, направленных на IP сервера - (для «4 инбаундов» домены не нужны) +- VPS с **Ubuntu 22.04/24.04** или **Debian 12/13** и доступом root по SSH +- Для Hysteria2 по желанию — домен, направленный на IP сервера - Для XKeen — роутер **Keenetic/Netcraze** и USB-флешка ## Полезное @@ -46,12 +48,17 @@ Keenetic с XKeen. Каждая инструкция — - [Генератор конфигураций Mihomo](https://rockblack.info/mihomo_generator) от RockBlack - [IP-адреса для AmneziaWG](https://github.com/RockBlack-VPN/ip-address) — актуальные списки от RockBlack +## Как устроены скрипты + +Скрипты ставят только официальные сборки — [3X-UI](https://github.com/MHSanaei/3x-ui) +и [Hysteria](https://github.com/HyNetworks/hysteria) — в версиях, которые мы +проверили: версия закреплена в начале каждого скрипта, скачанный файл сверяется +с контрольной суммой. Код открыт: [scripts/](scripts/). + ## Благодарности -Инструкции опираются на работу авторов этих проектов: -[x-ui-pro](https://github.com/mozaroc/x-ui-pro) · +Скрипты и инструкции опираются на работу авторов этих проектов: [3X-UI](https://github.com/MHSanaei/3x-ui) · -[Blitz](https://github.com/ReturnFI/Blitz) · -[Hysteria](https://github.com/apernet/hysteria) · +[Hysteria](https://github.com/HyNetworks/hysteria) · [XKeen](https://github.com/jameszeroX/XKeen) · [Xray-core](https://github.com/XTLS/Xray-core) diff --git a/manuals/3x-ui.md b/manuals/3x-ui.md new file mode 100644 index 0000000..c1f8d12 --- /dev/null +++ b/manuals/3x-ui.md @@ -0,0 +1,89 @@ +# 🛡 3X-UI + REALITY + +[← Все инструкции](../README.md#инструкции) + +Одна команда ставит панель [3X-UI](https://github.com/MHSanaei/3x-ui) и сразу +создаёт рабочее подключение VLESS REALITY. Домены не нужны. + +**Что делает [скрипт](../scripts/3x-ui.sh):** + +- ставит официальную 3X-UI проверенной версии её собственным установщиком; +- выпускает для панели сертификат Let's Encrypt на IP сервера и сам его продлевает; +- прячет панель на случайном порту и случайном пути, со случайными логином и паролем; +- подбирает сайт для маскировки REALITY — проверяет, что тот отвечает по TLS 1.3 и HTTP/2; +- создаёт подключение VLESS REALITY на 443 порту и первого клиента; +- открывает в ufw только нужные порты; +- выдаёт ссылку `vless://` и QR-код. + +## Что понадобится + +- VPS с Ubuntu 22.04/24.04 или Debian 12/13, доступ root по SSH +- Свободные порты **443** и **80** — на свежем сервере они свободны + +## Установка + +Подключитесь к серверу по SSH и выполните: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/3x-ui.sh) +``` + +Через пару минут скрипт покажет всё нужное: + +![Результат установки 3X-UI](assets/script-3x-ui.svg) + +1. **Адрес панели** — откройте его в браузере. +2. **Логин и пароль** от панели. +3. **Ссылка для подключения** — вставьте её в клиент или отсканируйте QR-код. + +Всё это сохраняется в файл `/root/3x-ui.txt` — посмотреть снова: `cat /root/3x-ui.txt`. + +## Подключение + +Ссылку `vless://` поддерживают: + +| Устройство | Приложение | +|---|---| +| iPhone, iPad | Streisand, Happ, Hiddify | +| Android | Hiddify, v2rayNG, Happ | +| Windows, macOS, Linux | Hiddify, v2rayN | + +В приложении нажмите **+** → **Импорт из буфера** или отсканируйте QR-код. + +## Добавить друга + +В панели откройте **Клиенты → Добавить клиента**, выберите подключение +**REALITY**, задайте имя и при желании лимит трафика и срок. Ссылку для +друга покажет кнопка **QR-код** рядом с клиентом. + +## Параметры + +Команду можно запустить с параметрами — например, если порт 443 уже занят: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/3x-ui.sh) --port 8443 +``` + +| Параметр | Что делает | +|---|---| +| `--port 8443` | порт REALITY вместо 443 | +| `--sni www.apple.com` | свой сайт для маскировки | +| `--panel-ssl none` | без сертификата: панель будет доступна только через SSH-туннель | +| `--no-ufw` | не трогать файрвол | + +> [!NOTE] +> Если порт 80 занят, сертификат для панели не получить — скрипт сам +> переключится на доступ через SSH-туннель и покажет нужную команду. + +## Обслуживание + +Управление панелью — официальная команда `x-ui`: перезапуск, логи, +обновление, удаление. + +```bash +x-ui +``` + +--- + +[← Все инструкции](../README.md#инструкции) · [Дальше: 🚀 Hysteria2 →](hysteria2.md) diff --git a/manuals/assets/script-3x-ui.svg b/manuals/assets/script-3x-ui.svg new file mode 100644 index 0000000..262f589 --- /dev/null +++ b/manuals/assets/script-3x-ui.svg @@ -0,0 +1 @@ +root@vps: ~root@vps: ~# bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/…/3x-ui.sh)==> Ставлю пакеты: curl, jq, openssl, qrencode, ufw==> Маскировка: www.microsoft.com==> Ставлю 3X-UI v3.8.5 официальным установщиком (пара минут)==> Создаю подключение VLESS REALITY на порту 443==> Настраиваю ufw: SSH 22, REALITY 443, панель 41873 и 80Готово! 3X-UI и REALITY работают.1Панель: https://203.0.113.10:41873/Xk2vQ9pLmT4sRz8aWc2Логин: aB3kL9mQ2x2Пароль: Zp7Wq2Lk9Vn4Tx8Rb3Hs3vless://5f2c…@203.0.113.10:443?type=tcp&security=reality&pbk=…[ здесь QR-код — отсканируйте его телефоном ] \ No newline at end of file diff --git a/manuals/assets/script-hy2-add.svg b/manuals/assets/script-hy2-add.svg new file mode 100644 index 0000000..c1a0d9d --- /dev/null +++ b/manuals/assets/script-hy2-add.svg @@ -0,0 +1 @@ +root@vps: ~root@vps: ~# hy2 add friend==> Пользователь friend добавлен.hy2://friend:4b7d…@hy.example.com:443/?sni=hy.example.com#friend# hy2 listПользователи: • admin • friend# hy2 del friend==> Пользователь friend удалён, его ссылка больше не работает. \ No newline at end of file diff --git a/manuals/assets/script-hysteria2.svg b/manuals/assets/script-hysteria2.svg new file mode 100644 index 0000000..f5a8157 --- /dev/null +++ b/manuals/assets/script-hysteria2.svg @@ -0,0 +1 @@ +root@vps: ~root@vps: ~# bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/…/hysteria2.sh)Установка Hysteria21Домен (Enter — без домена): hy.example.com1Почта для Let's Encrypt: you@example.com==> Скачиваю Hysteria 2.12.3 (amd64) с GitHub==> Контрольная сумма совпала: 8c7a68a906998b74…==> Настраиваю ufw: SSH 22/tcp, Hysteria 443/udp, сайт 80/tcp и 443/tcp==> Запускаю Hysteria и получаю сертификат для hy.example.comГотово! Hysteria2 работает.2hy2://admin:9c1e…@hy.example.com:443/?sni=hy.example.com#admin[ здесь QR-код — отсканируйте его телефоном ] \ No newline at end of file diff --git a/manuals/hysteria2.md b/manuals/hysteria2.md new file mode 100644 index 0000000..994f7be --- /dev/null +++ b/manuals/hysteria2.md @@ -0,0 +1,76 @@ +# 🚀 Hysteria2 + +[← Все инструкции](../README.md#инструкции) + +[Hysteria2](https://github.com/HyNetworks/hysteria) работает поверх UDP и +хорошо держит скорость на плохих каналах — отличная пара к REALITY. Одна +команда ставит сервер, сертификат и сайт-заглушку и выдаёт готовую ссылку. + +**Что делает [скрипт](../scripts/hysteria2.sh):** + +- ставит официальный Hysteria2 проверенной версии и сверяет контрольную сумму; +- с доменом — получает сертификат Let's Encrypt, без домена — делает свой + и закрепляет его отпечаток прямо в ссылке; +- поднимает сайт-заглушку: при заходе на ваш домен откроется страница + «RETRYING CONNECTION»; +- создаёт пользователя со случайным паролем и открывает нужные порты в ufw; +- выдаёт ссылку `hy2://` и QR-код; +- ставит команду `hy2`, чтобы добавлять и удалять друзей без правки конфигов. + +## Что понадобится + +- VPS с Ubuntu 22.04/24.04 или Debian 12/13, доступ root по SSH +- По желанию — домен, A-запись которого указывает на IP сервера + +## Установка + +Подключитесь к серверу по SSH и выполните: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/hysteria2.sh) +``` + +![Установка Hysteria2](assets/script-hysteria2.svg) + +1. Скрипт спросит **домен и почту**. Нет домена — просто нажмите Enter. +2. В конце он покажет **ссылку** `hy2://` и QR-код. + +> [!TIP] +> Без домена всё тоже работает. Домен даёт настоящий сертификат и +> сайт-заглушку, так что сервер снаружи выглядит обычным сайтом. + +## Подключение + +Ссылку `hy2://` поддерживают: + +| Устройство | Приложение | +|---|---| +| iPhone, iPad | Streisand, Hiddify, Happ | +| Android | Hiddify, NekoBox | +| Windows, macOS, Linux | Hiddify, v2rayN | + +## Друзья и управление + +![Команды hy2](assets/script-hy2-add.svg) + +| Команда | Что делает | +|---|---| +| `hy2 add имя` | добавить пользователя и показать его ссылку | +| `hy2 del имя` | удалить — его ссылка сразу перестанет работать | +| `hy2 list` | список пользователей | +| `hy2 link имя` | ссылка и QR-код ещё раз | +| `hy2 status` | версия, адрес и состояние | +| `hy2 update` | обновить до новой проверенной версии | +| `hy2 uninstall` | удалить всё | + +## Если что-то пошло не так + +- **«Не удалось получить сертификат»** — A-запись домена ещё не обновилась + или порт 80 закрыт у хостера. Исправьте и выполните `hy2 restart`. +- **Клиент не подключается** — проверьте, что у хостера открыт UDP-порт 443 + (у некоторых хостеров есть свой файрвол в личном кабинете). +- **Порт 443 занят** — поставьте на другой: добавьте к команде установки `--port 8443`. + +--- + +[← Все инструкции](../README.md#инструкции) · [Дальше: 📶 XKeen на Keenetic →](xkeen-keenetic.md) diff --git a/manuals/xkeen-keenetic.md b/manuals/xkeen-keenetic.md index 1097245..13881a0 100644 --- a/manuals/xkeen-keenetic.md +++ b/manuals/xkeen-keenetic.md @@ -11,7 +11,7 @@ - Роутер **Keenetic** или **Netcraze** с USB-портом - USB-флешка, отформатированная в **ext4** -- Свой сервер с VLESS — например, из [этой инструкции](3x-ui-4-inbounds.md) +- Свой сервер с VLESS — например, из [этой инструкции](3x-ui.md) ## 1. Установите компоненты diff --git a/scripts/3x-ui.sh b/scripts/3x-ui.sh new file mode 100755 index 0000000..d713d2e --- /dev/null +++ b/scripts/3x-ui.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# 3X-UI + VLESS REALITY одной командой — https://github.com/PinkmanXXX/pinkman_manuals +# +# Установка: bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/3x-ui.sh) +# +# Ставит официальную панель 3X-UI (версия закреплена ниже) её собственным +# установщиком, получает для панели сертификат Let's Encrypt на IP, создаёт +# подключение VLESS REALITY на 443 порту, настраивает ufw и выдаёт ссылку +# vless:// с QR-кодом. Домены не нужны. + +set -Eeuo pipefail + +XUI_VERSION="v3.8.5" +XUI_REPO="MHSanaei/3x-ui" +RESULT=/root/3x-ui.txt +XUI_ENV=/etc/x-ui/install-result.env +# Сайты для маскировки REALITY: нужны TLS 1.3 и HTTP/2. Берём первый доступный. +SNI_CANDIDATES=(www.microsoft.com www.apple.com dl.google.com www.amazon.com) + +if [[ -t 1 ]]; then + G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m' +else + G=; Y=; R=; B=; D=; N= +fi +say() { printf '%s\n' "${G}==>${N} $*"; } +warn() { printf '%s\n' "${Y}!${N} $*" >&2; } +die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; } +trap 'die "Ошибка в строке $LINENO. Исправьте причину и запустите скрипт ещё раз."' ERR + +rand_str() { openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$1"; } +port_busy() { ss -H -ln"${2:0:1}" "sport = :$1" 2>/dev/null | grep -q .; } + +public_ip() { + local ip + for u in https://api.ipify.org https://ifconfig.me/ip https://ipv4.icanhazip.com; do + ip=$(curl -4 -fsS -m 6 "$u" 2>/dev/null | tr -d '[:space:]') || true + [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && { echo "$ip"; return; } + done + ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' +} + +free_port() { + local p + for _ in $(seq 1 50); do + p=$(shuf -i 20000-60000 -n 1) + port_busy "$p" tcp || { echo "$p"; return; } + done + die "Не нашёл свободный порт для панели." +} + +# REALITY маскируется под чужой сайт: он должен отвечать по TLS 1.3 и HTTP/2. +sni_ok() { + echo | timeout 8 openssl s_client -connect "$1:443" -servername "$1" -tls1_3 -alpn h2 2>/dev/null \ + | grep -q 'ALPN protocol: h2' +} + +# ---------- API панели ---------- + +api() { # METHOD path [json] + local url="$API/$2" out + if [[ $1 == GET ]]; then + out=$(curl -fsSk -m 20 -H "Authorization: Bearer $TOKEN" "$url") + else + out=$(curl -fsSk -m 20 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X "$1" -d "$3" "$url") + fi + [[ $(jq -r '.success' <<<"$out") == true ]] || die "Панель ответила ошибкой на $2: $(jq -r '.msg // .' <<<"$out" | head -c 300)" + jq -c '.obj' <<<"$out" +} + +wait_panel() { + local i + for i in $(seq 1 60); do + curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $TOKEN" "$API/server/getNewUUID" 2>/dev/null && return 0 + sleep 2 + done + die "Панель не отвечает. Лог: journalctl -u x-ui -n 50" +} + +# ---------- установка ---------- + +main() { + [[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз." + command -v systemctl >/dev/null || die "Нужен systemd." + [[ -f $RESULT ]] && die "3X-UI уже установлена этим скриптом. Управление: команда x-ui, данные для входа: cat $RESULT" + if [[ -d /usr/local/x-ui && ! -f $XUI_ENV ]]; then + die "3X-UI уже установлена другим способом — не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную." + fi + + local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no + while [[ $# -gt 0 ]]; do + case $1 in + --port) PORT=$2; shift 2 ;; + --sni) SNI=$2; shift 2 ;; + --panel-ssl) PANEL_SSL=$2; shift 2 ;; + --host) HOST=$2; shift 2 ;; + --user) NAME=$2; shift 2 ;; + --no-ufw) UFW=no; shift ;; + -y|--yes) yes=yes; shift ;; + -h|--help) usage; exit 0 ;; + *) die "Неизвестный параметр: $1 (см. --help)" ;; + esac + done + [[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT" + [[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)." + [[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none" + if port_busy "$PORT" tcp && ! { [[ -f $XUI_ENV ]] && ss -H -ltnp "sport = :$PORT" | grep -q xray; }; then + die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт — укажите другой: --port 8443" + fi + + if [[ $PANEL_SSL == auto ]]; then + if port_busy 80 tcp; then + PANEL_SSL=none + warn "Порт 80 занят — сертификат для панели не получить. Панель будет доступна только через SSH-туннель." + else + PANEL_SSL=ip + fi + fi + [[ $PANEL_SSL == ip ]] && port_busy 80 tcp && die "Для сертификата панели нужен свободный порт 80/tcp." + + say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw" + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron >/dev/null + + HOST=${HOST:-$(public_ip)} + [[ -n $HOST ]] || die "Не удалось узнать внешний IP. Укажите его: --host 1.2.3.4" + + if [[ -z $SNI ]]; then + say "Выбираю сайт для маскировки REALITY" + for s in "${SNI_CANDIDATES[@]}"; do + if sni_ok "$s"; then SNI=$s; break; fi + done + [[ -n $SNI ]] || die "Ни один сайт из списка не ответил по TLS 1.3 + HTTP/2. Укажите свой: --sni example.com" + elif ! sni_ok "$SNI"; then + die "$SNI не отвечает по TLS 1.3 + HTTP/2 — REALITY с ним работать не будет. Выберите другой сайт." + fi + say "Маскировка: ${B}$SNI${N}" + + # --- официальный установщик 3X-UI с закреплённой версией --- + local panel_port panel_path panel_user panel_pass tmp + panel_port=$(free_port) + panel_path=$(rand_str 18) + panel_user=$(rand_str 10) + panel_pass=$(rand_str 20) + if [[ -f $XUI_ENV ]]; then + say "3X-UI уже стоит после прошлого запуска — продолжаю с создания подключения" + else + tmp=$(mktemp) + say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)" + curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh" + if ! XUI_NONINTERACTIVE=1 XUI_SSL_MODE="$PANEL_SSL" XUI_SERVER_IP="$HOST" \ + XUI_PANEL_PORT="$panel_port" XUI_WEB_BASE_PATH="$panel_path" \ + XUI_USERNAME="$panel_user" XUI_PASSWORD="$panel_pass" \ + bash "$tmp" "$XUI_VERSION" /var/log/3x-ui-install.log 2>&1; then + tail -20 /var/log/3x-ui-install.log >&2 + die "Установщик 3X-UI завершился с ошибкой. Полный лог: /var/log/3x-ui-install.log" + fi + rm -f "$tmp" + fi + [[ -f $XUI_ENV ]] || die "Установщик не сохранил данные входа. Лог: /var/log/3x-ui-install.log" + + # Данные для входа — из файла, который пишет сам установщик. + # shellcheck disable=SC1090 + . "$XUI_ENV" + TOKEN=$XUI_API_TOKEN + local scheme=http + [[ $XUI_ACCESS_URL == https://* ]] && scheme=https + API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api" + wait_panel + + # Без сертификата панель и подписки не должны торчать наружу по HTTP. + if [[ $PANEL_SSL == none ]]; then + say "Панель без сертификата — открываю её только для SSH-туннеля (127.0.0.1)" + local all + all=$(api POST setting/all '{}') + api POST setting/update "$(jq -c '.webListen = "127.0.0.1" | .subListen = "127.0.0.1"' <<<"$all")" >/dev/null + systemctl restart x-ui + wait_panel + fi + + # --- подключение VLESS REALITY --- + local keys priv pub uuid sid sub settings stream sniffing body existing + existing=$(api GET inbounds/list | jq -c --argjson p "$PORT" '[.[] | select(.port == $p)][0] // empty') + if [[ -n $existing ]]; then + [[ $(jq -r '.protocol' <<<"$existing") == vless ]] || die "Порт $PORT в панели уже занят другим подключением." + say "Подключение на порту $PORT уже есть после прошлого запуска — беру его" + # API отдаёт настройки то строкой JSON, то объектом — поддерживаем оба. + stream=$(jq -c '.streamSettings | if type == "string" then fromjson else . end' <<<"$existing") + settings=$(jq -c '.settings | if type == "string" then fromjson else . end' <<<"$existing") + uuid=$(jq -r '.clients[0].id' <<<"$settings") + NAME=$(jq -r '.clients[0].email' <<<"$settings") + pub=$(jq -r '.realitySettings.settings.publicKey' <<<"$stream") + sid=$(jq -r '.realitySettings.shortIds[0]' <<<"$stream") + SNI=$(jq -r '.realitySettings.serverNames[0]' <<<"$stream") + else + say "Создаю подключение VLESS REALITY на порту $PORT" + keys=$(api GET server/getNewX25519Cert) + priv=$(jq -r '.privateKey' <<<"$keys") + pub=$(jq -r '.publicKey' <<<"$keys") + uuid=$(cat /proc/sys/kernel/random/uuid) + sid=$(openssl rand -hex 8) + sub=$(rand_str 16 | tr 'A-Z' 'a-z') + + settings=$(jq -nc --arg id "$uuid" --arg email "$NAME" --arg sub "$sub" '{ + clients: [{id: $id, flow: "xtls-rprx-vision", email: $email, limitIp: 0, totalGB: 0, + expiryTime: 0, enable: true, tgId: 0, subId: $sub, reset: 0}], + decryption: "none", fallbacks: []}') + stream=$(jq -nc --arg sni "$SNI" --arg priv "$priv" --arg pub "$pub" --arg sid "$sid" '{ + network: "tcp", security: "reality", externalProxy: [], + realitySettings: {show: false, xver: 0, target: ($sni + ":443"), serverNames: [$sni], + privateKey: $priv, minClientVer: "", maxClientVer: "", maxTimediff: 0, shortIds: [$sid], + settings: {publicKey: $pub, fingerprint: "chrome", serverName: "", spiderX: "/"}}, + tcpSettings: {acceptProxyProtocol: false, header: {type: "none"}}}') + sniffing='{"enabled":true,"destOverride":["http","tls","quic"],"metadataOnly":false,"routeOnly":false}' + body=$(jq -nc --argjson port "$PORT" --arg s "$settings" --arg st "$stream" --arg sn "$sniffing" '{ + remark: "REALITY", enable: true, listen: "", port: $port, protocol: "vless", + settings: $s, streamSettings: $st, sniffing: $sn, expiryTime: 0, total: 0}') + api POST inbounds/add "$body" >/dev/null + fi + + local link + link="vless://$uuid@$HOST:$PORT?type=tcp&security=reality&pbk=$pub&fp=chrome&sni=$SNI&sid=$sid&spx=%2F&flow=xtls-rprx-vision#$NAME@$HOST" + + # Ждём, пока Xray поднимет порт. + for _ in $(seq 1 20); do port_busy "$PORT" tcp && break; sleep 1; done + port_busy "$PORT" tcp || die "Xray не открыл порт $PORT. Лог: x-ui log" + + # --- файрвол --- + if [[ $UFW == yes ]]; then + local ssh_port + ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}') + ssh_port=${ssh_port:-22} + local extra="" + [[ $PANEL_SSL == ip ]] && extra=", панель $XUI_PANEL_PORT и 80 для продления сертификата" + say "Настраиваю ufw: SSH $ssh_port, REALITY $PORT$extra" + ufw allow "$ssh_port/tcp" >/dev/null + ufw allow "$PORT/tcp" >/dev/null + if [[ $PANEL_SSL == ip ]]; then + ufw allow "$XUI_PANEL_PORT/tcp" >/dev/null + ufw allow 80/tcp >/dev/null + fi + ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную." + fi + + # --- итог --- + local panel_url + if [[ $PANEL_SSL == ip ]]; then + panel_url="https://$HOST:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH" + else + panel_url="http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH (через SSH-туннель: ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST)" + fi + umask 077 + cat >"$RESULT" <${N} $*"; } +warn() { printf '%s\n' "${Y}!${N} $*" >&2; } +die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; } + +trap 'die "Ошибка в строке $LINENO. Если это установка — исправьте причину и запустите скрипт ещё раз."' ERR + +need_root() { [[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз."; } + +# ---------- проверки ---------- + +check_os() { + [[ -r /etc/os-release ]] || die "Не удалось определить систему." + . /etc/os-release + case "${ID:-}:${VERSION_ID:-}" in + ubuntu:22.04|ubuntu:24.04|debian:12|debian:13) ;; + *) warn "Проверено на Ubuntu 22.04/24.04 и Debian 12/13, у вас ${PRETTY_NAME:-неизвестно}. Продолжаю." ;; + esac + command -v systemctl >/dev/null || die "Нужен systemd." +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) echo amd64 ;; + aarch64|arm64) echo arm64 ;; + *) die "Архитектура $(uname -m) не поддерживается." ;; + esac +} + +port_busy() { # port proto(tcp|udp) + ss -H -ln"${2:0:1}" "sport = :$1" 2>/dev/null | grep -q . +} + +public_ip() { + local ip + for u in https://api.ipify.org https://ifconfig.me/ip https://ipv4.icanhazip.com; do + ip=$(curl -4 -fsS -m 6 "$u" 2>/dev/null | tr -d '[:space:]') || true + [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && { echo "$ip"; return; } + done + ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' +} + +rand() { openssl rand -hex "${1:-16}"; } + +# ---------- установка ---------- + +install_packages() { + say "Ставлю пакеты: curl, openssl, qrencode, ufw" + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq curl openssl qrencode ca-certificates iproute2 ufw >/dev/null +} + +install_binary() { + local arch tmp url expected actual + arch=$(detect_arch) + url="https://github.com/$HY_REPO/releases/download/app/v$HY_VERSION" + tmp=$(mktemp -d) + say "Скачиваю Hysteria $HY_VERSION ($arch) с GitHub" + curl -fsSL --retry 3 -o "$tmp/hysteria" "$url/hysteria-linux-$arch" + curl -fsSL --retry 3 -o "$tmp/hashes.txt" "$url/hashes.txt" + expected=$(awk -v f="build/hysteria-linux-$arch" '$2==f {print $1}' "$tmp/hashes.txt") + actual=$(sha256sum "$tmp/hysteria" | awk '{print $1}') + [[ -n $expected && $expected == "$actual" ]] || { rm -rf "$tmp"; die "Контрольная сумма не совпала — файл повреждён или подменён."; } + install -m 755 "$tmp/hysteria" "$BIN" + rm -rf "$tmp" + say "Контрольная сумма совпала: ${D}${actual:0:16}…${N}" +} + +install_cli() { + # Скрипт копирует сам себя, чтобы работала команда hy2. + local src="${BASH_SOURCE[0]}" + if [[ -f $src && $src != /dev/fd/* && $src != /proc/* ]]; then + install -m 755 "$src" "$CLI" + else + curl -fsSL --retry 3 -o "$CLI" "$SELF_URL" && chmod 755 "$CLI" + fi +} + +write_masq() { + mkdir -p "$MASQ_DIR" + [[ -f $MASQ_DIR/index.html ]] && return + cat >"$MASQ_DIR/index.html" <<'HTML' +Please wait
RETRYING CONNECTION
+HTML +} + +make_self_signed() { + local sni=$1 + openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -keyout "$CONF_DIR/server.key" -out "$CONF_DIR/server.crt" \ + -subj "/CN=$sni" -addext "subjectAltName=DNS:$sni" -days 3650 2>/dev/null + chown hysteria:hysteria "$CONF_DIR/server.key" "$CONF_DIR/server.crt" + chmod 600 "$CONF_DIR/server.key" +} + +cert_pin() { + openssl x509 -in "$CONF_DIR/server.crt" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f' +} + +render_config() { + local u p + . "$STATE" + { + echo "# Сгенерировано hy2 — правьте через команду hy2, ручные изменения перезапишутся." + echo "listen: :$PORT" + echo + if [[ -n ${DOMAIN:-} ]]; then + echo "acme:" + echo " type: http" + echo " domains:" + echo " - $DOMAIN" + echo " email: $EMAIL" + echo " dir: $DATA_DIR/acme" + else + echo "tls:" + echo " cert: $CONF_DIR/server.crt" + echo " key: $CONF_DIR/server.key" + fi + echo + echo "auth:" + echo " type: userpass" + echo " userpass:" + while read -r u p; do + [[ -n $u ]] && echo " \"$u\": \"$p\"" + done <"$USERS" + echo + echo "masquerade:" + echo " type: file" + echo " file:" + echo " dir: $MASQ_DIR" + if [[ -n ${DOMAIN:-} ]]; then + echo " listenHTTP: :80" + echo " listenHTTPS: :$PORT" + echo " forceHTTPS: true" + fi + } >"$CONF.new" + chown root:hysteria "$CONF.new" + chmod 640 "$CONF.new" + mv "$CONF.new" "$CONF" +} + +write_unit() { + cat >"$UNIT" <"$SYSCTL" + sysctl -q -p "$SYSCTL" 2>/dev/null || warn "Не удалось применить sysctl (так бывает в контейнерах) — на работу не влияет." +} + +setup_ufw() { + . "$STATE" + [[ ${UFW:-yes} == yes ]] || return 0 + local ssh_port + ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}') + ssh_port=${ssh_port:-22} + say "Настраиваю ufw: SSH $ssh_port/tcp, Hysteria $PORT/udp${DOMAIN:+, сайт 80/tcp и $PORT/tcp}" + ufw allow "$ssh_port/tcp" >/dev/null + ufw allow "$PORT/udp" >/dev/null + if [[ -n ${DOMAIN:-} ]]; then + ufw allow 80/tcp >/dev/null + ufw allow "$PORT/tcp" >/dev/null + fi + ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную." +} + +# Ждём строку «server up and running» в логе с момента запуска: с доменом +# Hysteria сначала получает сертификат, это занимает до минуты. +start_and_wait() { + local since i log + since=$(date '+%Y-%m-%d %H:%M:%S') + systemctl restart hysteria-server + for i in $(seq 1 90); do + log=$(journalctl -u hysteria-server --since "$since" --no-pager -o cat 2>/dev/null || true) + grep -q 'server up and running' <<<"$log" && return 0 + if grep -q 'FATAL' <<<"$log"; then + systemctl stop hysteria-server || true + echo >&2 + grep 'FATAL' <<<"$log" | tail -1 | cut -c1-400 >&2 + echo >&2 + if grep -qi 'acme\|certificate' <<<"$log"; then + die "Не удалось получить сертификат. Проверьте, что A-запись домена указывает на этот сервер, а порт 80/tcp открыт у хостера. Потом: hy2 restart" + fi + die "Hysteria не запустилась — причина выше. После исправления: hy2 restart" + fi + sleep 1 + done + die "Hysteria не ответила за 90 секунд. Лог: journalctl -u hysteria-server -n 50" +} + +cmd_install() { + need_root + check_os + [[ -f $STATE ]] && die "Hysteria уже установлена этим скриптом. Команды управления: hy2 help" + + local DOMAIN="" EMAIL="" PORT=443 USERNAME="admin" HOST="" UFW=yes SNI="" yes=no + while [[ $# -gt 0 ]]; do + case $1 in + --domain) DOMAIN=$2; shift 2 ;; + --email) EMAIL=$2; shift 2 ;; + --port) PORT=$2; shift 2 ;; + --user) USERNAME=$2; shift 2 ;; + --host) HOST=$2; shift 2 ;; + --sni) SNI=$2; shift 2 ;; + --no-ufw) UFW=no; shift ;; + -y|--yes) yes=yes; shift ;; + *) die "Неизвестный параметр: $1 (см. hy2 help)" ;; + esac + done + + if [[ $yes == no && -t 0 && -z $DOMAIN ]]; then + echo + echo "${B}Установка Hysteria2${N}" + echo "С доменом сертификат выпустит Let's Encrypt, а по адресу домена откроется сайт-заглушка." + echo "Без домена будет самоподписанный сертификат — это тоже работает." + read -rp "Домен (Enter — без домена): " DOMAIN + if [[ -n $DOMAIN ]]; then read -rp "Почта для Let's Encrypt: " EMAIL; fi + fi + if [[ -n $DOMAIN ]]; then + [[ $DOMAIN =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] || die "Похоже, это не домен: $DOMAIN" + [[ $EMAIL == *@*.* ]] || die "Для Let's Encrypt нужна почта: --email you@example.com" + fi + [[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT" + [[ $USERNAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя пользователя: латиница, цифры, _ . - (до 32 символов)." + + port_busy "$PORT" udp && die "UDP-порт $PORT уже занят. Выберите другой: --port 8443" + if [[ -n $DOMAIN ]]; then + port_busy 80 tcp && die "TCP-порт 80 занят — он нужен Let's Encrypt для проверки домена." + port_busy "$PORT" tcp && die "TCP-порт $PORT занят — на нём будет сайт-заглушка." + fi + + install_packages + HOST=${HOST:-${DOMAIN:-$(public_ip)}} + [[ -n $HOST ]] || die "Не удалось узнать внешний IP. Укажите его: --host 1.2.3.4" + if [[ -n $DOMAIN ]]; then + local resolved my_ip + resolved=$(getent ahostsv4 "$DOMAIN" 2>/dev/null | awk 'NR==1{print $1}') || true + [[ -n $resolved ]] || die "Домен $DOMAIN не найден в DNS. Создайте A-запись на IP сервера и подождите пару минут." + my_ip=$(public_ip) + [[ $resolved == "$my_ip" ]] || warn "Домен $DOMAIN указывает на $resolved, а IP сервера $my_ip. Если сертификат не выпустится — проверьте A-запись." + fi + SNI=${SNI:-${DOMAIN:-www.bing.com}} + + id hysteria &>/dev/null || useradd --system --no-create-home --home-dir "$DATA_DIR" --shell /usr/sbin/nologin hysteria + install -d -o hysteria -g hysteria -m 750 "$DATA_DIR" + install -d -o root -g hysteria -m 750 "$CONF_DIR" + + install_binary + write_masq + [[ -z $DOMAIN ]] && make_self_signed "$SNI" + + cat >"$STATE" <"$USERS" + chmod 600 "$USERS" + + render_config + write_unit + tune_sysctl + install_cli + setup_ufw + + say "Запускаю Hysteria${DOMAIN:+ и получаю сертификат для $DOMAIN}" + systemctl daemon-reload + systemctl enable hysteria-server >/dev/null 2>&1 + start_and_wait + + echo + echo "${G}${B}Готово! Hysteria2 работает.${N}" + show_link "$USERNAME" + echo + echo "Добавить друга: ${B}hy2 add имя${N} · все команды: ${B}hy2 help${N}" +} + +# ---------- управление ---------- + +require_installed() { need_root; [[ -f $STATE ]] || die "Hysteria не установлена этим скриптом."; } + +user_link() { + . "$STATE" + local u=$1 p q + p=$(awk -v u="$u" '$1==u {print $2}' "$USERS") + [[ -n $p ]] || die "Нет пользователя $u" + q="sni=$SNI" + if [[ -z ${DOMAIN:-} ]]; then q="$q&insecure=1&pinSHA256=$(cert_pin)"; fi + echo "hy2://$u:$p@$HOST:$PORT/?$q#$u@$HOST" +} + +show_link() { + local link + link=$(user_link "$1") + echo + echo "Ссылка для ${B}$1${N} — вставьте в Hiddify, v2rayN, Streisand или NekoBox:" + echo + echo "$link" + echo + command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$link" +} + +reload_service() { + render_config + start_and_wait +} + +cmd_restart() { + require_installed + systemctl reset-failed hysteria-server 2>/dev/null || true + reload_service + say "Hysteria перезапущена." +} + +cmd_add() { + require_installed + local u=${1:-} + [[ $u =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Использование: hy2 add имя (латиница, цифры, _ . -)" + awk -v u="$u" '$1==u {f=1} END {exit !f}' "$USERS" && die "Пользователь $u уже есть. Ссылка: hy2 link $u" + printf '%s %s\n' "$u" "$(rand 16)" >>"$USERS" + reload_service + say "Пользователь $u добавлен." + show_link "$u" +} + +cmd_del() { + require_installed + local u=${1:-} + [[ -n $u ]] || die "Использование: hy2 del имя" + awk -v u="$u" '$1==u {f=1} END {exit !f}' "$USERS" || die "Нет пользователя $u" + [[ $(grep -c . "$USERS") -gt 1 ]] || die "Это последний пользователь — сначала добавьте другого." + awk -v u="$u" '$1!=u' "$USERS" >"$USERS.new" && mv "$USERS.new" "$USERS" && chmod 600 "$USERS" + reload_service + say "Пользователь $u удалён, его ссылка больше не работает." +} + +cmd_list() { + require_installed + echo "Пользователи:" + awk '{print " • " $1}' "$USERS" +} + +cmd_link() { + require_installed + local u=${1:-$(awk 'NR==1{print $1}' "$USERS")} + show_link "$u" +} + +cmd_status() { + require_installed + . "$STATE" + echo "Версия: $("$BIN" version 2>/dev/null | awk '/^Version:/ {print $2}')" + echo "Адрес: $HOST:$PORT/udp" + if [[ -n ${DOMAIN:-} ]]; then echo "Сертификат: Let's Encrypt для $DOMAIN"; else echo "Сертификат: самоподписанный ($SNI)"; fi + echo "Пользователей: $(grep -c . "$USERS")" + systemctl --no-pager --lines=5 status hysteria-server || true +} + +cmd_update() { + require_installed + local tmp + tmp=$(mktemp) + say "Скачиваю свежую версию скрипта" + curl -fsSL --retry 3 -o "$tmp" "$SELF_URL" + bash "$tmp" __update_binary + install -m 755 "$tmp" "$CLI" + rm -f "$tmp" +} + +cmd_update_binary() { + require_installed + local cur + cur=$("$BIN" version 2>/dev/null | awk '/^Version:/ {print $2}') + if [[ $cur == "v$HY_VERSION" ]]; then say "Уже стоит последняя проверенная версия $HY_VERSION."; return; fi + install_binary + reload_service + say "Hysteria обновлена: $cur → v$HY_VERSION" +} + +cmd_uninstall() { + require_installed + local ans="" + if [[ ${1:-} != -y && -t 0 ]]; then + read -rp "Удалить Hysteria и всех пользователей? [y/N] " ans + [[ $ans =~ ^[yYдД]$ ]] || { echo "Отменено."; return; } + fi + . "$STATE" + systemctl disable --now hysteria-server >/dev/null 2>&1 || true + rm -f "$UNIT" "$BIN" "$SYSCTL" + systemctl daemon-reload + if [[ ${UFW:-yes} == yes ]] && command -v ufw >/dev/null; then + ufw delete allow "$PORT/udp" >/dev/null 2>&1 || true + if [[ -n ${DOMAIN:-} ]]; then + ufw delete allow 80/tcp >/dev/null 2>&1 || true + ufw delete allow "$PORT/tcp" >/dev/null 2>&1 || true + fi + fi + rm -rf "$CONF_DIR" "$DATA_DIR" "$MASQ_DIR" + userdel hysteria 2>/dev/null || true + rm -f "$CLI" + say "Hysteria удалена." +} + +cmd_help() { + cat <