Установщик 3X-UI: 12 протоколов и единая подписка одной командой

- REALITY, Hysteria2, XHTTP, VLESS/VMess WS, Trojan gRPC, Shadowsocks 2022, TUIC,
  WireGuard, AmneziaWG (классика и 3.1, обфускация генерируется), MTProto
- Все подключения пользователя с общим subId — 3X-UI собирает одну подписку;
  Clash/Mihomo-клиенты получают готовый конфиг, остальные — ссылки
- Свой сертификат: --cert/--key; без Let's Encrypt — самоподписанный, отпечаток в ссылках (pcs)
- --protocols для выбора протоколов, повторный запуск досоздаёт недостающее
- Генератор Mihomo: AmneziaWG 3.1 с правильными именами параметров и version: 3
- Инструкция и матрица обновлены: подписка проверена на Xray, Mihomo и sing-box
This commit is contained in:
itsnotkubrick
2026-09-27 17:19:33 +03:00
parent 78935727be
commit 5958eceefb
9 changed files with 428 additions and 123 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
| | Инструкция | Что получится | Сложность | | | Инструкция | Что получится | Сложность |
|---|---|---|---| |---|---|---|---|
| 🛡 | [3X-UI + REALITY](manuals/3x-ui.md) | Панель и подключение VLESS REALITY одной командой, без доменов | ●○○ | | 🛡 | [3X-UI: все протоколы](manuals/3x-ui.md) | 12 протоколов и единая подписка одной командой, без доменов | ●○○ |
| 🚀 | [Hysteria2](manuals/hysteria2.md) | Быстрый протокол на UDP, друзья добавляются командой `hy2 add` | ●○○ | | 🚀 | [Hysteria2](manuals/hysteria2.md) | Быстрый протокол на UDP, друзья добавляются командой `hy2 add` | ●○○ |
| 📶 | [XKeen на Keenetic](manuals/xkeen-keenetic.md) | Прокси для выбранных устройств прямо на роутере | ●●○ | | 📶 | [XKeen на Keenetic](manuals/xkeen-keenetic.md) | Прокси для выбранных устройств прямо на роутере | ●●○ |
+52 -33
View File
@@ -1,19 +1,43 @@
# 🛡 3X-UI + REALITY # 🛡 3X-UI: все протоколы одной командой
[← Все инструкции](../README.md#инструкции) [← Все инструкции](../README.md#инструкции)
Одна команда ставит панель [3X-UI](https://github.com/MHSanaei/3x-ui) и сразу Одна команда ставит панель [3X-UI](https://github.com/MHSanaei/3x-ui), создаёт подключения
создаёт рабочее подключение VLESS REALITY. Домены не нужны. на 12 протоколах и выдаёт **одну ссылку-подписку**: приложение само получит
подходящий ему формат и нужные протоколы. Домены не нужны.
**Что делает [скрипт](../scripts/3x-ui.sh):** **Что делает [скрипт](../scripts/3x-ui.sh):**
- ставит официальную 3X-UI проверенной версии её собственным установщиком; - ставит официальную 3X-UI проверенной версии её собственным установщиком
- выпускает для панели сертификат Let's Encrypt на IP сервера и сам его продлевает; и ядро Xray, с которым работают все клиенты (Hiddify, Mihomo, v2rayN, Happ);
- прячет панель на случайном порту и случайном пути, со случайными логином и паролем; - выпускает сертификат Let's Encrypt на IP сервера и сам его продлевает;
- подбирает сайт для маскировки REALITY — проверяет, что тот отвечает по TLS 1.3 и HTTP/2; - прячет панель на случайном порту и пути, со случайными логином и паролем;
- создаёт подключение VLESS REALITY на 443 порту и первого клиента; - подбирает сайт для маскировки REALITY — с TLS 1.3 и HTTP/2;
- открывает в ufw только нужные порты; - создаёт подключения на всех протоколах из таблицы ниже, генерирует ключи
- выдаёт ссылку `vless://` и QR-код. и обфускацию AmneziaWG;
- включает единую подписку: Clash Verge и FlClash получают готовый конфиг Mihomo,
остальные приложения — список ссылок;
- открывает в ufw только нужные порты.
## Протоколы
| Протокол | Порт | Работает в |
|---|---|---|
| VLESS REALITY | 443/tcp | всех приложениях |
| Hysteria2 | 443/udp | всех приложениях |
| VLESS XHTTP + REALITY | 8443/tcp | v2rayN, Happ, Clash Verge, FlClash |
| VLESS WebSocket + TLS | 2053/tcp | всех приложениях |
| Trojan gRPC + TLS | 2083/tcp | всех приложениях |
| VMess WebSocket + TLS | 2087/tcp | всех приложениях |
| Shadowsocks 2022 | 8388/tcp+udp | всех приложениях |
| TUIC v5 | 8444/udp | Hiddify, Clash Verge, FlClash, NekoBox |
| WireGuard | 51820/udp | всех приложениях и WireGuard |
| AmneziaWG | 51821/udp | AmneziaWG на роутерах Keenetic, Clash Verge, FlClash |
| AmneziaWG 3.1 | 51822/udp | AmneziaVPN, Clash Verge, FlClash |
| MTProto | 8445/tcp | прокси прямо в Telegram |
Каждый протокол проверен настоящим подключением на ядрах Xray, Mihomo и sing-box —
стенд лежит в [tests/matrix](../tests/matrix/).
## Что понадобится ## Что понадобится
@@ -34,51 +58,46 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/Reality_Hyster
1. **Адрес панели** — откройте его в браузере. 1. **Адрес панели** — откройте его в браузере.
2. **Логин и пароль** от панели. 2. **Логин и пароль** от панели.
3. **Ссылка для подключения** — вставьте её в клиент или отсканируйте QR-код. 3. **Подписка** — вставьте её в приложение или отсканируйте QR-код.
Всё это сохраняется в файл `/root/3x-ui.txt` — посмотреть снова: `cat /root/3x-ui.txt`. Всё это, вместе с отдельными ссылками на каждый протокол, сохраняется в `/root/3x-ui.txt`:
посмотреть снова — `cat /root/3x-ui.txt`.
## Подключение ## Подключение
Ссылку `vless://` поддерживают: В приложении нажмите **+** → **Импорт из буфера** (или «Добавить подписку») и вставьте ссылку:
| Устройство | Приложение | | Устройство | Приложение |
|---|---| |---|---|
| iPhone, iPad | Streisand, Happ, Hiddify | | iPhone, iPad | Happ, Streisand, Hiddify |
| Android | Hiddify, v2rayNG, Happ | | Android | Hiddify, Happ, v2rayNG, FlClash |
| Windows, macOS, Linux | Hiddify, v2rayN | | Windows, macOS, Linux | Hiddify, v2rayN, Clash Verge |
В приложении нажмите **+** → **Импорт из буфера** или отсканируйте QR-код. Для AmneziaVPN и Telegram возьмите отдельные ссылки `vpn://` и `tg://` из `/root/3x-ui.txt`.
## Добавить друга ## Добавить друга
В панели откройте **Клиенты → Добавить клиента**, выберите подключение В панели откройте **Клиенты → Добавить клиента**, отметьте все подключения и задайте
**REALITY**, задайте имя и при желании лимит трафика и срок. Ссылку для одинаковый **Subscription ID** — у друга появится своя подписка со всеми протоколами.
друга покажет кнопка **QR-код** рядом с клиентом.
## Параметры ## Параметры
Команду можно запустить с параметрами — например, если порт 443 уже занят:
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/Reality_Hysteria2/main/scripts/3x-ui.sh) --port 8443
```
| Параметр | Что делает | | Параметр | Что делает |
|---|---| |---|---|
| `--port 8443` | порт REALITY вместо 443 | | `--protocols reality,hy2,ws` | только выбранные протоколы; `minimal` — только REALITY |
| `--sni www.apple.com` | свой сайт для маскировки | | `--port 8443` | порт REALITY (TCP) и Hysteria2 (UDP) вместо 443 |
| `--panel-ssl none` | без сертификата: панель будет доступна только через SSH-туннель | | `--cert fullchain.pem --key privkey.pem --host домен` | свой сертификат, например для домена |
| `--sni www.samsung.com` | свой сайт для маскировки REALITY |
| `--no-ufw` | не трогать файрвол | | `--no-ufw` | не трогать файрвол |
> [!NOTE] > [!NOTE]
> Если порт 80 занят, сертификат для панели не получить — скрипт сам > Если порт 80 занят, сертификат Let's Encrypt не получить. Тогда скрипт сделает свой
> переключится на доступ через SSH-туннель и покажет нужную команду. > сертификат и впишет его отпечаток в ссылки, а панель будет доступна через SSH-туннель.
> Подписка в этом режиме работает только изнутри сервера — используйте отдельные ссылки.
## Обслуживание ## Обслуживание
Управление панелью — официальная команда `x-ui`: перезапуск, логи, Управление панелью — официальная команда `x-ui`: перезапуск, логи, обновление, удаление.
обновление, удаление.
```bash ```bash
x-ui x-ui
+1 -1
View File
@@ -1 +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 &lt;(curl -fsSL https://raw.githubusercontent.com/…/scripts/3x-ui.sh)</text><text x="22" y="91" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==&gt; </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">==&gt; </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">==&gt; </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">==&gt; </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">==&gt; </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&amp;security=reality&amp;pbk=…</text><text x="22" y="364" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">[ здесь QR-код — отсканируйте его телефоном ]</text></svg> <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 &lt;(curl -fsSL https://raw.githubusercontent.com/…/scripts/3x-ui.sh)</text><text x="22" y="91" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==&gt; </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">==&gt; </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">==&gt; </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">==&gt; </text><text x="54.52" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Создаю подключения: REALITY, Hysteria2, XHTTP, WS, TUIC…</text><text x="22" y="175" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">==&gt; </text><text x="54.52" y="175" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">Настраиваю ufw: 443/tcp 443/udp 8443/tcp 2053/tcp …</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 работает: 12 протоколов.</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">https://203.0.113.10:2096/k3v9qx2m7p1a/admin</text><text x="22" y="364" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">[ здесь QR-код — отсканируйте его телефоном ]</text></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

+319 -82
View File
@@ -1,12 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 3X-UI + VLESS REALITY одной командой — https://github.com/itsnotkubrick/Reality_Hysteria2 # 3X-UI со всеми протоколами одной командой — https://github.com/itsnotkubrick/Reality_Hysteria2
# #
# Установка: bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/Reality_Hysteria2/main/scripts/3x-ui.sh) # Установка: bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/Reality_Hysteria2/main/scripts/3x-ui.sh)
# #
# Ставит официальную панель 3X-UI (версия закреплена ниже) её собственным # Ставит официальную панель 3X-UI (версия закреплена ниже) её собственным
# установщиком, получает для панели сертификат Let's Encrypt на IP, создаёт # установщиком, получает сертификат Let's Encrypt на IP, создаёт подключения
# подключение VLESS REALITY на 443 порту, настраивает ufw и выдаёт ссылку # REALITY, XHTTP, VLESS/VMess WS, Trojan gRPC, Shadowsocks 2022, Hysteria2, TUIC,
# vless:// с QR-кодом. Домены не нужны. # WireGuard, AmneziaWG (классика и 3.1) и MTProto, включает единую подписку
# с форматом под каждый клиент и настраивает ufw. Домены не нужны.
# Каждый протокол проверен настоящими клиентами — см. tests/matrix.
set -Eeuo pipefail set -Eeuo pipefail
@@ -22,6 +24,10 @@ XUI_ENV=/etc/x-ui/install-result.env
# Apple, iCloud, Microsoft и домены .ru сам Xray не советует — их тут нет. # Apple, iCloud, Microsoft и домены .ru сам Xray не советует — их тут нет.
SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com) SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)
ALL_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
declare -A PORTS=([xhttp]=8443 [ws]=2053 [trojan]=2083 [vmess]=2087 [ss]=8388 [tuic]=8444 [wg]=51820 [awg]=51821 [awg3]=51822 [mtproto]=8445)
PROTOS=(); CREATED=(); OPEN=()
if [[ -t 1 ]]; then if [[ -t 1 ]]; then
G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m' G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m'
else else
@@ -91,7 +97,7 @@ main() {
die "3X-UI уже установлена другим способом — не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную." die "3X-UI уже установлена другим способом — не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
fi fi
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--port) PORT=$2; shift 2 ;; --port) PORT=$2; shift 2 ;;
@@ -99,6 +105,9 @@ main() {
--panel-ssl) PANEL_SSL=$2; shift 2 ;; --panel-ssl) PANEL_SSL=$2; shift 2 ;;
--host) HOST=$2; shift 2 ;; --host) HOST=$2; shift 2 ;;
--user) NAME=$2; shift 2 ;; --user) NAME=$2; shift 2 ;;
--protocols) protos=$2; shift 2 ;;
--cert) ucert=$2; shift 2 ;;
--key) ukey=$2; shift 2 ;;
--no-ufw) UFW=no; shift ;; --no-ufw) UFW=no; shift ;;
-y|--yes) yes=yes; shift ;; -y|--yes) yes=yes; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
@@ -108,6 +117,18 @@ main() {
[[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT" [[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT"
[[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)." [[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)."
[[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none" [[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none"
if [[ -n $ucert || -n $ukey ]]; then
[[ -s $ucert && -s $ukey ]] || die "Нужны оба файла: --cert fullchain.pem --key privkey.pem"
openssl x509 -in "$ucert" -noout 2>/dev/null || die "$ucert — не сертификат в формате PEM"
PANEL_SSL=custom
fi
case $protos in
all) PROTOS=("${ALL_PROTOS[@]}") ;;
minimal) PROTOS=(reality) ;;
*) IFS=, read -ra PROTOS <<<"$protos"
local x
for x in "${PROTOS[@]}"; do [[ " ${ALL_PROTOS[*]} " == *" $x "* ]] || die "Неизвестный протокол: $x. Доступны: ${ALL_PROTOS[*]}"; done ;;
esac
if port_busy "$PORT" tcp && ! { [[ -f $XUI_ENV ]] && ss -H -ltnp "sport = :$PORT" | grep -q xray; }; then if port_busy "$PORT" tcp && ! { [[ -f $XUI_ENV ]] && ss -H -ltnp "sport = :$PORT" | grep -q xray; }; then
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт — укажите другой: --port 8443" die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт — укажите другой: --port 8443"
fi fi
@@ -121,6 +142,14 @@ main() {
fi fi
fi fi
[[ $PANEL_SSL == ip ]] && port_busy 80 tcp && die "Для сертификата панели нужен свободный порт 80/tcp." [[ $PANEL_SSL == ip ]] && port_busy 80 tcp && die "Для сертификата панели нужен свободный порт 80/tcp."
# Доверенный сертификат (Let's Encrypt или свой) — панель и подписка доступны снаружи по HTTPS.
TRUSTED=no
[[ $PANEL_SSL == ip || $PANEL_SSL == custom ]] && TRUSTED=yes
if [[ $PANEL_SSL == custom ]]; then
mkdir -p /root/cert/custom
install -m 644 "$ucert" /root/cert/custom/fullchain.pem
install -m 600 "$ukey" /root/cert/custom/privkey.pem
fi
say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw" say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
@@ -148,12 +177,12 @@ main() {
panel_user=$(rand_str 10) panel_user=$(rand_str 10)
panel_pass=$(rand_str 20) panel_pass=$(rand_str 20)
if [[ -f $XUI_ENV ]]; then if [[ -f $XUI_ENV ]]; then
say "3X-UI уже стоит после прошлого запуска — продолжаю с создания подключения" say "3X-UI уже стоит после прошлого запуска — продолжаю с создания подключений"
else else
tmp=$(mktemp) tmp=$(mktemp)
say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)" say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)"
curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh" 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" \ if ! XUI_NONINTERACTIVE=1 XUI_SSL_MODE="${PANEL_SSL/custom/none}" XUI_SERVER_IP="$HOST" \
XUI_PANEL_PORT="$panel_port" XUI_WEB_BASE_PATH="$panel_path" \ XUI_PANEL_PORT="$panel_port" XUI_WEB_BASE_PATH="$panel_path" \
XUI_USERNAME="$panel_user" XUI_PASSWORD="$panel_pass" \ XUI_USERNAME="$panel_user" XUI_PASSWORD="$panel_pass" \
bash "$tmp" "$XUI_VERSION" </dev/null >/var/log/3x-ui-install.log 2>&1; then bash "$tmp" "$XUI_VERSION" </dev/null >/var/log/3x-ui-install.log 2>&1; then
@@ -173,6 +202,14 @@ main() {
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api" API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
wait_panel wait_panel
if [[ $PANEL_SSL == custom ]]; then
say "Подключаю ваш сертификат к панели"
/usr/local/x-ui/x-ui cert -webCert /root/cert/custom/fullchain.pem -webCertKey /root/cert/custom/privkey.pem >/dev/null 2>&1
systemctl restart x-ui
API="https://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
wait_panel
fi
# Без сертификата панель и подписки не должны торчать наружу по HTTP. # Без сертификата панель и подписки не должны торчать наружу по HTTP.
if [[ $PANEL_SSL == none ]]; then if [[ $PANEL_SSL == none ]]; then
say "Панель без сертификата — открываю её только для SSH-туннеля (127.0.0.1)" say "Панель без сертификата — открываю её только для SSH-туннеля (127.0.0.1)"
@@ -197,114 +234,314 @@ main() {
[[ $cur_core == "$XRAY_CORE" ]] || warn "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться." [[ $cur_core == "$XRAY_CORE" ]] || warn "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться."
fi fi
# --- подключение VLESS REALITY --- # --- сертификат для протоколов с TLS ---
local keys priv pub uuid sid sub settings stream sniffing body existing setup_tls_cert
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" '{ # --- подключения: все выбранные протоколы, один subId на пользователя ---
clients: [{id: $id, flow: "xtls-rprx-vision", email: $email, limitIp: 0, totalGB: 0, SUBID=$(tr 'A-Z' 'a-z' <<<"$NAME")
expiryTime: 0, enable: true, tgId: 0, subId: $sub, reset: 0}], EXISTING=$(api GET inbounds/list)
decryption: "none", fallbacks: []}') local p
stream=$(jq -nc --arg sni "$SNI" --arg priv "$priv" --arg pub "$pub" --arg sid "$sid" '{ for p in "${PROTOS[@]}"; do "proto_$p"; done
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 # --- подписка: ссылки, Clash/Mihomo и JSON с автоопределением клиента ---
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" setup_subscription
# Ждём, пока 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 if [[ $UFW == yes ]]; then
local ssh_port local ssh_port
ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}') ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}')
ssh_port=${ssh_port:-22} ssh_port=${ssh_port:-22}
local extra="" OPEN+=("$ssh_port/tcp")
[[ $PANEL_SSL == ip ]] && extra=", панель $XUI_PANEL_PORT и 80 для продления сертификата" [[ $TRUSTED == yes ]] && OPEN+=("$XUI_PANEL_PORT/tcp" "$SUB_PORT/tcp")
say "Настраиваю ufw: SSH $ssh_port, REALITY $PORT$extra" [[ $PANEL_SSL == ip ]] && OPEN+=("80/tcp")
ufw allow "$ssh_port/tcp" >/dev/null say "Настраиваю ufw: ${OPEN[*]}"
ufw allow "$PORT/tcp" >/dev/null local o
if [[ $PANEL_SSL == ip ]]; then for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
ufw allow "$XUI_PANEL_PORT/tcp" >/dev/null
ufw allow 80/tcp >/dev/null
fi
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную." ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную."
fi fi
# --- итог --- # --- итог ---
local panel_url local panel_url links
if [[ $PANEL_SSL == ip ]]; then if [[ $TRUSTED == yes ]]; then
panel_url="https://$HOST:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH" panel_url="https://$HOST:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"
else 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)" 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 fi
links=$(sub_links)
umask 077 umask 077
cat >"$RESULT" <<EOF {
3X-UI $XUI_VERSION — данные для входа (файл виден только root) echo "3X-UI $XUI_VERSION — данные для входа (файл виден только root)"
echo
Панель: $panel_url echo "Панель: $panel_url"
Логин: $XUI_USERNAME echo "Логин: $XUI_USERNAME"
Пароль: $XUI_PASSWORD echo "Пароль: $XUI_PASSWORD"
echo
Подключение REALITY ($NAME): [[ $TRUSTED == yes ]] && { echo "Подписка ($NAME) — все протоколы одной ссылкой:"; echo "$SUB_URL"; echo; }
$link echo "Отдельные подключения ($NAME):"
EOF echo "$links"
} >"$RESULT"
echo echo
echo "${G}${B}Готово! 3X-UI и REALITY работают.${N}" echo "${G}${B}Готово! 3X-UI работает: ${#CREATED[@]} протоколов.${N}"
echo "${D}${CREATED[*]}${N}"
echo echo
echo "Панель: ${B}$panel_url${N}" echo "Панель: ${B}$panel_url${N}"
echo "Логин: ${B}$XUI_USERNAME${N}" echo "Логин: ${B}$XUI_USERNAME${N}"
echo "Пароль: ${B}$XUI_PASSWORD${N}" echo "Пароль: ${B}$XUI_PASSWORD${N}"
echo echo
echo "Ссылка для ${B}$NAME${N} — вставьте в Hiddify, v2rayN, Streisand или Happ:" if [[ $TRUSTED == yes ]]; then
echo "Подписка для ${B}$NAME${N} — все протоколы одной ссылкой. Вставьте её в Hiddify, v2rayN, Happ,"
echo "Clash Verge или FlClash: приложение само получит подходящий формат."
echo
echo "$SUB_URL"
echo
qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true
else
echo "Без сертификата подписка недоступна снаружи — вот ссылки по одной:"
echo
echo "$links"
fi
echo echo
echo "$link" if [[ -n $PIN && " ${CREATED[*]} " == *" TUIC "* ]]; then
echo warn "TUIC со своим сертификатом: в клиенте включите «Разрешить небезопасный» (allow insecure) — отпечаток TUIC-ссылки не передают."
qrencode -t ANSIUTF8 -m 1 "$link" || true fi
echo echo "Всё это сохранено в ${B}$RESULT${N}. Друзей добавляйте в панели: Клиенты → Добавить клиента,"
echo "Всё это сохранено в ${B}$RESULT${N}. Друзей добавляйте в панели:" echo "отметьте все подключения и задайте одинаковый Subscription ID — у друга будет своя подписка."
echo "Клиенты → Добавить клиента → подключение REALITY." }
# ---------- сертификат ----------
setup_tls_cert() {
PIN=""
if [[ $PANEL_SSL == custom ]]; then
CERT=/root/cert/custom/fullchain.pem; KEY=/root/cert/custom/privkey.pem
elif [[ $PANEL_SSL == ip && -s /root/cert/ip/fullchain.pem ]]; then
CERT=/root/cert/ip/fullchain.pem; KEY=/root/cert/ip/privkey.pem
else
# Без Let's Encrypt — свой сертификат, а его отпечаток уходит в ссылки (pcs),
# чтобы клиенты доверяли именно ему.
CERT=/root/cert/self/fullchain.pem; KEY=/root/cert/self/privkey.pem
if [[ ! -s $CERT ]]; then
mkdir -p /root/cert/self
local san="DNS:$HOST"
[[ $HOST =~ ^[0-9.]+$ ]] && san="IP:$HOST"
openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout "$KEY" -out "$CERT" \
-subj "/CN=$HOST" -addext "subjectAltName=$san" -days 3650 2>/dev/null
chmod 600 "$KEY"
fi
PIN=$(openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')
fi
}
tls_json() { # alpn(JSON-массив)
jq -nc --arg sni "$HOST" --arg c "$CERT" --arg k "$KEY" --arg pin "$PIN" --argjson alpn "$1" '{
serverName: $sni, alpn: $alpn, certificates: [{certificateFile: $c, keyFile: $k}],
settings: ({fingerprint: "chrome"} + (if $pin != "" then {pinnedPeerCertSha256: [$pin]} else {} end))}'
}
# ---------- протоколы ----------
client_base() { # суффикс
jq -nc --arg e "$NAME-$1" --arg s "$SUBID" '{email: $e, limitIp: 0, totalGB: 0, expiryTime: 0, enable: true, tgId: 0, subId: $s, comment: "", reset: 0}'
}
uuid() { cat /proc/sys/kernel/random/uuid; }
rnd() { shuf -i "$1-$2" -n 1; }
# add_inbound remark port proto(tcp|udp|both) protocol settings stream
add_inbound() {
local remark=$1 port=$2 net=$3 protocol=$4 settings=$5 stream=$6 body
if jq -e --arg r "$remark" 'any(.[]; .remark == $r)' <<<"$EXISTING" >/dev/null; then
CREATED+=("$remark"); open_port "$port" "$net"; return
fi
local n
for n in ${net/both/tcp udp}; do
if port_busy "$port" "$n"; then warn "$remark пропущен: порт $port/$n занят"; return; fi
done
body=$(jq -nc --arg rm "$remark" --argjson port "$port" --arg p "$protocol" --arg s "$settings" --arg st "$stream" '{
remark: $rm, enable: true, listen: "", port: $port, protocol: $p, settings: $s, streamSettings: $st,
sniffing: "{\"enabled\":true,\"destOverride\":[\"http\",\"tls\",\"quic\"],\"metadataOnly\":false,\"routeOnly\":false}",
expiryTime: 0, total: 0}')
if api POST inbounds/add "$body" >/dev/null; then
CREATED+=("$remark"); open_port "$port" "$net"
fi
}
open_port() { # port net
case $2 in
tcp|udp) OPEN+=("$1/$2") ;;
both) OPEN+=("$1/tcp" "$1/udp") ;;
esac
}
proto_reality() {
local keys stream settings
keys=$(api GET server/getNewX25519Cert)
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base reality)" '{clients: [$c + {id: $id, flow: "xtls-rprx-vision"}], decryption: "none", fallbacks: []}')
stream=$(jq -nc --arg sni "$SNI" --argjson k "$keys" --arg sid "$(openssl rand -hex 8)" '{
network: "tcp", security: "reality", externalProxy: [],
realitySettings: {show: false, xver: 0, target: ($sni + ":443"), serverNames: [$sni], privateKey: $k.privateKey,
minClientVer: "", maxClientVer: "", maxTimediff: 0, shortIds: [$sid],
settings: {publicKey: $k.publicKey, fingerprint: "chrome", serverName: "", spiderX: "/"}},
tcpSettings: {acceptProxyProtocol: false, header: {type: "none"}}}')
add_inbound "REALITY" "$PORT" tcp vless "$settings" "$stream"
}
proto_xhttp() {
local keys stream settings
keys=$(api GET server/getNewX25519Cert)
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base xhttp)" '{clients: [$c + {id: $id, flow: ""}], decryption: "none"}')
stream=$(jq -nc --arg sni "$SNI" --argjson k "$keys" --arg sid "$(openssl rand -hex 8)" --arg path "/$(rand_str 10 | tr 'A-Z' 'a-z')" '{
network: "xhttp", security: "reality", xhttpSettings: {path: $path, mode: "auto"},
realitySettings: {target: ($sni + ":443"), serverNames: [$sni], privateKey: $k.privateKey, shortIds: [$sid],
settings: {publicKey: $k.publicKey, fingerprint: "chrome", spiderX: "/"}}}')
add_inbound "XHTTP" "${PORTS[xhttp]}" tcp vless "$settings" "$stream"
}
proto_ws() {
local settings stream
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base ws)" '{clients: [$c + {id: $id, flow: ""}], decryption: "none"}')
stream=$(jq -nc --argjson t "$(tls_json '["http/1.1"]')" --arg path "/$(rand_str 10 | tr 'A-Z' 'a-z')" '{network: "ws", security: "tls", wsSettings: {path: $path}, tlsSettings: $t}')
add_inbound "VLESS-WS" "${PORTS[ws]}" tcp vless "$settings" "$stream"
}
proto_trojan() {
local settings stream
settings=$(jq -nc --arg pw "$(rand_str 16)" --argjson c "$(client_base trojan)" '{clients: [$c + {password: $pw}]}')
stream=$(jq -nc --argjson t "$(tls_json '["h2"]')" --arg sn "$(rand_str 8 | tr 'A-Z' 'a-z')" '{network: "grpc", security: "tls", grpcSettings: {serviceName: $sn}, tlsSettings: $t}')
add_inbound "Trojan-gRPC" "${PORTS[trojan]}" tcp trojan "$settings" "$stream"
}
proto_vmess() {
local settings stream
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base vmess)" '{clients: [$c + {id: $id, security: "auto", alterId: 0}]}')
stream=$(jq -nc --argjson t "$(tls_json '["http/1.1"]')" --arg path "/$(rand_str 10 | tr 'A-Z' 'a-z')" '{network: "ws", security: "tls", wsSettings: {path: $path}, tlsSettings: $t}')
add_inbound "VMess-WS" "${PORTS[vmess]}" tcp vmess "$settings" "$stream"
}
proto_ss() {
local settings
settings=$(jq -nc --arg pw "$(openssl rand -base64 16)" --arg upw "$(openssl rand -base64 16)" --argjson c "$(client_base ss)" '{
method: "2022-blake3-aes-128-gcm", password: $pw, network: "tcp,udp", clients: [$c + {method: "", password: $upw}]}')
add_inbound "Shadowsocks" "${PORTS[ss]}" both shadowsocks "$settings" '{"network":"tcp","security":"none"}'
}
proto_hy2() {
local settings stream
settings=$(jq -nc --arg a "$(rand_str 16)" --argjson c "$(client_base hy2)" '{version: 2, clients: [$c + {auth: $a}]}')
stream=$(jq -nc --argjson t "$(tls_json '["h3"]')" '{network: "hysteria", hysteriaSettings: {version: 2}, security: "tls", tlsSettings: $t}')
add_inbound "Hysteria2" "$PORT" udp hysteria "$settings" "$stream"
}
proto_tuic() {
local settings id
id=$(uuid)
settings=$(jq -nc --arg id "$id" --arg pw "$(rand_str 16)" --arg c "$CERT" --arg k "$KEY" --argjson cb "$(client_base tuic)" '{
server: {certificate: $c, private_key: $k, congestion_control: "bbr", alpn: ["h3"], udp_relay_mode: "native",
zero_rtt_handshake: false, log_level: "warn", sni: ""},
clients: [$cb + {uuid: $id, id: $id, password: $pw}]}')
add_inbound "TUIC" "${PORTS[tuic]}" udp tuic "$settings" '{}'
}
wg_pair() { /usr/local/x-ui/bin/xray-linux-* wg; }
proto_wg() {
local srv cli settings
srv=$(wg_pair); cli=$(wg_pair)
settings=$(jq -nc --arg sk "$(awk '/Private/ {print $NF}' <<<"$srv")" \
--arg pr "$(awk '/Private/ {print $NF}' <<<"$cli")" --arg pu "$(awk '/Public|Password/ {print $NF}' <<<"$cli" | head -1)" \
--argjson c "$(client_base wg)" '{mtu: 1420, secretKey: $sk, peers: [], subnetIp: "10.0.0.0", subnetCidr: 24,
clients: [$c + {privateKey: $pr, publicKey: $pu, allowedIPs: ["10.0.0.2/32"]}]}')
add_inbound "WireGuard" "${PORTS[wg]}" udp wireguard "$settings" '{"network":"tcp","security":"none"}'
}
# AmneziaWG: классические параметры понимают Mihomo и роутеры Keenetic,
# полный набор 3.1 — только приложения AmneziaVPN/AmneziaWG. Поэтому два подключения.
awg_obfuscation() { # classic|full
local jmin s1 s2
jmin=$(rnd 40 89); s1=$(rnd 15 150); s2=$(rnd 15 150)
while ((s1 + 56 == s2)); do s2=$(rnd 15 150); done
local o
o=$(jq -nc --argjson jc "$(rnd 3 6)" --argjson jmin "$jmin" --argjson jmax "$((jmin + $(rnd 50 250)))" --argjson s1 "$s1" --argjson s2 "$s2" \
--arg h1 "$(rnd 5 536870911)" --arg h2 "$(rnd 536870912 1073741823)" --arg h3 "$(rnd 1073741824 1610612735)" --arg h4 "$(rnd 1610612736 2147483647)" \
'{jc: $jc, jmin: $jmin, jmax: $jmax, s1: $s1, s2: $s2, h1: $h1, h2: $h2, h3: $h3, h4: $h4}')
if [[ $1 == full ]]; then
local cp rk rj rt ka ha
cp=$(rnd 8 24); rk=$(rnd 100 120); rj=$((rk + $(rnd 10 40) + $(rnd 30 60))); rt=$(rnd 3 6); ka=$(rnd 8 12); ha=$(rnd 15 25)
o=$(jq -c --argjson s3 "$(rnd 12 55)" --argjson s4 "$(rnd 12 27)" --arg i1 "<r $(rnd 32 256)>" --arg hp "$(openssl rand -base64 32)" \
--arg cp "$cp-$((cp + $(rnd 8 40)))" --arg rk "$rk-$((rk + $(rnd 10 40)))" --arg rj "$rj-$((rj + $(rnd 30 90)))" \
--arg rt "$rt-$((rt + $(rnd 1 4)))" --arg ka "$ka-$((ka + $(rnd 2 8)))" --arg ha "$ha-$((ha + $(rnd 5 25)))" \
'. + {s3: $s3, s4: $s4, i1: $i1, headerProtectionKey: $hp, contentPaddingAddition: $cp, rekeyAfterTime: $rk,
rejectAfterTime: $rj, rekeyTimeout: $rt, keepaliveTimeout: $ka, maxHandshakeAttempts: $ha}' <<<"$o")
fi
echo "$o"
}
awg_inbound() { # remark port subnet client-ip suffix mode
local srv cli settings
srv=$(wg_pair); cli=$(wg_pair)
settings=$(jq -nc --arg pk "$(awk '/Private/ {print $NF}' <<<"$srv")" --arg pub "$(awk '/Public|Password/ {print $NF}' <<<"$srv" | head -1)" \
--arg pr "$(awk '/Private/ {print $NF}' <<<"$cli")" --arg pu "$(awk '/Public|Password/ {print $NF}' <<<"$cli" | head -1)" \
--arg net "$3" --arg ip "$4" --argjson o "$(awg_obfuscation "$6")" --argjson c "$(client_base "$5")" '{
server: ({privateKey: $pk, publicKey: $pub, subnetIp: $net, subnetCidr: 24, primaryDns: "1.1.1.1", secondaryDns: "8.8.8.8"} + $o),
clients: [$c + {privateKey: $pr, publicKey: $pu, allowedIPs: [$ip]}]}')
add_inbound "$1" "$2" udp amneziawg "$settings" '{}'
}
proto_awg() { awg_inbound "AmneziaWG" "${PORTS[awg]}" 10.8.1.0 10.8.1.2/32 awg classic; }
proto_awg3() { awg_inbound "AmneziaWG-3.1" "${PORTS[awg3]}" 10.8.2.0 10.8.2.2/32 awg3 full; }
proto_mtproto() {
local settings
settings=$(jq -nc --argjson c "$(client_base mtproto)" '{fakeTlsDomain: "www.cloudflare.com", clients: [$c + {secret: ""}]}')
add_inbound "MTProto" "${PORTS[mtproto]}" tcp mtproto "$settings" '{}'
}
# ---------- подписка ----------
setup_subscription() {
local all
all=$(api POST setting/all '{}')
SUB_PORT=$(jq -r '.subPort // 2096' <<<"$all")
SUB_PATH=$(jq -r '.subPath // "/sub/"' <<<"$all")
if [[ $SUB_PATH == /sub/ || -z $SUB_PATH ]]; then SUB_PATH="/$(rand_str 12 | tr 'A-Z' 'a-z')/"; fi
local upd
upd=$(jq -c --arg path "$SUB_PATH" --arg c "$CERT" --arg k "$KEY" --arg ssl "$TRUSTED" --arg title "3X-UI + Hysteria2 Kit" '
.subEnable = true | .subPath = $path | .subTitle = $title
| .subClashEnable = true | .subClashAutoDetect = true | .subJsonEnable = true | .subJsonAutoDetect = true
| if $ssl == "yes" then .subCertFile = $c | .subKeyFile = $k | .subListen = "" else . end' <<<"$all")
if [[ $upd != "$all" ]]; then
api POST setting/update "$upd" >/dev/null
systemctl restart x-ui
wait_panel
fi
if [[ $TRUSTED == yes ]]; then SUB_URL="https://$HOST:$SUB_PORT$SUB_PATH$SUBID"; else SUB_URL="http://127.0.0.1:$SUB_PORT$SUB_PATH$SUBID"; fi
SUB_FETCH="$(if [[ $TRUSTED == yes ]]; then echo https; else echo http; fi)://$HOST:$SUB_PORT$SUB_PATH$SUBID"
}
# Ссылки пользователя — из его же подписки (её собирает сама 3X-UI).
sub_links() {
local raw i
for i in $(seq 1 20); do
# Запрос идёт на этот же сервер, но с настоящим адресом в Host — 3X-UI подставит его в ссылки.
raw=$(curl -fsSk -m 10 -A "v2rayN/7.0" --connect-to "$HOST:$SUB_PORT:127.0.0.1:$SUB_PORT" \
"$SUB_FETCH" 2>/dev/null) && [[ -n $raw ]] && break
sleep 2
done
if grep -q '://' <<<"$raw"; then echo "$raw"; else base64 -d <<<"$raw" 2>/dev/null; fi
} }
usage() { usage() {
cat <<EOF cat <<EOF
3X-UI + VLESS REALITY одной командой 3X-UI со всеми протоколами одной командой
--port 443 порт REALITY (по умолчанию 443) --protocols all all (по умолчанию), minimal (только REALITY) или список через запятую:
reality,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto
--port 443 порт REALITY (TCP) и Hysteria2 (UDP), по умолчанию 443
--sni сайт сайт для маскировки (по умолчанию подбирается сам) --sni сайт сайт для маскировки (по умолчанию подбирается сам)
--panel-ssl ip|none сертификат панели: ip — Let's Encrypt на IP (нужен порт 80), --panel-ssl ip|none сертификат панели: ip — Let's Encrypt на IP (нужен порт 80),
none — панель только через SSH-туннель (по умолчанию выбирается сам) none — панель только через SSH-туннель (по умолчанию выбирается сам)
--cert файл --key файл свой сертификат (например, для домена) вместо Let's Encrypt на IP;
тогда --host — это домен из сертификата
--user admin имя первого клиента --user admin имя первого клиента
--host 1.2.3.4 адрес в ссылке, если IP определился неверно --host 1.2.3.4 адрес в ссылке, если IP определился неверно
--no-ufw не трогать файрвол --no-ufw не трогать файрвол
+26 -1
View File
@@ -11,4 +11,29 @@ Xray, Mihomo, sing-box и официальным клиентом AmneziaWG. П
4. Прогон в клиентском контейнере с бинарниками в `/cl`: `bash matrix-run.sh`. 4. Прогон в клиентском контейнере с бинарниками в `/cl`: `bash matrix-run.sh`.
5. AmneziaWG: `awg-set.sh classic|mid|full` меняет обфускацию на сервере. 5. AmneziaWG: `awg-set.sh classic|mid|full` меняет обфускацию на сервере.
Результат на 2026-09-27 (3X-UI 3.8.5, Xray 26.6.27, Mihomo 1.19.31, sing-box 1.14.2) — в описании коммита. Без `PIN` (сервер с доверенным сертификатом) клиенты проверяют сертификат как обычно.
## Результат 2026-09-27
3X-UI 3.8.5, Xray 26.6.27, Mihomo 1.19.31, sing-box 1.14.2, amneziawg-tools 3.1.
Сервер поставлен `scripts/3x-ui.sh` с доверенным сертификатом, ссылки взяты из его подписки.
| Протокол | Xray | Mihomo | sing-box |
|---|---|---|---|
| VLESS REALITY | ✓ | ✓ | ✓ |
| Hysteria2 | ✓ | ✓ | ✓ |
| VLESS XHTTP + REALITY | ✓ | ✓ | — |
| VLESS WS + TLS | ✓ | ✓ | ✓ |
| Trojan gRPC + TLS | ✓ | ✓ | ✓ |
| VMess WS + TLS | ✓ | ✓ | ✓ |
| Shadowsocks 2022 | ✓ | ✓ | ✓ |
| TUIC v5 | — | ✓ | ✓ |
| WireGuard | ✓ | ✓ | ✓ |
| AmneziaWG (классика) | — | ✓ | — |
| AmneziaWG 3.1 | — | ✓ | — |
Подписка в формате Clash (User-Agent Clash Verge) — все 11 прокси ✓ в Mihomo.
Официальный клиент AmneziaWG 3.1 подключается к AmneziaWG 3.1.
MTProto: `mtg doctor` — все дата-центры Telegram доступны.
Со своим (самоподписанным) сертификатом подписка Clash от 3X-UI не передаёт Mihomo отпечаток —
TLS-протоколы в ней не работают; ссылки с `pcs` работают.
+3 -3
View File
@@ -8,9 +8,9 @@ fs.rmSync(out, { recursive: true, force: true });
const rows = []; const rows = [];
proxies.filter((p) => p.type !== 'mtproto').forEach((p, i) => { proxies.filter((p) => p.type !== 'mtproto').forEach((p, i) => {
// Тестовый сервер с самоподписанным сертификатом — разрешаем клиентам его принять. // Тестовый сервер с самоподписанным сертификатом — разрешаем клиентам его принять.
if (p.tls && p.tls.security === 'tls') { p.tls.insecure = true; p.tls.pin = PIN; } if (PIN && p.tls && p.tls.security === "tls") { p.tls.insecure = true; p.tls.pin = PIN; }
if (p.type === 'hysteria2' || p.type === 'tuic') p.insecure = true; if (PIN && (p.type === "hysteria2" || p.type === "tuic")) p.insecure = true;
if (p.type === 'hysteria2') p.pinSHA256 = PIN; if (PIN && p.type === "hysteria2") p.pinSHA256 = PIN;
const id = String(i).padStart(2, '0') + '-' + p.name.replace(/[^a-z0-9-]/gi, '').slice(0, 24); const id = String(i).padStart(2, '0') + '-' + p.name.replace(/[^a-z0-9-]/gi, '').slice(0, 24);
const dir = path.join(out, id); const dir = path.join(out, id);
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
+2 -1
View File
@@ -245,7 +245,8 @@
if (!i.privatekey || !p.publickey || !p.endpoint) throw new Error('конфиг WireGuard неполный'); if (!i.privatekey || !p.publickey || !p.endpoint) throw new Error('конфиг WireGuard неполный');
const ep = /^\[?([^\]]+?)\]?:(\d+)$/.exec(p.endpoint); const ep = /^\[?([^\]]+?)\]?:(\d+)$/.exec(p.endpoint);
if (!ep) throw new Error('в конфиге WireGuard неверный Endpoint'); if (!ep) throw new Error('в конфиге WireGuard неверный Endpoint');
const awgKeys = ['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4', 'i1', 'i2', 'i3', 'i4', 'i5']; const awgKeys = ['jc', 'jmin', 'jmax', 's1', 's2', 's3', 's4', 'h1', 'h2', 'h3', 'h4', 'i1', 'i2', 'i3', 'i4', 'i5',
'headerprotectionkey', 'contentpaddingaddition', 'rekeyaftertime', 'rejectaftertime', 'rekeytimeout', 'keepalivetimeout', 'maxhandshakeattempts'];
const awg = {}; const awg = {};
awgKeys.forEach((k) => { if (i[k] !== undefined && i[k] !== '') awg[k] = /^\d+$/.test(i[k]) ? Number(i[k]) : i[k]; }); awgKeys.forEach((k) => { if (i[k] !== undefined && i[k] !== '') awg[k] = /^\d+$/.test(i[k]) ? Number(i[k]) : i[k]; });
const isAwg = Object.keys(awg).length > 0; const isAwg = Object.keys(awg).length > 0;
+14 -1
View File
@@ -141,7 +141,20 @@
o['private-key'] = p.privateKey; o['public-key'] = p.publicKey; o['private-key'] = p.privateKey; o['public-key'] = p.publicKey;
o['pre-shared-key'] = p.preSharedKey || undefined; o.mtu = p.mtu; o['pre-shared-key'] = p.preSharedKey || undefined; o.mtu = p.mtu;
o.reserved = p.reserved || undefined; o.reserved = p.reserved || undefined;
if (p.awg) o['amnezia-wg-option'] = p.awg; // Параметры AmneziaWG в именах Mihomo. Для 3.1 (есть ключ защиты заголовков)
// обязателен version: 3 — без него Mihomo не подключается к такому серверу.
if (p.awg) {
const names = { headerprotectionkey: 'header-protection-key', contentpaddingaddition: 'content-padding-addition',
rekeyaftertime: 'rekey-after-time', rejectaftertime: 'reject-after-time', rekeytimeout: 'rekey-timeout',
keepalivetimeout: 'keepalive-timeout', maxhandshakeattempts: 'max-handshake-attempts' };
const c = {};
Object.keys(p.awg).forEach((k) => {
const v = p.awg[k];
c[names[k] || k] = /^h[1-4]$/.test(k) ? String(v) : v;
});
if (c['header-protection-key']) c.version = 3;
o['amnezia-wg-option'] = c;
}
break; break;
case 'hysteria2': case 'hysteria2':
o.password = p.password; o.sni = p.sni; o.alpn = p.alpn; o.password = p.password; o.sni = p.sni; o.alpn = p.alpn;
+10
View File
@@ -119,6 +119,16 @@ test('Xray 26: вместо allowInsecure — отпечаток или пред
assert.equal(pinned.warnings.length, 0); assert.equal(pinned.warnings.length, 0);
}); });
test('Mihomo: AmneziaWG 3.1 — имена параметров Mihomo и version: 3', () => {
const conf = '[Interface]\nPrivateKey = cHJpdg==\nAddress = 10.8.2.2/32\nJc = 3\nJmin = 49\nJmax = 139\nS1 = 141\nS2 = 92\nS3 = 22\nS4 = 18\nH1 = 5\nH2 = 6\nH3 = 7\nH4 = 8\nI1 = <r 89>\nHeaderProtectionKey = a2V5\nRekeyTimeout = 5-6\n[Peer]\nPublicKey = cHVi\nEndpoint = a.example.com:51822\n';
const r = buildMihomo(parseText('vpn://' + Buffer.from(conf).toString('base64')).proxies, { secret: 'x' });
const o = r.config.proxies[0]['amnezia-wg-option'];
assert.equal(o['header-protection-key'], 'a2V5');
assert.equal(o['rekey-timeout'], '5-6');
assert.equal(o.version, 3);
assert.equal(o.h1, '5');
});
test('Mihomo: порты XKeen и прокси', () => { test('Mihomo: порты XKeen и прокси', () => {
const r = buildMihomo(parseText([REALITY, HY2, SS_2022].join('\n')).proxies, { secret: 'x' }); const r = buildMihomo(parseText([REALITY, HY2, SS_2022].join('\n')).proxies, { secret: 'x' });
assert.equal(r.config['redir-port'], 5000); assert.equal(r.config['redir-port'], 5000);