Свои скрипты установки 3X-UI + REALITY и Hysteria2
- 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).
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Пошаговые инструкции: свой VPN-сервер за 15 минут и прокси на роутере Keenetic**
|
**Пошаговые инструкции: свой VPN-сервер за 15 минут и прокси на роутере Keenetic**
|
||||||
|
|
||||||
[](#инструкции)
|
[](#инструкции)
|
||||||
[](https://github.com/PinkmanXXX/pinkman_manuals/commits)
|
[](https://github.com/PinkmanXXX/pinkman_manuals/commits)
|
||||||
|
|
||||||
[Инструкции](#инструкции) · [Что понадобится](#что-понадобится) · [Полезное](#полезное)
|
[Инструкции](#инструкции) · [Что понадобится](#что-понадобится) · [Полезное](#полезное)
|
||||||
@@ -13,18 +13,21 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Здесь собраны короткие проверенные инструкции: как поднять VPN-сервер на
|
Свой VPN-сервер — одной командой. Скрипты из этого репозитория ставят
|
||||||
3X-UI или Hysteria2 и направить трафик через прокси прямо на роутере
|
3X-UI с REALITY или Hysteria2 на чистый VPS, сами настраивают сертификаты
|
||||||
Keenetic с XKeen. Каждая инструкция —
|
и файрвол и выдают готовую ссылку с 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 + REALITY](manuals/3x-ui.md) | Панель и подключение VLESS REALITY одной командой, без доменов | ●○○ |
|
||||||
| ⚡ | [3X-UI с 4 инбаундами](manuals/3x-ui-4-inbounds.md) | То же одной командой, без своих доменов | ●○○ |
|
| 🚀 | [Hysteria2](manuals/hysteria2.md) | Быстрый протокол на UDP, друзья добавляются командой `hy2 add` | ●○○ |
|
||||||
| 🚀 | [Hysteria2 (Blitz)](manuals/hysteria2-blitz.md) | Быстрый протокол на UDP с веб-панелью | ●○○ |
|
|
||||||
| 📶 | [XKeen на Keenetic](manuals/xkeen-keenetic.md) | Прокси для выбранных устройств прямо на роутере | ●●○ |
|
| 📶 | [XKeen на Keenetic](manuals/xkeen-keenetic.md) | Прокси для выбранных устройств прямо на роутере | ●●○ |
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
@@ -33,9 +36,8 @@ Keenetic с XKeen. Каждая инструкция —
|
|||||||
|
|
||||||
## Что понадобится
|
## Что понадобится
|
||||||
|
|
||||||
- VPS с **Ubuntu 24.04** или **Debian 12** и доступом по SSH
|
- VPS с **Ubuntu 22.04/24.04** или **Debian 12/13** и доступом root по SSH
|
||||||
- Для 3X-UI Pro — **два домена или сабдомена**, направленных на IP сервера
|
- Для Hysteria2 по желанию — домен, направленный на IP сервера
|
||||||
(для «4 инбаундов» домены не нужны)
|
|
||||||
- Для XKeen — роутер **Keenetic/Netcraze** и USB-флешка
|
- Для XKeen — роутер **Keenetic/Netcraze** и USB-флешка
|
||||||
|
|
||||||
## Полезное
|
## Полезное
|
||||||
@@ -46,12 +48,17 @@ Keenetic с XKeen. Каждая инструкция —
|
|||||||
- [Генератор конфигураций Mihomo](https://rockblack.info/mihomo_generator) от RockBlack
|
- [Генератор конфигураций Mihomo](https://rockblack.info/mihomo_generator) от RockBlack
|
||||||
- [IP-адреса для AmneziaWG](https://github.com/RockBlack-VPN/ip-address) — актуальные списки от 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) ·
|
[3X-UI](https://github.com/MHSanaei/3x-ui) ·
|
||||||
[Blitz](https://github.com/ReturnFI/Blitz) ·
|
[Hysteria](https://github.com/HyNetworks/hysteria) ·
|
||||||
[Hysteria](https://github.com/apernet/hysteria) ·
|
|
||||||
[XKeen](https://github.com/jameszeroX/XKeen) ·
|
[XKeen](https://github.com/jameszeroX/XKeen) ·
|
||||||
[Xray-core](https://github.com/XTLS/Xray-core)
|
[Xray-core](https://github.com/XTLS/Xray-core)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
```
|
||||||
|
|
||||||
|
Через пару минут скрипт покажет всё нужное:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="389" viewBox="0 0 860 389" role="img" aria-label="root@vps: ~"><title>root@vps: ~</title><style>.s{font-family:Inter, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif}.m{font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}</style><rect x="0.5" y="0.5" width="859" height="388" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H859" stroke="#30363d"/><circle cx="22" cy="22" r="6" fill="#ff5f57"/><circle cx="40" cy="22" r="6" fill="#febc2e"/><circle cx="58" cy="22" r="6" fill="#28c840"/><text x="430.0" y="27" class="m" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">root@vps: ~</text><text x="22" y="70" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># </text><text x="38.260000000000005" y="70" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/…/3x-ui.sh)</text><text x="22" y="91" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="91" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Ставлю пакеты: curl, jq, openssl, qrencode, ufw</text><text x="22" y="112" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="112" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Маскировка: www.microsoft.com</text><text x="22" y="133" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="133" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Ставлю 3X-UI v3.8.5 официальным установщиком (пара минут)</text><text x="22" y="154" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Создаю подключение VLESS REALITY на порту 443</text><text x="22" y="175" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="175" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Настраиваю ufw: SSH 22, REALITY 443, панель 41873 и 80</text><text x="22" y="196" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><text x="22" y="217" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">Готово! 3X-UI и REALITY работают.</text><text x="22" y="238" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><rect x="10" y="244" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="254.5" r="11" fill="#3fb950"/><text x="832" y="259.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="22" y="259" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Панель: </text><text x="95.17" y="259" class="m" font-size="13.5" fill="#39c5cf" xml:space="preserve">https://203.0.113.10:41873/Xk2vQ9pLmT4sRz8aWc</text><rect x="10" y="265" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="275.5" r="11" fill="#3fb950"/><text x="832" y="280.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">2</text><text x="22" y="280" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Логин: </text><text x="95.17" y="280" class="m" font-size="13.5" fill="#39c5cf" xml:space="preserve">aB3kL9mQ2x</text><rect x="10" y="286" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="296.5" r="11" fill="#3fb950"/><text x="832" y="301.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">2</text><text x="22" y="301" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Пароль: </text><text x="95.17" y="301" class="m" font-size="13.5" fill="#39c5cf" xml:space="preserve">Zp7Wq2Lk9Vn4Tx8Rb3Hs</text><text x="22" y="322" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><rect x="10" y="328" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="338.5" r="11" fill="#3fb950"/><text x="832" y="343.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">3</text><text x="22" y="343" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">vless://5f2c…@203.0.113.10:443?type=tcp&security=reality&pbk=…</text><text x="22" y="364" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">[ здесь QR-код — отсканируйте его телефоном ]</text></svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="305" viewBox="0 0 860 305" role="img" aria-label="root@vps: ~"><title>root@vps: ~</title><style>.s{font-family:Inter, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif}.m{font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}</style><rect x="0.5" y="0.5" width="859" height="304" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H859" stroke="#30363d"/><circle cx="22" cy="22" r="6" fill="#ff5f57"/><circle cx="40" cy="22" r="6" fill="#febc2e"/><circle cx="58" cy="22" r="6" fill="#28c840"/><text x="430.0" y="27" class="m" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">root@vps: ~</text><text x="22" y="70" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># </text><text x="38.260000000000005" y="70" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">hy2 add friend</text><text x="22" y="91" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="91" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Пользователь friend добавлен.</text><text x="22" y="112" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">hy2://friend:4b7d…@hy.example.com:443/?sni=hy.example.com#friend</text><text x="22" y="133" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><text x="22" y="154" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># </text><text x="38.260000000000005" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">hy2 list</text><text x="22" y="175" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Пользователи:</text><text x="22" y="196" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"> • admin</text><text x="22" y="217" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"> • friend</text><text x="22" y="238" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><text x="22" y="259" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># </text><text x="38.260000000000005" y="259" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">hy2 del friend</text><text x="22" y="280" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="280" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Пользователь friend удалён, его ссылка больше не работает.</text></svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="860" height="368" viewBox="0 0 860 368" role="img" aria-label="root@vps: ~"><title>root@vps: ~</title><style>.s{font-family:Inter, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif}.m{font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}</style><rect x="0.5" y="0.5" width="859" height="367" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H859" stroke="#30363d"/><circle cx="22" cy="22" r="6" fill="#ff5f57"/><circle cx="40" cy="22" r="6" fill="#febc2e"/><circle cx="58" cy="22" r="6" fill="#28c840"/><text x="430.0" y="27" class="m" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">root@vps: ~</text><text x="22" y="70" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># </text><text x="38.260000000000005" y="70" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/…/hysteria2.sh)</text><text x="22" y="91" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><text x="22" y="112" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Установка Hysteria2</text><rect x="10" y="118" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="128.5" r="11" fill="#3fb950"/><text x="832" y="133.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="22" y="133" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Домен (Enter — без домена): </text><text x="249.64000000000001" y="133" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">hy.example.com</text><rect x="10" y="139" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="149.5" r="11" fill="#3fb950"/><text x="832" y="154.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="22" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Почта для Let's Encrypt: </text><text x="225.25000000000003" y="154" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">you@example.com</text><text x="22" y="175" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="175" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Скачиваю Hysteria 2.12.3 (amd64) с GitHub</text><text x="22" y="196" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="196" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Контрольная сумма совпала: 8c7a68a906998b74…</text><text x="22" y="217" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="217" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Настраиваю ufw: SSH 22/tcp, Hysteria 443/udp, сайт 80/tcp и 443/tcp</text><text x="22" y="238" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==> </text><text x="54.52" y="238" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Запускаю Hysteria и получаю сертификат для hy.example.com</text><text x="22" y="259" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><text x="22" y="280" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">Готово! Hysteria2 работает.</text><text x="22" y="301" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></text><rect x="10" y="307" width="840" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="832" cy="317.5" r="11" fill="#3fb950"/><text x="832" y="322.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">2</text><text x="22" y="322" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">hy2://admin:9c1e…@hy.example.com:443/?sni=hy.example.com#admin</text><text x="22" y="343" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">[ здесь QR-код — отсканируйте его телефоном ]</text></svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -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)
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. Скрипт спросит **домен и почту**. Нет домена — просто нажмите Enter.
|
||||||
|
2. В конце он покажет **ссылку** `hy2://` и QR-код.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Без домена всё тоже работает. Домен даёт настоящий сертификат и
|
||||||
|
> сайт-заглушку, так что сервер снаружи выглядит обычным сайтом.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
Ссылку `hy2://` поддерживают:
|
||||||
|
|
||||||
|
| Устройство | Приложение |
|
||||||
|
|---|---|
|
||||||
|
| iPhone, iPad | Streisand, Hiddify, Happ |
|
||||||
|
| Android | Hiddify, NekoBox |
|
||||||
|
| Windows, macOS, Linux | Hiddify, v2rayN |
|
||||||
|
|
||||||
|
## Друзья и управление
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Команда | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `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)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
- Роутер **Keenetic** или **Netcraze** с USB-портом
|
- Роутер **Keenetic** или **Netcraze** с USB-портом
|
||||||
- USB-флешка, отформатированная в **ext4**
|
- USB-флешка, отформатированная в **ext4**
|
||||||
- Свой сервер с VLESS — например, из [этой инструкции](3x-ui-4-inbounds.md)
|
- Свой сервер с VLESS — например, из [этой инструкции](3x-ui.md)
|
||||||
|
|
||||||
## 1. Установите компоненты
|
## 1. Установите компоненты
|
||||||
|
|
||||||
|
|||||||
Executable
+296
@@ -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" </dev/null >/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" <<EOF
|
||||||
|
3X-UI $XUI_VERSION — данные для входа (файл виден только root)
|
||||||
|
|
||||||
|
Панель: $panel_url
|
||||||
|
Логин: $XUI_USERNAME
|
||||||
|
Пароль: $XUI_PASSWORD
|
||||||
|
|
||||||
|
Подключение REALITY ($NAME):
|
||||||
|
$link
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${G}${B}Готово! 3X-UI и REALITY работают.${N}"
|
||||||
|
echo
|
||||||
|
echo "Панель: ${B}$panel_url${N}"
|
||||||
|
echo "Логин: ${B}$XUI_USERNAME${N}"
|
||||||
|
echo "Пароль: ${B}$XUI_PASSWORD${N}"
|
||||||
|
echo
|
||||||
|
echo "Ссылка для ${B}$NAME${N} — вставьте в Hiddify, v2rayN, Streisand или Happ:"
|
||||||
|
echo
|
||||||
|
echo "$link"
|
||||||
|
echo
|
||||||
|
qrencode -t ANSIUTF8 -m 1 "$link" || true
|
||||||
|
echo
|
||||||
|
echo "Всё это сохранено в ${B}$RESULT${N}. Друзей добавляйте в панели:"
|
||||||
|
echo "Клиенты → Добавить клиента → подключение REALITY."
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
3X-UI + VLESS REALITY одной командой
|
||||||
|
|
||||||
|
--port 443 порт REALITY (по умолчанию 443)
|
||||||
|
--sni сайт сайт для маскировки (по умолчанию подбирается сам)
|
||||||
|
--panel-ssl ip|none сертификат панели: ip — Let's Encrypt на IP (нужен порт 80),
|
||||||
|
none — панель только через SSH-туннель (по умолчанию выбирается сам)
|
||||||
|
--user admin имя первого клиента
|
||||||
|
--host 1.2.3.4 адрес в ссылке, если IP определился неверно
|
||||||
|
--no-ufw не трогать файрвол
|
||||||
|
-y не задавать вопросов
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+501
@@ -0,0 +1,501 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Hysteria2 одной командой — https://github.com/PinkmanXXX/pinkman_manuals
|
||||||
|
#
|
||||||
|
# Установка: bash <(curl -fsSL https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/hysteria2.sh)
|
||||||
|
# Управление: hy2 help
|
||||||
|
#
|
||||||
|
# Ставит официальный бинарник Hysteria2 (версия закреплена ниже, контрольная
|
||||||
|
# сумма проверяется), настраивает сертификат, сайт-заглушку, ufw и выдаёт
|
||||||
|
# ссылку hy2:// с QR-кодом.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
HY_VERSION="2.12.3"
|
||||||
|
HY_REPO="HyNetworks/hysteria"
|
||||||
|
SELF_URL="https://raw.githubusercontent.com/PinkmanXXX/pinkman_manuals/main/scripts/hysteria2.sh"
|
||||||
|
|
||||||
|
BIN=/usr/local/bin/hysteria
|
||||||
|
CLI=/usr/local/bin/hy2
|
||||||
|
CONF_DIR=/etc/hysteria
|
||||||
|
CONF=$CONF_DIR/config.yaml
|
||||||
|
STATE=$CONF_DIR/pinkman.env # настройки установки
|
||||||
|
USERS=$CONF_DIR/users # строки «имя пароль»
|
||||||
|
MASQ_DIR=/var/www/masq
|
||||||
|
DATA_DIR=/var/lib/hysteria
|
||||||
|
UNIT=/etc/systemd/system/hysteria-server.service
|
||||||
|
SYSCTL=/etc/sysctl.d/99-hysteria.conf
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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'
|
||||||
|
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Please wait</title><style>body{background:#080808;height:100vh;margin:0;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:sans-serif}.dots{display:flex;gap:15px;margin-bottom:30px}.d{width:20px;height:20px;background:#fff;border-radius:50%;animation:b 1.4s infinite ease-in-out both}.d:nth-child(1){animation-delay:-.32s}.d:nth-child(2){animation-delay:-.16s}@keyframes b{0%,80%,100%{transform:scale(0);opacity:.2}40%{transform:scale(1);opacity:1}}.t{color:#555;font-size:14px;letter-spacing:2px;font-weight:600}</style></head><body><div class="dots"><div class="d"></div><div class="d"></div><div class="d"></div></div><div class="t">RETRYING CONNECTION</div></body></html>
|
||||||
|
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" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Hysteria2 Server (pinkman_manuals)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
StartLimitIntervalSec=600
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$BIN server --config $CONF
|
||||||
|
WorkingDirectory=$DATA_DIR
|
||||||
|
User=hysteria
|
||||||
|
Group=hysteria
|
||||||
|
Environment=HYSTERIA_LOG_LEVEL=info
|
||||||
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW
|
||||||
|
NoNewPrivileges=true
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
tune_sysctl() {
|
||||||
|
# Буферы UDP побольше — рекомендация разработчиков Hysteria для скорости.
|
||||||
|
printf 'net.core.rmem_max=16777216\nnet.core.wmem_max=16777216\n' >"$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" <<EOF
|
||||||
|
DOMAIN=$DOMAIN
|
||||||
|
EMAIL=$EMAIL
|
||||||
|
PORT=$PORT
|
||||||
|
HOST=$HOST
|
||||||
|
SNI=$SNI
|
||||||
|
UFW=$UFW
|
||||||
|
EOF
|
||||||
|
chmod 600 "$STATE"
|
||||||
|
printf '%s %s\n' "$USERNAME" "$(rand 16)" >"$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 <<EOF
|
||||||
|
${B}hy2${N} — управление Hysteria2
|
||||||
|
|
||||||
|
hy2 add имя добавить пользователя и показать его ссылку
|
||||||
|
hy2 del имя удалить пользователя
|
||||||
|
hy2 list список пользователей
|
||||||
|
hy2 link [имя] ссылка и QR-код
|
||||||
|
hy2 status версия, адрес, состояние сервиса
|
||||||
|
hy2 restart перезапустить (например, после исправления DNS)
|
||||||
|
hy2 update обновить скрипт и Hysteria до проверенной версии
|
||||||
|
hy2 uninstall удалить всё
|
||||||
|
|
||||||
|
Параметры установки (для запуска без вопросов):
|
||||||
|
--domain example.com --email you@example.com сертификат Let's Encrypt
|
||||||
|
--port 443 UDP-порт (по умолчанию 443)
|
||||||
|
--user admin имя первого пользователя
|
||||||
|
--sni www.bing.com SNI для самоподписанного сертификата
|
||||||
|
--host 1.2.3.4 адрес в ссылке, если IP определился неверно
|
||||||
|
--no-ufw не трогать файрвол
|
||||||
|
-y не задавать вопросов
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local cmd=${1:-}
|
||||||
|
case $cmd in
|
||||||
|
add|del|list|link|status|restart|update|uninstall|help) shift; "cmd_$cmd" "$@" ;;
|
||||||
|
__update_binary) cmd_update_binary ;;
|
||||||
|
install) shift; cmd_install "$@" ;;
|
||||||
|
-h|--help) cmd_help ;;
|
||||||
|
""|-*)
|
||||||
|
if [[ -f $STATE && $# -eq 0 ]]; then cmd_help; else cmd_install "$@"; fi ;;
|
||||||
|
*) die "Неизвестная команда: $cmd (см. hy2 help)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user