1.1: подписанное автообновление, kit backup и --restore, случайная заглушка
- kit update ставит только релизы, у которых SHA256SUMS подписан ключом автора (ssh-keygen -Y verify), версия внутри подписи совпадает, файлы сошлись с хешами; откат на старую версию запрещён; после обновления проверяется подписка, иначе возврат - автообновление: systemd-таймер раз в сутки ночью со случайной задержкой, kit update --manual / --auto, журнал /var/log/kit-update.log - hy2 update тоже проверяет подпись - tools/release.sh: проверка версий и хеша установщика 3X-UI, SHA256SUMS и подпись - kit backup и 3x-ui.sh --restore: архив 600, строгая проверка архива, конфиги nginx и kit-sub собираются заново, смена IP в подключениях - сайт-заглушка: случайный из шести шаблонов внутри скрипта - zashboard закреплён на v3.29.1, короткие тире и 192.168.x.x в текстах и картинках - CI: ключ подписи одинаковый в kit и hy2, проверка подписи релиза - CHANGELOG, README, инструкция, SECURITY.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7GrBSRPVXhrz3RAprWDnt
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: ShellCheck
|
||||
run: shellcheck -S warning -e SC1090,SC2034,SC2211 scripts/*.sh
|
||||
run: shellcheck -S warning -e SC1090,SC2034,SC2211 scripts/*.sh tools/release.sh
|
||||
- name: Python
|
||||
run: python3 -m py_compile scripts/kit-sub.py
|
||||
- name: Генераторы
|
||||
@@ -24,3 +24,14 @@ jobs:
|
||||
grep -q "^KIT_VERSION=\"$v\"" scripts/3x-ui.sh
|
||||
grep -q "^KIT_VERSION=\"$v\"" scripts/hysteria2.sh
|
||||
grep -q "^KIT_VERSION=\"$v\"" scripts/kit.sh
|
||||
- name: Ключ подписи одинаковый в kit и hy2
|
||||
run: |
|
||||
signers() { awk '/^KIT_SIGNERS=\(/ {on = 1; next} on && /^\)/ {exit} on' "$1"; }
|
||||
diff <(signers scripts/kit.sh) <(signers scripts/hysteria2.sh)
|
||||
- name: Подпись релиза совпадает с файлами
|
||||
run: |
|
||||
[ -f SHA256SUMS ] || { echo "SHA256SUMS нет – релиз ещё не подписан"; exit 0; }
|
||||
grep -qx "# 3X-UI KIT $(cat VERSION)" SHA256SUMS
|
||||
grep -v '^#' SHA256SUMS | sha256sum -c -
|
||||
awk '/^KIT_SIGNERS=\(/ {on = 1; next} on && /^\)/ {exit} on && /^ *"/ {gsub(/^ *"|"$/, ""); print "releases@3x-ui-kit namespaces=\"3x-ui-kit-release\" " $0}' scripts/kit.sh >allowed_signers
|
||||
ssh-keygen -Y verify -f allowed_signers -I releases@3x-ui-kit -n 3x-ui-kit-release -s SHA256SUMS.sig <SHA256SUMS
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
- **Меню x-ui оставили в покое.** Больше не переписываем чужие файлы по расписанию, только
|
||||
один раз при установке.
|
||||
- **Строгие имена.** Параметры `--sni` и `--host` у установщика 3X-UI тоже проверяются.
|
||||
- **Веб-панель Mihomo на роутере закреплена.** Генератор больше не ставит на роутер «последнюю»
|
||||
версию zashboard, а только ту, которую мы видели в лицо (v3.29.1).
|
||||
|
||||
**Для тех, кто хочет проверить сам**
|
||||
|
||||
@@ -31,20 +33,41 @@
|
||||
- `SECURITY.md`: как сообщить об уязвимости, не публикуя её на весь интернет.
|
||||
- Проверка кода на каждый коммит: ShellCheck, Python, тесты генераторов.
|
||||
|
||||
**Новая команда: `kit update`**
|
||||
**Обновления приходят сами, но только с нашей печатью**
|
||||
|
||||
- Обновляет kit и подписку до новой версии, а пользователи, ссылки и подписки остаются прежними.
|
||||
Раньше, чтобы получить исправление, приходилось сносить всё и раздавать ссылки заново.
|
||||
- Раньше, чтобы получить исправление, приходилось сносить всё и раздавать ссылки заново.
|
||||
«Мам, удали старый VPN, вот новый» больше не повторится.
|
||||
- Если новая подписка вдруг не запустится, `kit update` молча вернёт старую. Ну, не молча,
|
||||
предупредит.
|
||||
- Раз в сутки ночью сервер сам проверяет, не вышла ли новая версия, и обновляет kit и подписку.
|
||||
Пользователи, ссылки и подписки остаются прежними. Панель 3X-UI и Xray не трогаем.
|
||||
- Каждый релиз подписан ключом автора, а открытая часть ключа уже лежит на вашем сервере.
|
||||
Подпись не сошлась? Сервер вежливо отказывается. Даже если взломают GitHub, чужое обновление
|
||||
не встанет: для этого нужен ещё и ключ, а он не в облаке, а у автора.
|
||||
- Откатить сервер на старую версию под видом новой тоже не выйдет: номер версии внутри подписи.
|
||||
- После обновления сервер проверяет, что подписка отвечает. Не отвечает – возвращает прежнюю
|
||||
и пишет об этом в журнал `/var/log/kit-update.log`.
|
||||
- Не любите сюрпризы? `kit update --manual`, и обновляться будете сами, командой `kit update`.
|
||||
|
||||
**Переезд без боли: `kit backup`**
|
||||
|
||||
- `kit backup` складывает в один файл всё, что нужно серверу: подключения, ключи, пользователей,
|
||||
настройки подписки. `--restore` поднимает из него тот же сервер на новом VPS.
|
||||
- Хостер поднял цены или IP попал в немилость? Переезжаете за 5 минут, а на домене клиенты
|
||||
вообще ничего не заметят.
|
||||
- Копия восстанавливается только из вашего файла и сначала проходит досмотр: лишние файлы
|
||||
и хитрые пути внутри архива, и восстановление не начнётся.
|
||||
|
||||
**Заглушка с характером**
|
||||
|
||||
- На случайный заход сервер теперь показывает один из шести сайтов: кофейню, фотографа, блог
|
||||
про Linux и других. Выбирается при установке, так что тысячи серверов 3X-UI KIT больше
|
||||
не выглядят как близнецы. Всё лежит внутри скрипта, ничего не качаем со сторонних сайтов.
|
||||
|
||||
**Как обновиться**
|
||||
|
||||
- Сервер 3X-UI KIT из версии 1.0 (там ещё нет `kit update`), один раз:
|
||||
`curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update`
|
||||
- Дальше хватит `kit update`, он сам подскажет, когда выйдет новая версия.
|
||||
- Hysteria2: `hy2 update`.
|
||||
- Дальше обновления будут приходить сами.
|
||||
- Hysteria2: `hy2 update` (тоже проверяет подпись).
|
||||
- Новые установки получают всё сразу.
|
||||
|
||||
## v1.0
|
||||
|
||||
@@ -14,10 +14,18 @@
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **Вышла версия 1.1:** исправления по аудиту безопасности, автообновление только подписанных
|
||||
> релизов, резервная копия и переезд. [Что нового](CHANGELOG.md) · сервер на 1.0 обновляется
|
||||
> одной командой:
|
||||
> ```bash
|
||||
> curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
> ```
|
||||
|
||||
**3X-UI KIT** превращает чистый VPS в готовый VPN-сервер за несколько минут. Скрипт ставит
|
||||
официальную панель [3X-UI](https://github.com/MHSanaei/3x-ui), настраивает все популярные
|
||||
протоколы, сертификат и файрвол и выдаёт одну ссылку-подписку. Её можно вставить в любое
|
||||
приложение — оно само получит подходящие ему протоколы. Домен не нужен.
|
||||
приложение – оно само получит подходящие ему протоколы. Домен не нужен.
|
||||
|
||||
<div align="center">
|
||||
<img alt="Конец установки 3X-UI KIT" src="manuals/assets/script-3x-ui.svg" width="760">
|
||||
@@ -25,23 +33,25 @@
|
||||
|
||||
## Возможности
|
||||
|
||||
- 🧩 **11 протоколов сразу** — VLESS REALITY, XHTTP, WebSocket, Trojan gRPC, VMess,
|
||||
- 🧩 **11 протоколов сразу** – VLESS REALITY, XHTTP, WebSocket, Trojan gRPC, VMess,
|
||||
Shadowsocks 2022, Hysteria2, TUIC, AmneziaWG (классика и 3.1) и MTProto для Telegram.
|
||||
- 🚪 **Всё TCP — через порт 443.** Панель, подписка и протоколы спрятаны за одним портом,
|
||||
- 🚪 **Всё TCP – через порт 443.** Панель, подписка и протоколы спрятаны за одним портом,
|
||||
а на случайный заход сервер показывает обычный сайт.
|
||||
- 🔗 **Одна подписка на все приложения.** Hiddify, Happ, v2rayN, Karing, Clash Verge и FlClash
|
||||
получают свой формат и только те протоколы, которые умеют.
|
||||
- 👥 **Дополнительные пользователи одной командой** — `kit user add` добавляет пользователя
|
||||
- 👥 **Дополнительные пользователи одной командой** – `kit user add` добавляет пользователя
|
||||
сразу во все протоколы с общим лимитом трафика, сроком и числом устройств.
|
||||
- 🔄 **Обновляется сам, но только подписанными релизами**, а `kit backup` переносит сервер
|
||||
на новый VPS вместе с пользователями и ключами.
|
||||
- 🔒 **Сертификат Let's Encrypt на IP** выпускается и продлевается сам, панель скрыта на
|
||||
случайном пути со случайными логином и паролем.
|
||||
- ✅ **Проверено настоящими клиентами** — каждый протокол на ядрах Xray, Mihomo и sing-box,
|
||||
- ✅ **Проверено настоящими клиентами** – каждый протокол на ядрах Xray, Mihomo и sing-box,
|
||||
в том числе с сервером в России: [tests/matrix](tests/matrix/).
|
||||
|
||||
## Что понадобится
|
||||
|
||||
- VPS с **Ubuntu 22.04/24.04** или **Debian 12/13** и доступом root по SSH
|
||||
- Свободные порты **443** и **80** — на свежем сервере они свободны
|
||||
- Свободные порты **443** и **80** – на свежем сервере они свободны
|
||||
|
||||
## Установка
|
||||
|
||||
@@ -56,24 +66,40 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
<details>
|
||||
<summary>🔍 Хотите сначала посмотреть скрипт? Установка с проверкой</summary>
|
||||
|
||||
Скачайте скрипт конкретной версии, прочитайте его и сверьте SHA256 с тем, что указан
|
||||
на странице [релиза](https://github.com/itsnotkubrick/3X-UI_KIT/releases/latest):
|
||||
Скачайте скрипт конкретной версии, прочитайте его и сверьте SHA256 с файлом `SHA256SUMS`
|
||||
из того же релиза. Этот файл подписан ключом автора, подпись тоже можно проверить:
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/3x-ui.sh
|
||||
v=1.1; raw=https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$v
|
||||
curl -fsSLO $raw/scripts/3x-ui.sh -O $raw/SHA256SUMS -O $raw/SHA256SUMS.sig
|
||||
less 3x-ui.sh
|
||||
sha256sum 3x-ui.sh
|
||||
grep 3x-ui.sh SHA256SUMS && sha256sum 3x-ui.sh
|
||||
bash 3x-ui.sh
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Проверка подписи</summary>
|
||||
|
||||
Открытый ключ автора записан в [`scripts/kit.sh`](scripts/kit.sh) (`KIT_SIGNERS`). Сохраните его
|
||||
строку в файл `allowed_signers` в виде `releases@3x-ui-kit namespaces="3x-ui-kit-release" ssh-ed25519 AAAA…`
|
||||
и выполните:
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y verify -f allowed_signers -I releases@3x-ui-kit -n 3x-ui-kit-release -s SHA256SUMS.sig < SHA256SUMS
|
||||
```
|
||||
|
||||
«Good signature» – файл подписан автором. Так же каждую ночь проверяет обновления ваш сервер.
|
||||
|
||||
</details>
|
||||
|
||||
Скрипт версии 1.1 скачивает свои части (`kit`, `kit-sub`) из того же релиза, а установщик
|
||||
3X-UI сверяет по SHA256 и не запускает, если он изменился. Первый раз удобнее ставить на
|
||||
свежий VPS без ваших данных.
|
||||
|
||||
</details>
|
||||
|
||||
Подробно — подключение приложений, дополнительные пользователи и параметры —
|
||||
в **[инструкции](manuals/3x-ui.md)**. Нужен только Hysteria2 — есть
|
||||
Подробно – подключение приложений, дополнительные пользователи и параметры –
|
||||
в **[инструкции](manuals/3x-ui.md)**. Нужен только Hysteria2 – есть
|
||||
[отдельный скрипт](manuals/hysteria2.md).
|
||||
|
||||
> [!WARNING]
|
||||
@@ -82,10 +108,10 @@ bash 3x-ui.sh
|
||||
|
||||
## Генераторы конфигов для XKeen
|
||||
|
||||
Вставьте ссылку на сервер или подписку, отметьте нужные сервисы — и получите
|
||||
Вставьте ссылку на сервер или подписку, отметьте нужные сервисы – и получите
|
||||
готовый конфиг и одну команду, которая сама положит его на роутер.
|
||||
Всё считается в браузере, ссылки никуда не отправляются.
|
||||
Как поставить XKeen на роутер — в [инструкции для Keenetic](manuals/xkeen-keenetic.md).
|
||||
Как поставить XKeen на роутер – в [инструкции для Keenetic](manuals/xkeen-keenetic.md).
|
||||
|
||||
| | Генератор | Что получится |
|
||||
|---|---|---|
|
||||
@@ -94,13 +120,13 @@ bash 3x-ui.sh
|
||||
|
||||
## Полезное
|
||||
|
||||
- [XKeen](https://github.com/jameszeroX/XKeen) и его [вики](https://github.com/jameszeroX/XKeen/wiki) — документация по маршрутизации на Keenetic
|
||||
- [XKeen UI](https://github.com/zxc-rv/XKeen-UI) — веб-интерфейс для XKeen
|
||||
- [IP-адреса для AmneziaWG](https://github.com/RockBlack-VPN/ip-address) — актуальные списки от RockBlack
|
||||
- [XKeen](https://github.com/jameszeroX/XKeen) и его [вики](https://github.com/jameszeroX/XKeen/wiki) – документация по маршрутизации на Keenetic
|
||||
- [XKeen UI](https://github.com/zxc-rv/XKeen-UI) – веб-интерфейс для XKeen
|
||||
- [IP-адреса для AmneziaWG](https://github.com/RockBlack-VPN/ip-address) – актуальные списки от RockBlack
|
||||
|
||||
## Поддержать проект
|
||||
|
||||
Скрипты и инструкции бесплатные. Донат добровольный — он помогает оплачивать
|
||||
Скрипты и инструкции бесплатные. Донат добровольный – он помогает оплачивать
|
||||
тестовые серверы и держать скрипты в актуальном состоянии. Спасибо! 💜
|
||||
|
||||
| Способ | |
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
скрипт его не запустит.
|
||||
- Бинарник Hysteria сверяется с SHA256, записанным в самом скрипте.
|
||||
- Свои части (`kit`, `kit-sub`, `hy2 update`) скрипт берёт из тега релиза, а не из ветки `main`.
|
||||
- Обновления (`kit update`, ночное автообновление, `hy2 update`) ставятся, только если файл
|
||||
`SHA256SUMS` релиза подписан ключом автора (`ssh-keygen -Y sign`), версия внутри подписи
|
||||
совпадает, а каждый файл сошёлся с подписанным SHA256. Закрытый ключ не хранится на GitHub.
|
||||
Откат на старую версию запрещён, после обновления проверяется подписка, при сбое – возврат.
|
||||
- Резервная копия `kit backup` доступна только root (права 600); `--restore` принимает только
|
||||
локальный файл и отклоняет архивы с лишними файлами, ссылками и путями вида `../`.
|
||||
- Подписка `kit-sub` работает без прав root: systemd `DynamicUser` и `LoadCredential`.
|
||||
- Все данные, которые попадают в файлы настроек, проверяются и экранируются.
|
||||
- Генераторы конфигов для роутеров работают только в браузере и никуда не отправляют ваши ссылки.
|
||||
@@ -22,4 +28,4 @@
|
||||
|
||||
Скрипт ставит стороннее ПО: панель 3X-UI, ядро Xray, Hysteria, nginx. Мы закрепляем их версии,
|
||||
но не проверяем их исходный код целиком. Сгенерированный конфиг Mihomo скачивает списки
|
||||
сайтов (rule-set) и веб-панель из их проектов на GitHub.
|
||||
сайтов (rule-set) и веб-панель zashboard (закреплённой версии) из их проектов на GitHub.
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
и ядро Xray, с которым работают все клиенты (Hiddify, Mihomo, v2rayN, Happ);
|
||||
- выпускает сертификат Let's Encrypt на IP сервера и сам его продлевает;
|
||||
- прячет панель на случайном порту и пути, со случайными логином и паролем;
|
||||
- подбирает сайт для маскировки REALITY — с TLS 1.3 и HTTP/2;
|
||||
- подбирает сайт для маскировки REALITY – с TLS 1.3 и HTTP/2;
|
||||
- создаёт подключения на всех протоколах из таблицы ниже, генерирует ключи
|
||||
и обфускацию AmneziaWG;
|
||||
- включает единую подписку с учётом приложения: Clash Verge и FlClash получают готовый
|
||||
конфиг Mihomo с AmneziaWG, остальные приложения — ссылки только на то, что они умеют;
|
||||
конфиг Mihomo с AmneziaWG, остальные приложения – ссылки только на то, что они умеют;
|
||||
- открывает в ufw только нужные порты.
|
||||
|
||||
## Протоколы
|
||||
@@ -42,17 +42,17 @@
|
||||
доходят до панели, поэтому лимит устройств работает.
|
||||
|
||||
Каждый протокол проверен настоящим подключением на ядрах Xray, Mihomo и sing-box,
|
||||
в том числе через интернет к серверу в России — стенд лежит в [tests/matrix](../tests/matrix/).
|
||||
в том числе через интернет к серверу в России – стенд лежит в [tests/matrix](../tests/matrix/).
|
||||
|
||||
> [!NOTE]
|
||||
> Обычный WireGuard по умолчанию не ставится: в России его рукопожатие режет DPI,
|
||||
> а попытки подключения могут привести к блокировке IP сервера. AmneziaWG — тот же WireGuard
|
||||
> с маскировкой — проходит. Если сервер и пользователи за границей, добавьте `wg` в `--protocols`.
|
||||
> а попытки подключения могут привести к блокировке IP сервера. AmneziaWG – тот же WireGuard
|
||||
> с маскировкой – проходит. Если сервер и пользователи за границей, добавьте `wg` в `--protocols`.
|
||||
|
||||
## Что понадобится
|
||||
|
||||
- VPS с Ubuntu 22.04/24.04 или Debian 12/13, доступ root по SSH
|
||||
- Свободные порты **443** и **80** — на свежем сервере они свободны
|
||||
- Свободные порты **443** и **80** – на свежем сервере они свободны
|
||||
|
||||
## Установка
|
||||
|
||||
@@ -66,12 +66,12 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
|
||||

|
||||
|
||||
1. **Адрес панели** — откройте его в браузере.
|
||||
1. **Адрес панели** – откройте его в браузере.
|
||||
2. **Логин и пароль** от панели.
|
||||
3. **Подписка** — вставьте её в приложение или отсканируйте QR-код.
|
||||
3. **Подписка** – вставьте её в приложение или отсканируйте QR-код.
|
||||
|
||||
Всё это, вместе с отдельными ссылками на каждый протокол, сохраняется в `/root/3x-ui.txt`:
|
||||
посмотреть снова — `cat /root/3x-ui.txt`.
|
||||
посмотреть снова – `cat /root/3x-ui.txt`.
|
||||
|
||||
## Подключение
|
||||
|
||||
@@ -84,8 +84,8 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
| Windows, macOS, Linux | Hiddify, v2rayN, Clash Verge |
|
||||
|
||||
Подписка смотрит, какое приложение её запрашивает: Clash Verge и FlClash получают AmneziaWG,
|
||||
а Happ, v2rayN, Karing и Hiddify — нет, потому что не умеют его. Для AmneziaVPN импортируйте
|
||||
ссылки `vpn://` из `/root/3x-ui.txt`, для Telegram — ссылку `tg://` оттуда же.
|
||||
а Happ, v2rayN, Karing и Hiddify – нет, потому что не умеют его. Для AmneziaVPN импортируйте
|
||||
ссылки `vpn://` из `/root/3x-ui.txt`, для Telegram – ссылку `tg://` оттуда же.
|
||||
|
||||
## Дополнительные пользователи
|
||||
|
||||
@@ -97,25 +97,35 @@ kit user add sasha --gb 50 --days 30
|
||||
|
||||
| Команда | Что делает |
|
||||
|---|---|
|
||||
| `kit user add имя [--gb N] [--days N] [--devices N]` | добавить; лимит трафика, срок и число устройств — по желанию |
|
||||
| `kit user add имя [--gb N] [--days N] [--devices N]` | добавить; лимит трафика, срок и число устройств – по желанию |
|
||||
| `kit user list` | кто сколько израсходовал, до какого числа и когда был в сети |
|
||||
| `kit user link имя [--all]` | подписка и QR-код ещё раз; `--all` — ещё ссылки `vpn://` и `tg://` |
|
||||
| `kit user limit имя --gb N --days N` | изменить лимиты (0 — без ограничений) |
|
||||
| `kit user link имя [--all]` | подписка и QR-код ещё раз; `--all` – ещё ссылки `vpn://` и `tg://` |
|
||||
| `kit user limit имя --gb N --days N` | изменить лимиты (0 – без ограничений) |
|
||||
| `kit user off имя` / `kit user on имя` | временно выключить и включить |
|
||||
| `kit user del имя` | удалить |
|
||||
|
||||
Трафик и лимиты общие на все протоколы: 50 ГБ — это 50 ГБ суммарно, через что бы пользователь ни подключался.
|
||||
Трафик и лимиты общие на все протоколы: 50 ГБ – это 50 ГБ суммарно, через что бы пользователь ни подключался.
|
||||
|
||||
## Обновление
|
||||
|
||||
Обновления kit и подписки kit-sub приходят сами: раз в сутки ночью сервер проверяет, не вышла ли
|
||||
новая версия 3X-UI KIT, и ставит её. Пользователи, ссылки и подписки не меняются, раздавать
|
||||
ничего заново не нужно.
|
||||
|
||||
Сервер ставит только **подписанные** релизы. Автор подписывает каждый релиз своим ключом, а
|
||||
открытая часть ключа записана в `kit` на вашем сервере. Если файлы на GitHub подменят, подпись
|
||||
не сойдётся и сервер откажется обновляться: одного взлома GitHub для этого мало. После
|
||||
обновления сервер проверяет, что подписка отвечает, и при сбое возвращает прежнюю версию.
|
||||
Панель 3X-UI и ядро Xray автообновление не трогает.
|
||||
|
||||
```bash
|
||||
kit update
|
||||
kit update # обновить сейчас, не дожидаясь ночи
|
||||
kit update --manual # выключить автообновление
|
||||
kit update --auto # включить обратно
|
||||
kit version # версии kit, панели и ядра, включено ли автообновление
|
||||
```
|
||||
|
||||
Обновляет команду `kit` и подписку `kit-sub` до новой версии 3X-UI KIT. Пользователи, ссылки
|
||||
и подписки не меняются, раздавать ничего заново не нужно. Файлы берутся из тега релиза
|
||||
на GitHub, а если новая подписка не запустится, `kit update` вернёт прежнюю.
|
||||
`kit version` покажет версию kit, панели и ядра Xray.
|
||||
Журнал ночных обновлений: `/var/log/kit-update.log`.
|
||||
|
||||
Сервер ставили версией 1.0? В ней команды `kit update` ещё нет, поэтому один раз:
|
||||
|
||||
@@ -123,19 +133,52 @@ kit update
|
||||
curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
```
|
||||
|
||||
Дальше обновления будут приходить сами.
|
||||
|
||||
Панель 3X-UI обновляется отдельно, через `x-ui`. Ядро Xray в панели не обновляйте до последней
|
||||
версии: с Xray 26.7 и новее Hiddify, FlClash и Clash Verge не подключаются к REALITY.
|
||||
|
||||
## Резервная копия и переезд
|
||||
|
||||
```bash
|
||||
kit backup
|
||||
```
|
||||
|
||||
Сохранит в `/root/kit-backup-ДАТА.tar.gz` всё, что нужно, чтобы поднять тот же сервер в другом
|
||||
месте: подключения, ключи, пользователей с их лимитами, настройки подписки и nginx. Файл виден
|
||||
только root, и в нём ключи от сервера, поэтому храните его как пароль. Скачать к себе:
|
||||
|
||||
```bash
|
||||
scp root@IP:/root/kit-backup-ДАТА.tar.gz .
|
||||
```
|
||||
|
||||
Переезд на новый VPS: скопируйте файл на новый сервер и запустите установщик с `--restore`:
|
||||
|
||||
```bash
|
||||
scp kit-backup-ДАТА.tar.gz root@НОВЫЙ_IP:
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh) --restore kit-backup-ДАТА.tar.gz
|
||||
```
|
||||
|
||||
Скрипт поставит ту же версию 3X-UI и вернёт всё из копии, логин и пароль панели остаются
|
||||
прежними. Копию он берёт только из локального файла и сначала проверяет: лишние файлы или
|
||||
странные пути внутри архива, и восстановление не начнётся.
|
||||
|
||||
- **Сервер на домене** (`--cert`, `--key`, `--host`): переключите A-запись домена на новый IP,
|
||||
и клиенты ничего не заметят.
|
||||
- **Сервер на IP:** скрипт сам поменяет адрес в подключениях, но старые подписки указывают на
|
||||
старый IP. Клиентам нужно один раз добавить подписку заново: `kit user link имя`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Что делает |
|
||||
|---|---|
|
||||
| `--protocols reality,hy2,ws` | только выбранные протоколы; `minimal` — только REALITY |
|
||||
| `--protocols reality,hy2,ws` | только выбранные протоколы; `minimal` – только REALITY |
|
||||
| `--port 8443` | порт REALITY (TCP) и Hysteria2 (UDP) вместо 443 |
|
||||
| `--cert fullchain.pem --key privkey.pem --host домен` | свой сертификат, например для домена |
|
||||
| `--sni www.samsung.com` | свой сайт для маскировки REALITY |
|
||||
| `--no-ufw` | не трогать файрвол |
|
||||
| `--multi-port` | прежняя схема: у каждого протокола свой порт, без nginx |
|
||||
| `--restore файл` | поднять сервер из резервной копии `kit backup` |
|
||||
|
||||
## Как устроена подписка
|
||||
|
||||
@@ -147,11 +190,11 @@ curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/script
|
||||
> [!NOTE]
|
||||
> Если порт 80 занят, сертификат Let's Encrypt не получить. Тогда скрипт сделает свой
|
||||
> сертификат и впишет его отпечаток в ссылки, а панель будет доступна через SSH-туннель.
|
||||
> Подписка в этом режиме работает только изнутри сервера — используйте отдельные ссылки.
|
||||
> Подписка в этом режиме работает только изнутри сервера – используйте отдельные ссылки.
|
||||
|
||||
## Обслуживание
|
||||
|
||||
Управление панелью — официальная команда `x-ui`: перезапуск, логи, обновление, удаление.
|
||||
Управление панелью – официальная команда `x-ui`: перезапуск, логи, обновление, удаление.
|
||||
|
||||
```bash
|
||||
x-ui
|
||||
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="330" viewBox="0 0 720 330" role="img" aria-label="Проводник · \\192.168.1.1\opkg\install"><title>Проводник · \\192.168.1.1\opkg\install</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="719" height="329" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="s" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">Проводник · \\192.168.1.1\opkg\install</text><text x="24" y="78" class="s" font-size="14" fill="#8b949e" font-weight="400" text-anchor="start">Флешка (ext4), открытая по сети: \\192.168.1.1\</text><rect x="30" y="92" width="330" height="32" rx="7" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="40" y="113" class="m" font-size="14.5" fill="#e6edf3" font-weight="400" text-anchor="start">📁 install</text><text x="380" y="113" class="s" font-size="12.5" fill="#3fb950" font-weight="400" text-anchor="start">создайте папку</text><rect x="64" y="136" width="330" height="32" rx="7" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="74" y="157" class="m" font-size="14.5" fill="#e6edf3" font-weight="400" text-anchor="start">📦 mipsel-installer.tar.gz</text><text x="414" y="157" class="s" font-size="12.5" fill="#3fb950" font-weight="400" text-anchor="start">установщик под ваш роутер</text><text x="24" y="206" class="s" font-size="13" fill="#e6edf3" font-weight="700" text-anchor="start">Какой установщик выбрать — по процессору роутера (есть в характеристиках модели):</text><rect x="24" y="218" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="235" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">mipsel</text><text x="128" y="235" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы MediaTek MT7621 / MT7628 — большинство моделей</text><rect x="24" y="246" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="263" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">mips</text><text x="128" y="263" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы EcoNet EN75xx</text><rect x="24" y="274" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="291" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">aarch64</text><text x="128" y="291" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы MediaTek MT798x (Filogic)</text></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="330" viewBox="0 0 720 330" role="img" aria-label="Проводник · \\192.168.x.x\opkg\install"><title>Проводник · \\192.168.x.x\opkg\install</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="719" height="329" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="s" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">Проводник · \\192.168.x.x\opkg\install</text><text x="24" y="78" class="s" font-size="14" fill="#8b949e" font-weight="400" text-anchor="start">Флешка (ext4), открытая по сети: \\192.168.x.x\</text><rect x="30" y="92" width="330" height="32" rx="7" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="40" y="113" class="m" font-size="14.5" fill="#e6edf3" font-weight="400" text-anchor="start">📁 install</text><text x="380" y="113" class="s" font-size="12.5" fill="#3fb950" font-weight="400" text-anchor="start">создайте папку</text><rect x="64" y="136" width="330" height="32" rx="7" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="74" y="157" class="m" font-size="14.5" fill="#e6edf3" font-weight="400" text-anchor="start">📦 mipsel-installer.tar.gz</text><text x="414" y="157" class="s" font-size="12.5" fill="#3fb950" font-weight="400" text-anchor="start">установщик под ваш роутер</text><text x="24" y="206" class="s" font-size="13" fill="#e6edf3" font-weight="700" text-anchor="start">Какой установщик выбрать – по процессору роутера (есть в характеристиках модели):</text><rect x="24" y="218" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="235" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">mipsel</text><text x="128" y="235" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы MediaTek MT7621 / MT7628 – большинство моделей</text><rect x="24" y="246" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="263" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">mips</text><text x="128" y="263" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы EcoNet EN75xx</text><rect x="24" y="274" width="90" height="24" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="69" y="291" class="m" font-size="13" fill="#3fb950" font-weight="400" text-anchor="middle">aarch64</text><text x="128" y="291" class="s" font-size="13" fill="#8b949e" font-weight="400" text-anchor="start">чипы MediaTek MT798x (Filogic)</text></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="230" viewBox="0 0 720 230" role="img" aria-label="Keenetic · Управление → OPKG"><title>Keenetic · Управление → OPKG</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="719" height="229" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="s" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">Keenetic · Управление → OPKG</text><rect x="12" y="62" width="696" height="60" rx="10" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="178" y="89" class="s" font-size="14" fill="#e6edf3" font-weight="400" text-anchor="end">Накопитель</text><rect x="194" y="70" width="466" height="30" rx="7" fill="#161b22" stroke="#3fb950" stroke-width="1" /><text x="206" y="90" class="s" font-size="13.5" fill="#e6edf3" font-weight="400" text-anchor="start">OPKG (ваша флешка)</text><path d="M638 82 l5 5 l5 -5" fill="none" stroke="#8b949e" stroke-width="1.6"/><text x="194" y="114" class="s" font-size="12" fill="#3fb950" font-weight="400" text-anchor="start">выберите флешку с папкой install</text><circle cx="688" cy="85" r="11" fill="#3fb950"/><text x="688" y="89.5" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="178" y="159" class="s" font-size="14" fill="#8b949e" font-weight="400" text-anchor="end">Сценарий initrc</text><rect x="194" y="140" width="466" height="30" rx="7" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="206" y="160" class="m" font-size="13.5" fill="#e6edf3" font-weight="400" text-anchor="start">/opt/etc/init.d/rc.unslung</text><text x="24" y="200" class="s" font-size="12" fill="#8b949e" font-weight="400" text-anchor="start">Нажмите «Сохранить» — установка Entware займёт несколько минут.</text></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="230" viewBox="0 0 720 230" role="img" aria-label="Keenetic · Управление → OPKG"><title>Keenetic · Управление → OPKG</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="719" height="229" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="s" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">Keenetic · Управление → OPKG</text><rect x="12" y="62" width="696" height="60" rx="10" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><text x="178" y="89" class="s" font-size="14" fill="#e6edf3" font-weight="400" text-anchor="end">Накопитель</text><rect x="194" y="70" width="466" height="30" rx="7" fill="#161b22" stroke="#3fb950" stroke-width="1" /><text x="206" y="90" class="s" font-size="13.5" fill="#e6edf3" font-weight="400" text-anchor="start">OPKG (ваша флешка)</text><path d="M638 82 l5 5 l5 -5" fill="none" stroke="#8b949e" stroke-width="1.6"/><text x="194" y="114" class="s" font-size="12" fill="#3fb950" font-weight="400" text-anchor="start">выберите флешку с папкой install</text><circle cx="688" cy="85" r="11" fill="#3fb950"/><text x="688" y="89.5" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="178" y="159" class="s" font-size="14" fill="#8b949e" font-weight="400" text-anchor="end">Сценарий initrc</text><rect x="194" y="140" width="466" height="30" rx="7" fill="#161b22" stroke="#30363d" stroke-width="1" /><text x="206" y="160" class="m" font-size="13.5" fill="#e6edf3" font-weight="400" text-anchor="start">/opt/etc/init.d/rc.unslung</text><text x="24" y="200" class="s" font-size="12" fill="#8b949e" font-weight="400" text-anchor="start">Нажмите «Сохранить» – установка Entware займёт несколько минут.</text></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
@@ -1 +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/…/scripts/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>
|
||||
<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/…/scripts/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>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="200" viewBox="0 0 720 200" role="img" aria-label="root@Keenetic: /opt/etc/xray/configs"><title>root@Keenetic: /opt/etc/xray/configs</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="719" height="199" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="m" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">root@Keenetic: /opt/etc/xray/configs</text><text x="22" y="70" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">~ # </text><text x="54.52" y="70" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">ls /opt/etc/xray/configs/</text><text x="22" y="91" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">03_inbounds.json # режим работы — оставьте как есть</text><rect x="10" y="97" width="700" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="692" cy="107.5" r="11" fill="#3fb950"/><text x="692" y="112.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="22" y="112" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">04_outbounds.json </text><text x="184.60000000000002" y="112" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># подключение к вашему серверу</text><rect x="10" y="118" width="700" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="692" cy="128.5" r="11" fill="#3fb950"/><text x="692" y="133.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">2</text><text x="22" y="133" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">05_routing.json </text><text x="184.60000000000002" y="133" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># что пускать через прокси</text><text x="22" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></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">xkeen -restart</text></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="200" viewBox="0 0 720 200" role="img" aria-label="root@Keenetic: /opt/etc/xray/configs"><title>root@Keenetic: /opt/etc/xray/configs</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="719" height="199" rx="14" fill="#0d1117" stroke="#30363d" stroke-width="1" /><path d="M1 44 H719" 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="360.0" y="27" class="m" font-size="13" fill="#8b949e" font-weight="500" text-anchor="middle">root@Keenetic: /opt/etc/xray/configs</text><text x="22" y="70" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve">~ # </text><text x="54.52" y="70" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">ls /opt/etc/xray/configs/</text><text x="22" y="91" class="m" font-size="13.5" fill="#8b949e" xml:space="preserve">03_inbounds.json # режим работы – оставьте как есть</text><rect x="10" y="97" width="700" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="692" cy="107.5" r="11" fill="#3fb950"/><text x="692" y="112.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">1</text><text x="22" y="112" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">04_outbounds.json </text><text x="184.60000000000002" y="112" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># подключение к вашему серверу</text><rect x="10" y="118" width="700" height="22" rx="5" fill="#3fb95022" stroke="#3fb950" stroke-width="1" /><circle cx="692" cy="128.5" r="11" fill="#3fb950"/><text x="692" y="133.0" class="s" font-size="12" fill="#0d1117" font-weight="700" text-anchor="middle">2</text><text x="22" y="133" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve">05_routing.json </text><text x="184.60000000000002" y="133" class="m" font-size="13.5" fill="#3fb950" xml:space="preserve"># что пускать через прокси</text><text x="22" y="154" class="m" font-size="13.5" fill="#e6edf3" xml:space="preserve"></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">xkeen -restart</text></svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -3,13 +3,13 @@
|
||||
[← На главную](../README.md)
|
||||
|
||||
[Hysteria2](https://github.com/HyNetworks/hysteria) работает поверх UDP и
|
||||
хорошо держит скорость на плохих каналах — отличная пара к REALITY. Одна
|
||||
хорошо держит скорость на плохих каналах – отличная пара к REALITY. Одна
|
||||
команда ставит сервер, сертификат и сайт-заглушку и выдаёт готовую ссылку.
|
||||
|
||||
**Что делает [скрипт](../scripts/hysteria2.sh):**
|
||||
|
||||
- ставит официальный Hysteria2 проверенной версии и сверяет контрольную сумму;
|
||||
- с доменом — получает сертификат Let's Encrypt, без домена — делает свой
|
||||
- с доменом – получает сертификат Let's Encrypt, без домена – делает свой
|
||||
и закрепляет его отпечаток прямо в ссылке;
|
||||
- поднимает сайт-заглушку: при заходе на ваш домен откроется страница
|
||||
«RETRYING CONNECTION»;
|
||||
@@ -20,7 +20,7 @@
|
||||
## Что понадобится
|
||||
|
||||
- VPS с Ubuntu 22.04/24.04 или Debian 12/13, доступ root по SSH
|
||||
- По желанию — домен, A-запись которого указывает на IP сервера
|
||||
- По желанию – домен, A-запись которого указывает на IP сервера
|
||||
|
||||
## Установка
|
||||
|
||||
@@ -32,7 +32,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
|
||||

|
||||
|
||||
1. Скрипт спросит **домен и почту**. Нет домена — просто нажмите Enter.
|
||||
1. Скрипт спросит **домен и почту**. Нет домена – просто нажмите Enter.
|
||||
2. В конце он покажет **ссылку** `hy2://` и QR-код.
|
||||
|
||||
> [!TIP]
|
||||
@@ -56,7 +56,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
| Команда | Что делает |
|
||||
|---|---|
|
||||
| `hy2 add имя` | добавить пользователя и показать его ссылку |
|
||||
| `hy2 del имя` | удалить — его ссылка сразу перестанет работать |
|
||||
| `hy2 del имя` | удалить – его ссылка сразу перестанет работать |
|
||||
| `hy2 list` | список пользователей |
|
||||
| `hy2 link имя` | ссылка и QR-код ещё раз |
|
||||
| `hy2 status` | версия, адрес и состояние |
|
||||
@@ -65,11 +65,11 @@ bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main
|
||||
|
||||
## Если что-то пошло не так
|
||||
|
||||
- **«Не удалось получить сертификат»** — A-запись домена ещё не обновилась
|
||||
- **«Не удалось получить сертификат»** – A-запись домена ещё не обновилась
|
||||
или порт 80 закрыт у хостера. Исправьте и выполните `hy2 restart`.
|
||||
- **Клиент не подключается** — проверьте, что у хостера открыт UDP-порт 443
|
||||
- **Клиент не подключается** – проверьте, что у хостера открыт UDP-порт 443
|
||||
(у некоторых хостеров есть свой файрвол в личном кабинете).
|
||||
- **Порт 443 занят** — поставьте на другой: добавьте к команде установки `--port 8443`.
|
||||
- **Порт 443 занят** – поставьте на другой: добавьте к команде установки `--port 8443`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
[XKeen](https://github.com/jameszeroX/XKeen) направляет через прокси
|
||||
(Xray или Mihomo) трафик только выбранных устройств, а остальные ходят
|
||||
в интернет напрямую. Всё работает на роутере — на телефоны и ПК ничего
|
||||
в интернет напрямую. Всё работает на роутере – на телефоны и ПК ничего
|
||||
ставить не нужно.
|
||||
|
||||
## Что понадобится
|
||||
|
||||
- Роутер **Keenetic** или **Netcraze** с USB-портом
|
||||
- USB-флешка, отформатированная в **ext4**
|
||||
- Свой сервер с VLESS — например, из [этой инструкции](3x-ui.md)
|
||||
- Свой сервер с VLESS – например, из [этой инструкции](3x-ui.md)
|
||||
|
||||
## 1. Установите компоненты
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## 2. Установите Entware
|
||||
|
||||
1. Вставьте флешку в роутер и откройте её по сети: `\\192.168.1.1\`.
|
||||
1. Вставьте флешку в роутер и откройте её по сети: `\\192.168.x.x\`.
|
||||
2. Создайте папку `install` и положите туда установщик под процессор роутера:
|
||||
[mipsel](https://bin.entware.net/mipselsf-k3.4/installer/mipsel-installer.tar.gz),
|
||||
[mips](https://bin.entware.net/mipssf-k3.4/installer/mips-installer.tar.gz) или
|
||||
@@ -37,12 +37,12 @@
|
||||

|
||||
|
||||
Через несколько минут Entware установится. Подключиться к нему можно по SSH:
|
||||
порт `222`, логин `root`, пароль `keenetic` — **сразу смените его** командой `passwd`.
|
||||
порт `222`, логин `root`, пароль `keenetic` – **сразу смените его** командой `passwd`.
|
||||
|
||||
## 3. Настройте DNS
|
||||
|
||||
Пропишите шифрованные DNS-серверы (DoT или DoH) по
|
||||
[инструкции Keenetic](https://support.keenetic.ru/ultra/kn-1811/ru/31543-dot-and-doh-proxy-servers-for-dns-requests-encryption.html) —
|
||||
[инструкции Keenetic](https://support.keenetic.ru/ultra/kn-1811/ru/31543-dot-and-doh-proxy-servers-for-dns-requests-encryption.html) –
|
||||
без этого XKeen работает неправильно.
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -50,7 +50,7 @@
|
||||
> Создайте его в разделе **Пользователи и доступ**, вставьте в
|
||||
> [шаблон xkeen.json](https://github.com/jameszeroX/XKeen/releases/download/2.0.1_Beta/xkeen.json)
|
||||
> и положите файл на роутер по пути `/opt/etc/xkeen/xkeen.json`.
|
||||
> Подробнее — в [вики XKeen](https://github.com/jameszeroX/XKeen/wiki/Порядок-установки).
|
||||
> Подробнее – в [вики XKeen](https://github.com/jameszeroX/XKeen/wiki/Порядок-установки).
|
||||
|
||||
## 4. Установите XKeen
|
||||
|
||||
@@ -73,16 +73,16 @@ sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/insta
|
||||
|
||||

|
||||
|
||||
1. `04_outbounds.json` — подключение к вашему серверу.
|
||||
2. `05_routing.json` — какие сайты и сервисы пускать через прокси.
|
||||
1. `04_outbounds.json` – подключение к вашему серверу.
|
||||
2. `05_routing.json` – какие сайты и сервисы пускать через прокси.
|
||||
|
||||
Оба файла собирает наш **[генератор Xray](https://itsnotkubrick.github.io/3X-UI_KIT/tools/xray/)**:
|
||||
вставьте ссылку `vless://`, отметьте сервисы — и получите одну команду,
|
||||
вставьте ссылку `vless://`, отметьте сервисы – и получите одну команду,
|
||||
которая сама запишет файлы на роутер и перезапустит XKeen. Для ядра Mihomo
|
||||
и ссылок Hysteria2 есть **[генератор Mihomo](https://itsnotkubrick.github.io/3X-UI_KIT/tools/mihomo/)**.
|
||||
|
||||
> [!NOTE]
|
||||
> Генераторы работают прямо в браузере — ссылки с паролями никуда не отправляются.
|
||||
> Генераторы работают прямо в браузере – ссылки с паролями никуда не отправляются.
|
||||
|
||||
Чтобы через прокси ходили только нужные устройства, в веб-интерфейсе откройте
|
||||
**Приоритеты подключений → Политики доступа в Интернет**, создайте политику
|
||||
@@ -92,7 +92,7 @@ sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/insta
|
||||
> Без политики `xkeen` через прокси пойдёт трафик **всех** устройств в сети.
|
||||
|
||||
> [!TIP]
|
||||
> Управлять настройками удобнее из браузера — через
|
||||
> Управлять настройками удобнее из браузера – через
|
||||
> [XKeen UI](https://github.com/zxc-rv/XKeen-UI).
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 3X-UI со всеми протоколами одной командой — https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
# 3X-UI со всеми протоколами одной командой – https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
#
|
||||
# Установка: bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)
|
||||
#
|
||||
@@ -8,7 +8,7 @@
|
||||
# REALITY, XHTTP, VLESS/VMess WS, Trojan gRPC, Shadowsocks 2022, Hysteria2, TUIC,
|
||||
# AmneziaWG (классика и 3.1) и MTProto (и WireGuard по запросу), включает единую подписку
|
||||
# с форматом под каждый клиент и настраивает ufw. Домены не нужны.
|
||||
# Каждый протокол проверен настоящими клиентами — см. tests/matrix.
|
||||
# Каждый протокол проверен настоящими клиентами – см. tests/matrix.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
@@ -19,19 +19,19 @@ KIT_VERSION="1.1"
|
||||
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
|
||||
KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"
|
||||
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
|
||||
# Clash Verge, Mihomo в XKeen) не проходят REALITY — проверено 2026-09-25.
|
||||
# 26.6.27 — последняя версия, с которой работают все клиенты и которую принимает 3X-UI.
|
||||
# Clash Verge, Mihomo в XKeen) не проходят REALITY – проверено 2026-09-25.
|
||||
# 26.6.27 – последняя версия, с которой работают все клиенты и которую принимает 3X-UI.
|
||||
XRAY_CORE="v26.6.27"
|
||||
XUI_REPO="MHSanaei/3x-ui"
|
||||
RESULT=/root/3x-ui.txt
|
||||
XUI_ENV=/etc/x-ui/install-result.env
|
||||
# Сайты для маскировки REALITY: нужны TLS 1.3 и HTTP/2. Берём первый доступный.
|
||||
# Apple, iCloud, Microsoft и домены .ru сам Xray не советует — их тут нет.
|
||||
# Apple, iCloud, Microsoft и домены .ru сам Xray не советует – их тут нет.
|
||||
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)
|
||||
# Обычный WireGuard в России режет DPI (проверено 2026-09-27: рукопожатие доходит до сервера,
|
||||
# ответ — нет), а его попытки могут привлечь блокировку IP. По умолчанию не ставим.
|
||||
# ответ – нет), а его попытки могут привлечь блокировку IP. По умолчанию не ставим.
|
||||
DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic 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=()
|
||||
@@ -101,7 +101,7 @@ wait_panel() {
|
||||
|
||||
# ---------- установка ----------
|
||||
|
||||
# Пасхалка — только в конце установки.
|
||||
# Пасхалка – только в конце установки.
|
||||
kit_banner() {
|
||||
echo
|
||||
printf '%s' "$G"
|
||||
@@ -119,7 +119,7 @@ ART
|
||||
echo " https://github.com/itsnotkubrick/3X-UI_KIT"
|
||||
echo " ${D}it's not Kubrick. it's just a VPN.${N}"
|
||||
echo
|
||||
echo "Ниже — данные для входа в панель и подключения."
|
||||
echo "Ниже – данные для входа в панель и подключения."
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -128,14 +128,14 @@ main() {
|
||||
if [[ -f $RESULT && -x /usr/local/x-ui/x-ui ]]; then
|
||||
die "3X-UI уже установлена этим скриптом. Управление: команда x-ui, данные для входа: cat $RESULT"
|
||||
fi
|
||||
# Панель удалили через меню x-ui, а наши файлы остались — убираем их и ставим заново.
|
||||
# Панель удалили через меню x-ui, а наши файлы остались – убираем их и ставим заново.
|
||||
if [[ -f $RESULT ]]; then
|
||||
warn "Панель 3X-UI удалена, но остались файлы прошлой установки — убираю их."
|
||||
systemctl disable --now kit-sub >/dev/null 2>&1 || true
|
||||
rm -rf /etc/systemd/system/kit-sub.service /usr/local/lib/kit-sub /etc/kit-sub /etc/kit /usr/local/bin/kit \
|
||||
warn "Панель 3X-UI удалена, но остались файлы прошлой установки – убираю их."
|
||||
systemctl disable --now kit-sub kit-update.timer >/dev/null 2>&1 || true
|
||||
rm -rf /etc/systemd/system/kit-sub.service /etc/systemd/system/kit-update.service /etc/systemd/system/kit-update.timer /usr/local/lib/kit-sub /etc/kit-sub /etc/kit /usr/local/bin/kit \
|
||||
/etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu /etc/cron.d/kit-sub-cert "$RESULT"
|
||||
systemctl daemon-reload
|
||||
# Наш nginx держит 443 — без этого проверка порта ниже не пустит REALITY.
|
||||
# Наш nginx держит 443 – без этого проверка порта ниже не пустит REALITY.
|
||||
if [[ -f /etc/nginx/kit-stream.conf ]]; then
|
||||
systemctl stop nginx >/dev/null 2>&1 || true
|
||||
rm -f /etc/nginx/kit-stream.conf /etc/nginx/conf.d/kit.conf
|
||||
@@ -143,10 +143,10 @@ main() {
|
||||
fi
|
||||
fi
|
||||
if [[ -d /usr/local/x-ui && ! -f $XUI_ENV ]]; then
|
||||
die "3X-UI уже установлена другим способом — не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
|
||||
die "3X-UI уже установлена другим способом – не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
|
||||
fi
|
||||
|
||||
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey="" multi=no
|
||||
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey="" multi=no restore=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--port) PORT=$2; shift 2 ;;
|
||||
@@ -159,12 +159,18 @@ main() {
|
||||
--multi-port) multi=yes; shift ;;
|
||||
--key) ukey=$2; shift 2 ;;
|
||||
--no-ufw) UFW=no; shift ;;
|
||||
--restore) restore=$2; shift 2 ;;
|
||||
-y|--yes) yes=yes; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Неизвестный параметр: $1 (см. --help)" ;;
|
||||
esac
|
||||
done
|
||||
[[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT"
|
||||
if [[ -n $restore ]]; then
|
||||
[[ -z $HOST || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host при восстановлении: IP нового сервера"
|
||||
restore_main "$restore"
|
||||
return
|
||||
fi
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$'
|
||||
[[ -z $SNI || $SNI =~ $re_host ]] || die "--sni: нужно имя сайта, например dl.google.com"
|
||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host: нужен IP или домен"
|
||||
@@ -172,7 +178,7 @@ main() {
|
||||
[[ $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"
|
||||
openssl x509 -in "$ucert" -noout 2>/dev/null || die "$ucert – не сертификат в формате PEM"
|
||||
PANEL_SSL=custom
|
||||
fi
|
||||
case $protos in
|
||||
@@ -183,19 +189,19 @@ main() {
|
||||
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 -E 'xray|nginx'; }; then
|
||||
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт — укажите другой: --port 8443"
|
||||
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт – укажите другой: --port 8443"
|
||||
fi
|
||||
|
||||
if [[ $PANEL_SSL == auto ]]; then
|
||||
if port_busy 80 tcp; then
|
||||
PANEL_SSL=none
|
||||
warn "Порт 80 занят — сертификат для панели не получить. Панель будет доступна только через SSH-туннель."
|
||||
warn "Порт 80 занят – сертификат для панели не получить. Панель будет доступна только через SSH-туннель."
|
||||
else
|
||||
PANEL_SSL=ip
|
||||
fi
|
||||
fi
|
||||
[[ $PANEL_SSL == ip ]] && port_busy 80 tcp && die "Для сертификата панели нужен свободный порт 80/tcp."
|
||||
# Доверенный сертификат (Let's Encrypt или свой) — панель и подписка доступны снаружи по HTTPS.
|
||||
# Доверенный сертификат (Let's Encrypt или свой) – панель и подписка доступны снаружи по HTTPS.
|
||||
TRUSTED=no
|
||||
[[ $PANEL_SSL == ip || $PANEL_SSL == custom ]] && TRUSTED=yes
|
||||
if [[ $PANEL_SSL == custom ]]; then
|
||||
@@ -219,7 +225,7 @@ main() {
|
||||
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 с ним работать не будет. Выберите другой сайт."
|
||||
die "$SNI не отвечает по TLS 1.3 + HTTP/2 – REALITY с ним работать не будет. Выберите другой сайт."
|
||||
fi
|
||||
say "Маскировка: ${B}$SNI${N}"
|
||||
# Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI.
|
||||
@@ -231,41 +237,20 @@ main() {
|
||||
SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com}
|
||||
|
||||
# --- официальный установщик 3X-UI с закреплённой версией ---
|
||||
local panel_port panel_path panel_user panel_pass tmp
|
||||
local panel_port panel_path panel_user panel_pass
|
||||
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 уже стоит после прошлого запуска — продолжаю с создания подключений"
|
||||
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"
|
||||
[[ $(sha256sum "$tmp" | awk '{print $1}') == "$XUI_INSTALL_SHA256" ]] \
|
||||
|| die "Установщик 3X-UI $XUI_VERSION не совпал с проверенным (SHA256) – не запускаю. Сообщите нам: github.com/itsnotkubrick/3X-UI_KIT/issues"
|
||||
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_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"
|
||||
install_xui "$panel_port" "$panel_path" "$panel_user" "$panel_pass"
|
||||
fi
|
||||
[[ -f $XUI_ENV ]] || die "Установщик не сохранил данные входа. Лог: /var/log/3x-ui-install.log"
|
||||
|
||||
# Данные для входа — из файла, который пишет сам установщик.
|
||||
# shellcheck disable=SC1090
|
||||
. "$XUI_ENV"
|
||||
TOKEN=$XUI_API_TOKEN
|
||||
# Панель может уже работать по HTTPS (сертификат ставится после установщика) — пробуем оба.
|
||||
local scheme
|
||||
for scheme in https http; do
|
||||
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
|
||||
curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $XUI_API_TOKEN" "$API/server/getNewUUID" 2>/dev/null && break
|
||||
done
|
||||
wait_panel
|
||||
# Данные для входа – из файла, который пишет сам установщик.
|
||||
connect_panel
|
||||
|
||||
if [[ $PANEL_SSL == custom ]]; then
|
||||
say "Подключаю ваш сертификат к панели"
|
||||
@@ -277,7 +262,7 @@ main() {
|
||||
|
||||
# Без сертификата панель и подписки не должны торчать наружу по HTTP.
|
||||
if [[ $PANEL_SSL == none ]]; then
|
||||
say "Панель без сертификата — открываю её только для SSH-туннеля (127.0.0.1)"
|
||||
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
|
||||
@@ -286,18 +271,7 @@ main() {
|
||||
fi
|
||||
|
||||
# --- ядро Xray, совместимое со всеми клиентами ---
|
||||
local cur_core
|
||||
cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
|
||||
if [[ $cur_core != "$XRAY_CORE" ]]; then
|
||||
say "Ставлю ядро Xray $XRAY_CORE (совместимо с Hiddify, Mihomo и другими клиентами)"
|
||||
api POST "server/installXray/$XRAY_CORE" '{}' >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
|
||||
[[ $cur_core == "$XRAY_CORE" ]] && break
|
||||
sleep 2
|
||||
done
|
||||
[[ $cur_core == "$XRAY_CORE" ]] || warn "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться."
|
||||
fi
|
||||
set_xray_core
|
||||
|
||||
# --- сертификат для протоколов с TLS ---
|
||||
setup_tls_cert
|
||||
@@ -305,18 +279,18 @@ main() {
|
||||
# --- подключения: все выбранные протоколы, один subId на пользователя ---
|
||||
EXISTING=$(api GET inbounds/list)
|
||||
SUBID=""
|
||||
# «Всё на 443» — для новых установок с доверенным сертификатом. Старую многопортовую
|
||||
# установку не переделываем: перенос работающих подключений — осознанное решение.
|
||||
# «Всё на 443» – для новых установок с доверенным сертификатом. Старую многопортовую
|
||||
# установку не переделываем: перенос работающих подключений – осознанное решение.
|
||||
if [[ $TRUSTED == yes && $multi == no ]]; then
|
||||
if jq -e 'any(.[]; .remark == "REALITY" and (.listen // "") != "127.0.0.1")' <<<"$EXISTING" >/dev/null; then
|
||||
warn "Установка уже работает в режиме с отдельными портами — оставляю его."
|
||||
warn "Установка уже работает в режиме с отдельными портами – оставляю его."
|
||||
else
|
||||
SINGLE=yes
|
||||
fi
|
||||
fi
|
||||
local p
|
||||
for p in "${PROTOS[@]}"; do "proto_$p"; done
|
||||
# Первый пользователь — сразу на всех протоколах (как «kit user add»).
|
||||
# Первый пользователь – сразу на всех протоколах (как «kit user add»).
|
||||
ensure_user
|
||||
|
||||
# --- подписка: ссылки, Clash/Mihomo и JSON с автоопределением клиента ---
|
||||
@@ -324,19 +298,14 @@ main() {
|
||||
[[ $SINGLE == yes ]] && setup_nginx
|
||||
install_kit_cli
|
||||
brand_xui_menu
|
||||
# Автообновление kit и kit-sub: только подписанные релизы, выключается kit update --manual.
|
||||
/usr/local/bin/kit update --auto >/dev/null 2>&1 || warn "Автообновление не включилось – включите позже: kit update --auto"
|
||||
|
||||
# --- файрвол ---
|
||||
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}
|
||||
OPEN+=("$ssh_port/tcp")
|
||||
[[ $TRUSTED == yes && $SINGLE == no ]] && OPEN+=("$XUI_PANEL_PORT/tcp" "$SUB_PORT/tcp")
|
||||
[[ $PANEL_SSL == ip ]] && OPEN+=("80/tcp")
|
||||
say "Настраиваю ufw: ${OPEN[*]}"
|
||||
local o
|
||||
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
|
||||
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную."
|
||||
setup_ufw
|
||||
fi
|
||||
|
||||
# --- итог ---
|
||||
@@ -359,13 +328,13 @@ main() {
|
||||
[[ $SINGLE == yes ]] && links=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${INNER[mtproto]}/\1\2port=443/" <<<"$links")
|
||||
umask 077
|
||||
{
|
||||
echo "3X-UI KIT (3X-UI $XUI_VERSION) — данные для входа (файл виден только root)"
|
||||
echo "3X-UI KIT (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"
|
||||
echo
|
||||
echo "Панель: $panel_url"
|
||||
echo "Логин: $XUI_USERNAME"
|
||||
echo "Пароль: $XUI_PASSWORD"
|
||||
echo
|
||||
[[ $TRUSTED == yes ]] && { echo "Подписка ($NAME) — все протоколы одной ссылкой:"; echo "$SUB_URL"; echo; }
|
||||
[[ $TRUSTED == yes ]] && { echo "Подписка ($NAME) – все протоколы одной ссылкой:"; echo "$SUB_URL"; echo; }
|
||||
echo "Отдельные подключения ($NAME):"
|
||||
echo "$links"
|
||||
} >"$RESULT"
|
||||
@@ -380,7 +349,7 @@ main() {
|
||||
echo "Пароль: ${B}$XUI_PASSWORD${N}"
|
||||
echo
|
||||
if [[ $TRUSTED == yes ]]; then
|
||||
echo "Подписка для ${B}$NAME${N} — все протоколы одной ссылкой. Вставьте её в Hiddify, v2rayN, Happ,"
|
||||
echo "Подписка для ${B}$NAME${N} – все протоколы одной ссылкой. Вставьте её в Hiddify, v2rayN, Happ,"
|
||||
echo "Clash Verge или FlClash: приложение само получит подходящий формат."
|
||||
echo
|
||||
echo "$SUB_URL"
|
||||
@@ -388,22 +357,77 @@ main() {
|
||||
qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true
|
||||
if [[ -n $AWG_LINKS ]]; then
|
||||
echo
|
||||
echo "AmneziaWG приходит по подписке в Clash Verge и FlClash; для AmneziaVPN — ссылки vpn:// в $RESULT."
|
||||
echo "AmneziaWG приходит по подписке в Clash Verge и FlClash; для AmneziaVPN – ссылки vpn:// в $RESULT."
|
||||
fi
|
||||
else
|
||||
echo "Без сертификата подписка недоступна снаружи — вот ссылки по одной:"
|
||||
echo "Без сертификата подписка недоступна снаружи – вот ссылки по одной:"
|
||||
echo
|
||||
echo "$links"
|
||||
fi
|
||||
echo
|
||||
if [[ -n $PIN && " ${CREATED[*]} " == *" TUIC "* ]]; then
|
||||
warn "TUIC со своим сертификатом: в клиенте включите «Разрешить небезопасный» (allow insecure) — отпечаток TUIC-ссылки не передают."
|
||||
warn "TUIC со своим сертификатом: в клиенте включите «Разрешить небезопасный» (allow insecure) – отпечаток TUIC-ссылки не передают."
|
||||
fi
|
||||
echo "Всё это сохранено в ${B}$RESULT${N}."
|
||||
echo
|
||||
echo "Дополнительные пользователи — одной командой, сразу во все протоколы, со своей подпиской:"
|
||||
echo "Дополнительные пользователи – одной командой, сразу во все протоколы, со своей подпиской:"
|
||||
echo " ${B}kit user add sasha --gb 50 --days 30${N}"
|
||||
echo " ${B}kit user list${N} — кто сколько израсходовал и до какого числа"
|
||||
echo " ${B}kit user list${N} – кто сколько израсходовал и до какого числа"
|
||||
}
|
||||
|
||||
# Официальный установщик 3X-UI закреплённой версии, сверенный по SHA256.
|
||||
install_xui() { # порт путь логин пароль
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)"
|
||||
curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh"
|
||||
[[ $(sha256sum "$tmp" | awk '{print $1}') == "$XUI_INSTALL_SHA256" ]] \
|
||||
|| die "Установщик 3X-UI $XUI_VERSION не совпал с проверенным (SHA256) – не запускаю. Сообщите нам: github.com/itsnotkubrick/3X-UI_KIT/issues"
|
||||
if ! XUI_NONINTERACTIVE=1 XUI_SSL_MODE="${PANEL_SSL/custom/none}" XUI_SERVER_IP="$HOST" \
|
||||
XUI_PANEL_PORT="$1" XUI_WEB_BASE_PATH="$2" \
|
||||
XUI_USERNAME="$3" XUI_PASSWORD="$4" \
|
||||
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"
|
||||
}
|
||||
|
||||
# Панель: API по https или http – сертификат мог появиться после установщика.
|
||||
connect_panel() {
|
||||
# shellcheck disable=SC1090
|
||||
. "$XUI_ENV"
|
||||
TOKEN=$XUI_API_TOKEN
|
||||
local scheme
|
||||
for scheme in https http; do
|
||||
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
|
||||
curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $XUI_API_TOKEN" "$API/server/getNewUUID" 2>/dev/null && break
|
||||
done
|
||||
wait_panel
|
||||
}
|
||||
|
||||
set_xray_core() {
|
||||
local cur_core
|
||||
cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
|
||||
if [[ $cur_core != "$XRAY_CORE" ]]; then
|
||||
say "Ставлю ядро Xray $XRAY_CORE (совместимо с Hiddify, Mihomo и другими клиентами)"
|
||||
api POST "server/installXray/$XRAY_CORE" '{}' >/dev/null
|
||||
for _ in $(seq 1 30); do
|
||||
cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
|
||||
[[ $cur_core == "$XRAY_CORE" ]] && break
|
||||
sleep 2
|
||||
done
|
||||
[[ $cur_core == "$XRAY_CORE" ]] || warn "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться."
|
||||
fi
|
||||
}
|
||||
|
||||
setup_ufw() {
|
||||
local ssh_port o
|
||||
ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}')
|
||||
OPEN+=("${ssh_port:-22}/tcp")
|
||||
say "Настраиваю ufw: ${OPEN[*]}"
|
||||
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
|
||||
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
|
||||
}
|
||||
|
||||
# ---------- сертификат ----------
|
||||
@@ -415,7 +439,7 @@ setup_tls_cert() {
|
||||
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),
|
||||
# Без Let's Encrypt – свой сертификат, а его отпечаток уходит в ссылки (pcs),
|
||||
# чтобы клиенты доверяли именно ему.
|
||||
CERT=/root/cert/self/fullchain.pem; KEY=/root/cert/self/privkey.pem
|
||||
if [[ ! -s $CERT ]]; then
|
||||
@@ -438,7 +462,7 @@ tls_json() { # alpn(JSON-массив)
|
||||
|
||||
# ---------- протоколы ----------
|
||||
|
||||
# AmneziaWG — в отдельной подписке: приложения на Xray и sing-box (Happ, v2rayN, Karing,
|
||||
# AmneziaWG – в отдельной подписке: приложения на Xray и sing-box (Happ, v2rayN, Karing,
|
||||
# Hiddify) его не умеют и видят как обычный WireGuard, который не подключается.
|
||||
client_base() { # суффикс
|
||||
local sid=$SUBID
|
||||
@@ -450,7 +474,7 @@ uuid() { cat /proc/sys/kernel/random/uuid; }
|
||||
rnd() { shuf -i "$1-$2" -n 1; }
|
||||
|
||||
# add_inbound remark port proto(tcp|udp|both|inner) protocol settings stream
|
||||
# inner — подключение за nginx: слушает 127.0.0.1, наружу порт не открываем.
|
||||
# inner – подключение за nginx: слушает 127.0.0.1, наружу порт не открываем.
|
||||
add_inbound() {
|
||||
local remark=$1 port=$2 net=$3 protocol=$4 settings=$5 stream=$6 body listen=""
|
||||
[[ $net == inner ]] && listen=127.0.0.1
|
||||
@@ -473,7 +497,7 @@ add_inbound() {
|
||||
local out
|
||||
out=$(curl -sSk -m 20 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X POST -d "$body" "$API/inbounds/add")
|
||||
if [[ $(jq -r '.success' <<<"$out") != true ]] && grep -q 'Duplicate email' <<<"$out"; then
|
||||
# Клиент с таким именем остался от удалённого подключения — берём уникальное имя.
|
||||
# Клиент с таким именем остался от удалённого подключения – берём уникальное имя.
|
||||
body=$(jq -c --arg sfx "-$(openssl rand -hex 2)" '.settings |= (fromjson | .clients[0].email += $sfx | tojson)' <<<"$body")
|
||||
out=$(curl -sSk -m 20 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X POST -d "$body" "$API/inbounds/add")
|
||||
fi
|
||||
@@ -490,7 +514,7 @@ open_port() { # port net
|
||||
}
|
||||
|
||||
# External Proxy 3X-UI: ссылки ведут на HOST:443, хотя подключение слушает localhost.
|
||||
# SNI — только для TLS через nginx: у REALITY своё имя сайта маскировки, его не трогаем.
|
||||
# SNI – только для TLS через nginx: у REALITY своё имя сайта маскировки, его не трогаем.
|
||||
ext_proxy() { # forceTls(same|tls) alpn(JSON)
|
||||
local sni=""
|
||||
[[ $HOST =~ ^[0-9.]+$ ]] || sni=$HOST
|
||||
@@ -606,7 +630,7 @@ proto_wg() {
|
||||
}
|
||||
|
||||
# AmneziaWG: классические параметры понимают Mihomo и роутеры Keenetic,
|
||||
# полный набор 3.1 — только приложения AmneziaVPN/AmneziaWG. Поэтому два подключения.
|
||||
# полный набор 3.1 – только приложения AmneziaVPN/AmneziaWG. Поэтому два подключения.
|
||||
awg_obfuscation() { # classic|full
|
||||
local jmin s1 s2
|
||||
jmin=$(rnd 40 89); s1=$(rnd 15 150); s2=$(rnd 15 150)
|
||||
@@ -655,12 +679,12 @@ telegram_reachable() {
|
||||
proto_mtproto() {
|
||||
# MTProto бесполезен, если сам сервер не достаёт до Telegram (некоторые хостеры его блокируют).
|
||||
if ! telegram_reachable; then
|
||||
warn "MTProto пропущен: с этого сервера недоступны серверы Telegram — прокси для Telegram здесь работать не будет."
|
||||
warn "MTProto пропущен: с этого сервера недоступны серверы Telegram – прокси для Telegram здесь работать не будет."
|
||||
return
|
||||
fi
|
||||
local settings
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
# FakeTLS-домен — свой сайт: nginx узнаёт MTProto по нему и передаёт mtg с реальным IP клиента.
|
||||
# FakeTLS-домен – свой сайт: nginx узнаёт MTProto по нему и передаёт mtg с реальным IP клиента.
|
||||
settings=$(jq -nc --arg d "$SNI3" --argjson c "$(client_base mtproto)" '{fakeTlsDomain: $d, proxyProtocolListener: true, clients: [$c + {secret: ""}]}')
|
||||
add_inbound "MTProto" "${INNER[mtproto]}" inner mtproto "$settings" '{}'
|
||||
else
|
||||
@@ -684,7 +708,7 @@ ensure_user() {
|
||||
[[ $missing == "[]" ]] || api POST "clients/$NAME/attach" "$(jq -nc --argjson i "$missing" '{inboundIds: $i}')" >/dev/null
|
||||
awg_attach "$NAME" "$SUBID"
|
||||
elif [[ -n $legacy ]]; then
|
||||
# Установка до kit 1.1: у каждого протокола свой клиент — оставляем как есть.
|
||||
# Установка до kit 1.1: у каждого протокола свой клиент – оставляем как есть.
|
||||
SUBID=$legacy
|
||||
else
|
||||
SUBID=$(rand_str 16 | tr 'A-Z' 'a-z')
|
||||
@@ -697,7 +721,7 @@ ensure_user() {
|
||||
# В 3X-UI 3.x у клиента одна запись и в ней одна пара ключей WireGuard и один адрес. Если
|
||||
# клиент подключён к двум AmneziaWG, в подписку для обоих уходят ключ и адрес одного из них,
|
||||
# и второй сервер клиента не узнаёт (проверено 2026-09-27). Поэтому к первому AmneziaWG
|
||||
# подключаем основную запись, а ко второму — запись-«двойник» «имя-awg» с подпиской «<id>-awg»
|
||||
# подключаем основную запись, а ко второму – запись-«двойник» «имя-awg» с подпиской «<id>-awg»
|
||||
# (subId в 3X-UI обязан быть уникальным) и теми же лимитами; kit-sub подмешивает её в Clash.
|
||||
awg_ids() { api GET inbounds/list | jq -r '[.[] | select(.protocol == "amneziawg") | .id] | sort | .[]'; }
|
||||
non_awg_ids() { api GET inbounds/list | jq -c '[.[] | select(.protocol != "amneziawg") | .id]'; }
|
||||
@@ -735,6 +759,10 @@ install_kit_cli() {
|
||||
printf 'MTPROTO_INNER=%q\n' "${INNER[mtproto]}"
|
||||
} >/etc/kit/kit.env
|
||||
chmod 600 /etc/kit/kit.env
|
||||
install_kit_file
|
||||
}
|
||||
|
||||
install_kit_file() {
|
||||
local d src=""
|
||||
d=$(dirname "${BASH_SOURCE[0]}")
|
||||
[[ -f $d/kit.sh && ${BASH_SOURCE[0]} != /dev/fd/* ]] && src=$d/kit.sh
|
||||
@@ -745,7 +773,7 @@ install_kit_cli() {
|
||||
|
||||
KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"
|
||||
|
||||
# После «x-ui → Uninstall» меню подсказывает команду официального установщика —
|
||||
# После «x-ui → Uninstall» меню подсказывает команду официального установщика –
|
||||
# меняем её на нашу. Только в echo: вызов установщика в «Update» не трогаем.
|
||||
# Правим один раз при установке, без cron: чужие файлы по расписанию не трогаем.
|
||||
brand_xui_menu() {
|
||||
@@ -760,13 +788,48 @@ brand_xui_menu() {
|
||||
|
||||
# ---------- всё на 443: nginx ----------
|
||||
|
||||
# Сайт-заглушка: на случайный заход по 443 сервер показывает обычный сайт. Шаблон выбирается
|
||||
# случайно из набора, чтобы тысячи серверов 3X-UI KIT не выглядели одинаково. Всё внутри
|
||||
# скрипта, со сторонних сайтов ничего не скачивается.
|
||||
stub_site() {
|
||||
local n year
|
||||
n=$(rnd 1 6); year=$(date +%Y)
|
||||
case $n in
|
||||
1) cat <<'HTML'
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7f9;color:#1f2328}main{text-align:center;padding:24px}h1{font-weight:600;font-size:28px}p{color:#57606a}</style></head><body><main><h1>Site is under construction</h1><p>Please check back soon.</p></main></body></html>
|
||||
HTML
|
||||
;;
|
||||
2) cat <<'HTML'
|
||||
<!DOCTYPE html><html><head><title>Welcome to nginx!</title><style>html{color-scheme:light dark}body{width:35em;margin:0 auto;font-family:Tahoma,Verdana,Arial,sans-serif}</style></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p><p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/>Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p><p><em>Thank you for using nginx.</em></p></body></html>
|
||||
HTML
|
||||
;;
|
||||
3) cat <<HTML
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lena Hart Photography</title><style>body{margin:0;font-family:Georgia,serif;background:#111;color:#eee}header{padding:48px 24px 16px;text-align:center}h1{font-weight:400;letter-spacing:.2em;text-transform:uppercase;font-size:22px}p{color:#aaa}.g{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;padding:24px}.g div{aspect-ratio:4/3;background:linear-gradient(135deg,#2b2b2b,#444)}footer{text-align:center;color:#666;padding:24px;font-size:13px}</style></head><body><header><h1>Lena Hart</h1><p>Landscape & travel photography</p></header><section class="g"><div></div><div></div><div></div><div></div><div></div><div></div></section><footer>© $year Lena Hart. New portfolio coming soon.</footer></body></html>
|
||||
HTML
|
||||
;;
|
||||
4) cat <<HTML
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Notes</title><style>body{max-width:640px;margin:0 auto;padding:40px 20px;font:16px/1.6 ui-monospace,Menlo,Consolas,monospace;color:#222;background:#fdfdf8}h1{font-size:20px}a{color:#0645ad}li{margin:6px 0}small{color:#888}</style></head><body><h1>~/notes</h1><p>Small notes about Linux, networks and home servers.</p><ul><li><a href="#">Setting up a home NAS with ZFS</a> <small>$year-03-14</small></li><li><a href="#">Notes on systemd timers</a> <small>$year-02-02</small></li><li><a href="#">Why I moved my blog to a static site</a> <small>$year-01-09</small></li></ul><p><small>Comments are closed.</small></p></body></html>
|
||||
HTML
|
||||
;;
|
||||
5) cat <<HTML
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Green Bean Coffee</title><style>body{margin:0;font-family:Helvetica,Arial,sans-serif;background:#f4efe6;color:#3b2f24;text-align:center}main{padding:72px 20px}h1{font-size:40px;margin:0 0 8px}p{font-size:18px}.b{display:inline-block;margin-top:16px;padding:10px 22px;border:2px solid #3b2f24;border-radius:24px}small{display:block;margin-top:48px;color:#8a7a68}</style></head><body><main><h1>Green Bean Coffee</h1><p>Specialty coffee · fresh pastries · open daily 8–18</p><span class="b">Online orders coming soon</span><small>© $year Green Bean Coffee</small></main></body></html>
|
||||
HTML
|
||||
;;
|
||||
6) cat <<HTML
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Status</title><style>body{margin:0;font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#fff;color:#24292f}main{max-width:560px;margin:64px auto;padding:0 20px}h1{font-size:22px}.r{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px solid #eaeef2}.ok{color:#1a7f37}small{color:#8c959f}</style></head><body><main><h1>System status</h1><div class="r"><span>Website</span><span class="ok">Operational</span></div><div class="r"><span>API</span><span class="ok">Operational</span></div><div class="r"><span>Storage</span><span class="ok">Operational</span></div><p><small>Updated automatically · $year</small></p></main></body></html>
|
||||
HTML
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
setup_nginx() {
|
||||
say "Настраиваю nginx: всё TCP через порт 443"
|
||||
apt-get install -y -qq nginx libnginx-mod-stream >/dev/null
|
||||
# Порт 80 нужен Let's Encrypt для продления сертификата — сайт nginx по умолчанию убираем.
|
||||
# Порт 80 нужен Let's Encrypt для продления сертификата – сайт nginx по умолчанию убираем.
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Панель — только через nginx.
|
||||
# Панель – только через nginx.
|
||||
local all
|
||||
all=$(api POST setting/all '{}')
|
||||
if [[ $(jq -r '.webListen' <<<"$all") != 127.0.0.1 ]]; then
|
||||
@@ -776,11 +839,9 @@ setup_nginx() {
|
||||
fi
|
||||
|
||||
install -d -m 755 /var/www/kit
|
||||
[[ -f /var/www/kit/index.html ]] || cat >/var/www/kit/index.html <<'HTML'
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7f9;color:#1f2328}main{text-align:center;padding:24px}h1{font-weight:600;font-size:28px}p{color:#57606a}</style></head><body><main><h1>Site is under construction</h1><p>Please check back soon.</p></main></body></html>
|
||||
HTML
|
||||
[[ -f /var/www/kit/index.html ]] || stub_site >/var/www/kit/index.html
|
||||
|
||||
# Маршруты — из текущих подключений панели: сайты REALITY, пути WebSocket, сервисы gRPC.
|
||||
# Маршруты – из текущих подключений панели: сайты REALITY, пути WebSocket, сервисы gRPC.
|
||||
local list reality_sni xhttp_sni mt_sni locs="" kind path port
|
||||
list=$(api GET inbounds/list)
|
||||
reality_sni=$(jq -r '.[] | select(.remark == "REALITY" and .listen == "127.0.0.1") | (.streamSettings | if type == "string" then fromjson else . end).realitySettings.serverNames[0]' <<<"$list")
|
||||
@@ -817,7 +878,7 @@ HTML
|
||||
local panel_path=/${XUI_WEB_BASE_PATH#/}
|
||||
panel_path=${panel_path%/}/
|
||||
{
|
||||
echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) — перезаписывается при повторном запуске."
|
||||
echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске."
|
||||
echo "stream {"
|
||||
echo " map \$ssl_preread_server_name \$kit_upstream {"
|
||||
[[ -n $reality_sni ]] && echo " $reality_sni 127.0.0.1:${INNER[reality]};"
|
||||
@@ -837,7 +898,7 @@ HTML
|
||||
echo "}"
|
||||
} >/etc/nginx/kit-stream.conf
|
||||
cat >/etc/nginx/conf.d/kit.conf <<NGX
|
||||
# Сгенерировано 3x-ui.sh (3X-UI KIT) — перезаписывается при повторном запуске.
|
||||
# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске.
|
||||
server {
|
||||
listen 127.0.0.1:${INNER[web]} ssl http2 proxy_protocol;
|
||||
server_name _;
|
||||
@@ -873,10 +934,10 @@ $locs
|
||||
}
|
||||
NGX
|
||||
grep -q 'kit-stream.conf' /etc/nginx/nginx.conf || echo 'include /etc/nginx/kit-stream.conf;' >>/etc/nginx/nginx.conf
|
||||
nginx -t >/tmp/nginx-test.log 2>&1 || { cat /tmp/nginx-test.log >&2; die "nginx не принял конфиг — лог выше."; }
|
||||
nginx -t >/tmp/nginx-test.log 2>&1 || { cat /tmp/nginx-test.log >&2; die "nginx не принял конфиг – лог выше."; }
|
||||
systemctl enable nginx >/dev/null 2>&1
|
||||
systemctl restart nginx
|
||||
# Let's Encrypt на IP продлевается каждые несколько дней — nginx раз в сутки перечитывает сертификат.
|
||||
# Let's Encrypt на IP продлевается каждые несколько дней – nginx раз в сутки перечитывает сертификат.
|
||||
echo '17 4 * * * root systemctl reload nginx >/dev/null 2>&1' >/etc/cron.d/kit-nginx-reload
|
||||
OPEN+=("443/tcp")
|
||||
local i
|
||||
@@ -892,7 +953,7 @@ setup_subscription() {
|
||||
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
|
||||
if [[ $TRUSTED == yes ]]; then
|
||||
# Наружу смотрит kit-sub (подписка с учётом приложения), 3X-UI — только на 127.0.0.1.
|
||||
# Наружу смотрит kit-sub (подписка с учётом приложения), 3X-UI – только на 127.0.0.1.
|
||||
SUB_PORT=2096; SUB_INTERNAL=2097
|
||||
# Без subURI панель показывает ссылку на внутренний порт 2097, до которого снаружи не достучаться.
|
||||
local uri="https://$HOST:$SUB_PORT$SUB_PATH"
|
||||
@@ -958,9 +1019,8 @@ UNIT
|
||||
|
||||
KIT_SUB_URL="$KIT_RAW/scripts/kit-sub.py"
|
||||
|
||||
install_kit_sub() {
|
||||
say "Ставлю подписку с учётом приложения (kit-sub)"
|
||||
apt-get install -y -qq python3 python3-yaml >/dev/null
|
||||
# Сам kit_sub.py: рядом со скриптом (запуск из папки scripts/) или из того же релиза.
|
||||
install_kit_sub_file() {
|
||||
install -d -m 755 /usr/local/lib/kit-sub /etc/kit-sub
|
||||
local src=${KIT_SUB_SRC:-}
|
||||
if [[ -z $src ]]; then
|
||||
@@ -970,6 +1030,12 @@ install_kit_sub() {
|
||||
if [[ -n $src ]]; then install -m 644 "$src" /usr/local/lib/kit-sub/kit_sub.py
|
||||
else curl -fsSL --retry 3 -o /usr/local/lib/kit-sub/kit_sub.py "$KIT_SUB_URL"; fi
|
||||
python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" /usr/local/lib/kit-sub/kit_sub.py || die "kit-sub скачался повреждённым"
|
||||
}
|
||||
|
||||
install_kit_sub() {
|
||||
say "Ставлю подписку с учётом приложения (kit-sub)"
|
||||
apt-get install -y -qq python3 python3-yaml >/dev/null
|
||||
install_kit_sub_file
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
# За nginx: слушаем только localhost, TLS снимает nginx на 443.
|
||||
jq -n --arg path "$SUB_PATH" --argjson port "${INNER[sub]}" --arg up "http://127.0.0.1:$SUB_INTERNAL" --arg host "$HOST" \
|
||||
@@ -994,40 +1060,231 @@ install_kit_sub() {
|
||||
[[ $SINGLE == yes ]] && kp=${INNER[sub]}
|
||||
for i in $(seq 1 20); do port_busy "$kp" tcp && return 0; sleep 1; done
|
||||
journalctl -u kit-sub -n 20 --no-pager >&2 || true
|
||||
die "kit-sub не запустился — лог выше."
|
||||
die "kit-sub не запустился – лог выше."
|
||||
}
|
||||
|
||||
# Ссылки пользователя — из его же подписки (её собирает сама 3X-UI).
|
||||
# Ссылки пользователя — прямо из подписки 3X-UI внутри сервера (мимо kit-sub, который
|
||||
# Ссылки пользователя – из его же подписки (её собирает сама 3X-UI).
|
||||
# Ссылки пользователя – прямо из подписки 3X-UI внутри сервера (мимо kit-sub, который
|
||||
# прячет от VPN-приложений vpn:// и tg://). sub_links subId [попыток]
|
||||
sub_links() {
|
||||
local id=$1 tries=${2:-20} raw="" i port=${SUB_INTERNAL:-$SUB_PORT} scheme=http
|
||||
[[ -z ${SUB_INTERNAL:-} && $TRUSTED == yes ]] && scheme=https
|
||||
for i in $(seq 1 "$tries"); do
|
||||
# Настоящий адрес в Host — 3X-UI подставит его в ссылки.
|
||||
# Настоящий адрес в Host – 3X-UI подставит его в ссылки.
|
||||
raw=$(curl -fsSk -m 10 -A "v2rayN/7.0" -H "Host: $HOST:$SUB_PORT" "$scheme://127.0.0.1:$port$SUB_PATH$id" 2>/dev/null) && [[ -n $raw ]] && break
|
||||
raw=""; sleep 2
|
||||
done
|
||||
if grep -q '://' <<<"$raw"; then echo "$raw"; else base64 -d <<<"$raw" 2>/dev/null || true; fi
|
||||
}
|
||||
|
||||
# ---------- восстановление из резервной копии (kit backup) ----------
|
||||
|
||||
# Файл настроек из копии можно подключать, только если в нём нет ничего, кроме
|
||||
# ИМЯ=значение без подстановок и команд.
|
||||
safe_env() {
|
||||
[[ -r $1 ]] || die "В копии нет файла ${1##*/}."
|
||||
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Меняет старый IP на новый в базе панели и файлах kit (только целиком, 1.2.3.4 не заденет 11.2.3.45).
|
||||
replace_host() { # старый новый файлы...
|
||||
python3 - "$@" <<'PY'
|
||||
import re, sqlite3, sys
|
||||
old, new, files = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
rx = re.compile(r"(?<![0-9.])" + re.escape(old) + r"(?![0-9])")
|
||||
sub = lambda v: rx.sub(new, v) if isinstance(v, str) else v
|
||||
db = sqlite3.connect("/etc/x-ui/x-ui.db")
|
||||
db.create_function("kit_sub", 1, sub)
|
||||
for table, cols in (("inbounds", ("settings", "stream_settings")), ("settings", ("value",))):
|
||||
for c in cols:
|
||||
db.execute("UPDATE %s SET %s = kit_sub(%s)" % (table, c, c))
|
||||
db.commit(); db.close()
|
||||
for f in files:
|
||||
s = open(f).read()
|
||||
open(f, "w").write(rx.sub(new, s))
|
||||
PY
|
||||
}
|
||||
|
||||
restore_main() { # файл
|
||||
local file=$1 tmp old_ip=""
|
||||
[[ -f $file ]] || die "Нет файла $file. Сначала скопируйте копию на этот сервер: scp kit-backup-….tar.gz root@IP:"
|
||||
[[ -d /usr/local/x-ui ]] && die "Восстанавливать можно только на чистый сервер, а здесь уже стоит 3X-UI."
|
||||
port_busy 443 tcp && die "Порт 443/tcp занят. Восстанавливать нужно на чистый VPS."
|
||||
|
||||
say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw, python3"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron python3 python3-yaml >/dev/null
|
||||
|
||||
# Сначала проверяем архив целиком: только наши пути, только файлы и папки, без ссылок
|
||||
# и «../». Чужой архив не должен ничего записать мимо.
|
||||
tmp=$(mktemp -d)
|
||||
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
||||
trap "rm -rf -- '$tmp'" EXIT
|
||||
python3 - "$file" "$tmp" <<'PY' || die "Это не резервная копия 3X-UI KIT или она повреждена. Ничего не менял."
|
||||
import os, sys, tarfile
|
||||
allowed = ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "etc/kit/kit.env", "etc/kit-sub/config.json",
|
||||
"etc/nginx/kit-stream.conf", "etc/nginx/conf.d/kit.conf", "var/www/kit", "root/cert/self",
|
||||
"root/cert/custom", "root/3x-ui.txt", "kit-backup.env")
|
||||
parents = {"etc", "etc/x-ui", "etc/kit", "etc/kit-sub", "etc/nginx", "etc/nginx/conf.d", "var", "var/www", "root", "root/cert"}
|
||||
with tarfile.open(sys.argv[1], "r:gz") as t:
|
||||
ms = []
|
||||
for m in t.getmembers():
|
||||
n = os.path.normpath(m.name)
|
||||
if n == ".":
|
||||
continue
|
||||
if n.startswith("/") or ".." in n.split("/"):
|
||||
sys.exit("путь " + m.name)
|
||||
if not (m.isfile() or m.isdir()):
|
||||
sys.exit("не файл " + m.name)
|
||||
if not (n in parents and m.isdir()) and not any(n == a or n.startswith(a + "/") for a in allowed):
|
||||
sys.exit("лишний " + m.name)
|
||||
m.name = n
|
||||
m.uid = m.gid = 0
|
||||
m.uname = m.gname = "root"
|
||||
m.mode = 0o700 if m.isdir() else 0o600
|
||||
ms.append(m)
|
||||
names = {m.name for m in ms}
|
||||
for need in ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "kit-backup.env"):
|
||||
if need not in names:
|
||||
sys.exit("нет " + need)
|
||||
if hasattr(tarfile, "data_filter"):
|
||||
t.extractall(sys.argv[2], members=ms, filter="data")
|
||||
else:
|
||||
t.extractall(sys.argv[2], members=ms)
|
||||
PY
|
||||
local f
|
||||
for f in "$tmp/kit-backup.env" "$tmp/etc/x-ui/install-result.env" "$tmp/etc/kit/kit.env"; do if [[ -f $f ]]; then safe_env "$f"; fi; done
|
||||
python3 -c "import sqlite3,sys; c=sqlite3.connect(sys.argv[1]); assert c.execute('PRAGMA integrity_check').fetchone()[0]=='ok'; c.execute('SELECT count(*) FROM inbounds')" \
|
||||
"$tmp/etc/x-ui/x-ui.db" 2>/dev/null || die "База панели в копии повреждена. Ничего не менял."
|
||||
|
||||
local BACKUP_HOST="" BACKUP_SSL="" BACKUP_DATE="" BACKUP_KIT_VERSION=""
|
||||
# shellcheck disable=SC1090
|
||||
. "$tmp/kit-backup.env"
|
||||
[[ $BACKUP_SSL =~ ^(ip|custom|none)$ ]] || die "В копии нет данных о сертификате."
|
||||
PANEL_SSL=$BACKUP_SSL
|
||||
# Домен переезжает вместе с сервером (поменяйте A-запись), IP – нет.
|
||||
if [[ $BACKUP_HOST =~ ^[0-9.]+$ ]]; then
|
||||
HOST=${HOST:-$(public_ip)}
|
||||
[[ -n $HOST ]] || die "Не удалось узнать внешний IP. Укажите его: --host 1.2.3.4"
|
||||
[[ $HOST != "$BACKUP_HOST" ]] && old_ip=$BACKUP_HOST
|
||||
else
|
||||
HOST=$BACKUP_HOST
|
||||
fi
|
||||
[[ $PANEL_SSL == ip ]] && port_busy 80 tcp && die "Для сертификата панели нужен свободный порт 80/tcp."
|
||||
# Конфиги nginx и kit-sub из архива не берём, а собираем заново: из копии – только
|
||||
# проверенные значения (путь подписки, порты, режим).
|
||||
SINGLE=no; SUB_PATH=""; SUB_INTERNAL=""; SUB_PORT=""
|
||||
if [[ -f $tmp/etc/kit/kit.env ]]; then
|
||||
SINGLE=$(. "$tmp/etc/kit/kit.env"; echo "${SINGLE:-no}")
|
||||
SUB_PATH=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_PATH:-}")
|
||||
SUB_INTERNAL=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_INTERNAL:-}")
|
||||
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ ]] \
|
||||
|| die "В копии странные настройки kit (kit.env) – не восстанавливаю."
|
||||
fi
|
||||
if [[ -f $tmp/etc/kit-sub/config.json ]]; then
|
||||
SUB_PORT=$(jq -r '.port' "$tmp/etc/kit-sub/config.json" 2>/dev/null || true)
|
||||
[[ $SUB_PORT =~ ^[0-9]{1,5}$ && -n $SUB_PATH ]] || die "В копии странные настройки подписки – не восстанавливаю."
|
||||
fi
|
||||
say "Копия от ${BACKUP_DATE:-?} (kit ${BACKUP_KIT_VERSION:-?}), сервер ${B}$HOST${N}"
|
||||
|
||||
# 3X-UI той же закреплённой версии, потом подменяем её базу на базу из копии:
|
||||
# подключения, ключи REALITY, пользователи и их подписки остаются прежними.
|
||||
install_xui "$(free_port)" "$(rand_str 18)" "$(rand_str 10)" "$(rand_str 20)"
|
||||
systemctl stop x-ui
|
||||
install -m 600 "$tmp/etc/x-ui/x-ui.db" /etc/x-ui/x-ui.db
|
||||
install -m 600 "$tmp/etc/x-ui/install-result.env" "$XUI_ENV"
|
||||
local c
|
||||
for c in self custom; do
|
||||
[[ -d $tmp/root/cert/$c ]] || continue
|
||||
install -d -m 700 "/root/cert/$c"
|
||||
install -m 644 "$tmp/root/cert/$c/fullchain.pem" "/root/cert/$c/fullchain.pem"
|
||||
install -m 600 "$tmp/root/cert/$c/privkey.pem" "/root/cert/$c/privkey.pem"
|
||||
done
|
||||
install -d -m 700 /etc/kit /etc/kit-sub
|
||||
[[ -f $tmp/etc/kit/kit.env ]] && install -m 600 "$tmp/etc/kit/kit.env" /etc/kit/kit.env
|
||||
[[ -f $tmp/root/3x-ui.txt ]] && install -m 600 "$tmp/root/3x-ui.txt" "$RESULT"
|
||||
if [[ -n $old_ip ]]; then
|
||||
say "Меняю адрес сервера в подключениях: $old_ip → $HOST"
|
||||
local files=()
|
||||
for f in "$XUI_ENV" /etc/kit/kit.env "$RESULT"; do if [[ -f $f ]]; then files+=("$f"); fi; done
|
||||
replace_host "$old_ip" "$HOST" "${files[@]}"
|
||||
fi
|
||||
systemctl start x-ui
|
||||
connect_panel
|
||||
set_xray_core
|
||||
|
||||
# Сертификат: Let's Encrypt на новый IP выпустил установщик, свой и самоподписанный – из копии.
|
||||
setup_tls_cert
|
||||
TRUSTED=no
|
||||
[[ $PANEL_SSL == ip || $PANEL_SSL == custom ]] && TRUSTED=yes
|
||||
if [[ -n $SUB_PORT ]]; then
|
||||
install_kit_sub
|
||||
[[ $SINGLE == yes ]] || OPEN+=("$SUB_PORT/tcp")
|
||||
fi
|
||||
# Всё на 443: nginx строит маршруты по подключениям из базы, заглушка – из копии.
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
install -d -m 755 /var/www/kit
|
||||
[[ -f $tmp/var/www/kit/index.html ]] && install -m 644 "$tmp/var/www/kit/index.html" /var/www/kit/index.html
|
||||
setup_nginx
|
||||
fi
|
||||
|
||||
if [[ -f /etc/kit/kit.env ]]; then
|
||||
install_kit_file
|
||||
brand_xui_menu
|
||||
/usr/local/bin/kit update --auto >/dev/null 2>&1 || warn "Автообновление не включилось – включите позже: kit update --auto"
|
||||
fi
|
||||
|
||||
# Порты – по подключениям из копии: что слушает не только localhost, то и открываем.
|
||||
if [[ $UFW == yes ]]; then
|
||||
local p
|
||||
while read -r p; do OPEN+=("$p"); done < <(api GET inbounds/list | jq -r '.[] | select(.listen != "127.0.0.1")
|
||||
| if (.protocol | test("^(hysteria|hysteria2|tuic|wireguard|amneziawg)$")) then "\(.port)/udp"
|
||||
elif .protocol == "shadowsocks" then "\(.port)/tcp", "\(.port)/udp" else "\(.port)/tcp" end')
|
||||
[[ $TRUSTED == yes && $SINGLE == no ]] && OPEN+=("$XUI_PANEL_PORT/tcp")
|
||||
[[ $PANEL_SSL == ip ]] && OPEN+=("80/tcp")
|
||||
setup_ufw
|
||||
fi
|
||||
|
||||
local n_in n_cl
|
||||
n_in=$(api GET inbounds/list | jq length)
|
||||
n_cl=$(api GET inbounds/list | jq '[.[] | (.settings | if type == "string" then fromjson else . end).clients // [] | .[].email] | unique | length')
|
||||
kit_banner
|
||||
echo
|
||||
echo "${G}${B}Готово! Сервер восстановлен из копии: $n_in подключений, $n_cl клиентских записей.${N}"
|
||||
echo "Логин и пароль панели прежние, адрес панели и подписки: ${B}cat $RESULT${N}"
|
||||
echo
|
||||
if [[ -n $old_ip ]]; then
|
||||
warn "IP сервера сменился: $old_ip → $HOST."
|
||||
echo "Старые подписки указывают на старый IP, поэтому клиентам нужно один раз добавить"
|
||||
echo "подписку заново: ${B}kit user list${N}, затем ${B}kit user link имя${N}."
|
||||
echo "${D}Чтобы в следующий раз переезд прошёл незаметно для клиентов, ставьте сервер на домен (--cert, --key, --host).${N}"
|
||||
else
|
||||
echo "Клиентам ничего менять не нужно: ключи, ссылки и подписки те же."
|
||||
fi
|
||||
[[ $PANEL_SSL == custom ]] && echo "Направьте A-запись домена ${B}$HOST${N} на IP этого сервера, если ещё не сделали."
|
||||
return 0
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
3X-UI со всеми протоколами одной командой
|
||||
|
||||
--protocols all all (по умолчанию — всё, кроме WireGuard), minimal (только REALITY)
|
||||
--protocols all all (по умолчанию – всё, кроме WireGuard), minimal (только REALITY)
|
||||
или список через запятую: reality,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto
|
||||
(обычный WireGuard в России блокируется — включайте его, только если сервер и
|
||||
(обычный WireGuard в России блокируется – включайте его, только если сервер и
|
||||
пользователи за границей)
|
||||
--port 443 порт REALITY (TCP) и Hysteria2 (UDP), по умолчанию 443
|
||||
--sni сайт сайт для маскировки (по умолчанию подбирается сам)
|
||||
--panel-ssl ip|none сертификат панели: ip — Let's Encrypt на IP (нужен порт 80),
|
||||
none — панель только через SSH-туннель (по умолчанию выбирается сам)
|
||||
--panel-ssl ip|none сертификат панели: ip – Let's Encrypt на IP (нужен порт 80),
|
||||
none – панель только через SSH-туннель (по умолчанию выбирается сам)
|
||||
--cert файл --key файл свой сертификат (например, для домена) вместо Let's Encrypt на IP;
|
||||
тогда --host — это домен из сертификата
|
||||
тогда --host – это домен из сертификата
|
||||
--user admin имя первого клиента
|
||||
--host 1.2.3.4 адрес в ссылке, если IP определился неверно
|
||||
--no-ufw не трогать файрвол
|
||||
--restore файл поднять сервер из резервной копии kit backup (на чистом VPS)
|
||||
-y не задавать вопросов
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hysteria2 одной командой — https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
# Hysteria2 одной командой – https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
#
|
||||
# Установка: bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/hysteria2.sh)
|
||||
# Управление: hy2 help
|
||||
@@ -43,11 +43,11 @@ 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
|
||||
trap 'die "Ошибка в строке $LINENO. Если это установка – исправьте причину и запустите скрипт ещё раз."' ERR
|
||||
|
||||
need_root() { [[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз."; }
|
||||
|
||||
# Прежнее имя файла настроек — переносим, чтобы старые установки не сломались.
|
||||
# Прежнее имя файла настроек – переносим, чтобы старые установки не сломались.
|
||||
if [[ -f $CONF_DIR/pinkman.env && ! -f $STATE && -w $CONF_DIR ]]; then
|
||||
mv "$CONF_DIR/pinkman.env" "$STATE"
|
||||
fi
|
||||
@@ -105,7 +105,7 @@ install_binary() {
|
||||
curl -fsSL --retry 3 -o "$tmp/hysteria" "$url/hysteria-linux-$arch"
|
||||
expected=${HY_SHA256[$arch]:-}
|
||||
actual=$(sha256sum "$tmp/hysteria" | awk '{print $1}')
|
||||
[[ -n $expected && $expected == "$actual" ]] || { rm -rf "$tmp"; die "Контрольная сумма не совпала — файл повреждён или подменён."; }
|
||||
[[ -n $expected && $expected == "$actual" ]] || { rm -rf "$tmp"; die "Контрольная сумма не совпала – файл повреждён или подменён."; }
|
||||
install -m 755 "$tmp/hysteria" "$BIN"
|
||||
rm -rf "$tmp"
|
||||
say "Контрольная сумма совпала: ${D}${actual:0:16}…${N}"
|
||||
@@ -146,7 +146,7 @@ render_config() {
|
||||
local u p
|
||||
. "$STATE"
|
||||
{
|
||||
echo "# Сгенерировано hy2 — правьте через команду hy2, ручные изменения перезапишутся."
|
||||
echo "# Сгенерировано hy2 – правьте через команду hy2, ручные изменения перезапишутся."
|
||||
echo "listen: :$PORT"
|
||||
echo
|
||||
if [[ -n ${DOMAIN:-} ]]; then
|
||||
@@ -212,9 +212,9 @@ EOF
|
||||
}
|
||||
|
||||
tune_sysctl() {
|
||||
# Буферы UDP побольше — рекомендация разработчиков Hysteria для скорости.
|
||||
# Буферы UDP побольше – рекомендация разработчиков Hysteria для скорости.
|
||||
printf 'net.core.rmem_max=16777216\nnet.core.wmem_max=16777216\n' >"$SYSCTL"
|
||||
sysctl -q -p "$SYSCTL" 2>/dev/null || warn "Не удалось применить sysctl (так бывает в контейнерах) — на работу не влияет."
|
||||
sysctl -q -p "$SYSCTL" 2>/dev/null || warn "Не удалось применить sysctl (так бывает в контейнерах) – на работу не влияет."
|
||||
}
|
||||
|
||||
setup_ufw() {
|
||||
@@ -230,7 +230,7 @@ setup_ufw() {
|
||||
ufw allow 80/tcp >/dev/null
|
||||
ufw allow "$PORT/tcp" >/dev/null
|
||||
fi
|
||||
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) — откройте порты у хостера вручную."
|
||||
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
|
||||
}
|
||||
|
||||
# Ждём строку «server up and running» в логе с момента запуска: с доменом
|
||||
@@ -250,7 +250,7 @@ start_and_wait() {
|
||||
if grep -qi 'acme\|certificate' <<<"$log"; then
|
||||
die "Не удалось получить сертификат. Проверьте, что A-запись домена указывает на этот сервер, а порт 80/tcp открыт у хостера. Потом: hy2 restart"
|
||||
fi
|
||||
die "Hysteria не запустилась — причина выше. После исправления: hy2 restart"
|
||||
die "Hysteria не запустилась – причина выше. После исправления: hy2 restart"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
@@ -281,11 +281,11 @@ cmd_install() {
|
||||
echo
|
||||
echo "${B}Установка Hysteria2${N}"
|
||||
echo "С доменом сертификат выпустит Let's Encrypt, а по адресу домена откроется сайт-заглушка."
|
||||
echo "Без домена будет самоподписанный сертификат — это тоже работает."
|
||||
read -rp "Домен (Enter — без домена): " DOMAIN
|
||||
echo "Без домена будет самоподписанный сертификат – это тоже работает."
|
||||
read -rp "Домен (Enter – без домена): " DOMAIN
|
||||
if [[ -n $DOMAIN ]]; then read -rp "Почта для Let's Encrypt: " EMAIL; fi
|
||||
fi
|
||||
# Значения попадают в install.env и конфиг Hysteria — пропускаем только допустимые символы.
|
||||
# Значения попадают в install.env и конфиг Hysteria – пропускаем только допустимые символы.
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$'
|
||||
if [[ -n $DOMAIN ]]; then
|
||||
[[ $DOMAIN =~ $re_host ]] || die "Похоже, это не домен: $DOMAIN"
|
||||
@@ -298,8 +298,8 @@ cmd_install() {
|
||||
|
||||
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 занят — на нём будет сайт-заглушка."
|
||||
port_busy 80 tcp && die "TCP-порт 80 занят – он нужен Let's Encrypt для проверки домена."
|
||||
port_busy "$PORT" tcp && die "TCP-порт $PORT занят – на нём будет сайт-заглушка."
|
||||
fi
|
||||
|
||||
install_packages
|
||||
@@ -310,7 +310,7 @@ cmd_install() {
|
||||
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-запись."
|
||||
[[ $resolved == "$my_ip" ]] || warn "Домен $DOMAIN указывает на $resolved, а IP сервера $my_ip. Если сертификат не выпустится – проверьте A-запись."
|
||||
fi
|
||||
SNI=${SNI:-${DOMAIN:-www.bing.com}}
|
||||
|
||||
@@ -371,7 +371,7 @@ show_link() {
|
||||
local link
|
||||
link=$(user_link "$1")
|
||||
echo
|
||||
echo "Ссылка для ${B}$1${N} — вставьте в Hiddify, v2rayN, Streisand или NekoBox:"
|
||||
echo "Ссылка для ${B}$1${N} – вставьте в Hiddify, v2rayN, Streisand или NekoBox:"
|
||||
echo
|
||||
echo "$link"
|
||||
echo
|
||||
@@ -406,7 +406,7 @@ cmd_del() {
|
||||
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 "Это последний пользователь — сначала добавьте другого."
|
||||
[[ $(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 удалён, его ссылка больше не работает."
|
||||
@@ -434,19 +434,41 @@ cmd_status() {
|
||||
systemctl --no-pager --lines=5 status hysteria-server || true
|
||||
}
|
||||
|
||||
# Открытый ключ, которым автор подписывает релизы (тот же, что в kit). Обновление ставится,
|
||||
# только если SHA256SUMS релиза подписан этим ключом и скрипт совпал с подписанным хешем.
|
||||
KIT_SIGNERS=(
|
||||
# KIT_SIGNER_KEY
|
||||
)
|
||||
KIT_SIG_NS="3x-ui-kit-release"
|
||||
KIT_SIG_ID="releases@3x-ui-kit"
|
||||
|
||||
cmd_update() {
|
||||
require_installed
|
||||
local tmp
|
||||
local latest
|
||||
local tmp latest raw sum k
|
||||
latest=$(curl -fsS -m 8 "$KIT_REPO_RAW/main/VERSION" | tr -d '[:space:]') || true
|
||||
[[ $latest =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "Не удалось узнать последнюю версию: GitHub недоступен с сервера."
|
||||
tmp=$(mktemp)
|
||||
say "Скачиваю скрипт версии $latest"
|
||||
curl -fsSL --retry 3 -o "$tmp" "$KIT_REPO_RAW/v$latest/scripts/hysteria2.sh"
|
||||
bash -n "$tmp" || { rm -f "$tmp"; die "Скачанный скрипт повреждён – ничего не менял."; }
|
||||
bash "$tmp" __update_binary
|
||||
install -m 755 "$tmp" "$CLI"
|
||||
rm -f "$tmp"
|
||||
[[ $latest != "$KIT_VERSION" && $(printf '%s\n%s\n' "$latest" "$KIT_VERSION" | sort -V | tail -1) == "$KIT_VERSION" ]] \
|
||||
&& die "На GitHub версия $latest старше вашей $KIT_VERSION – ничего не делаю."
|
||||
((${#KIT_SIGNERS[@]})) || die "В этой сборке нет ключа подписи – проверить обновление нечем."
|
||||
command -v ssh-keygen >/dev/null || apt-get install -y -qq openssh-client >/dev/null
|
||||
tmp=$(mktemp -d)
|
||||
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
||||
trap "rm -rf -- '$tmp'" EXIT
|
||||
raw="$KIT_REPO_RAW/v$latest"
|
||||
say "Скачиваю и проверяю подпись версии $latest"
|
||||
curl -fsSL --retry 3 -o "$tmp/SHA256SUMS" "$raw/SHA256SUMS" && curl -fsSL --retry 3 -o "$tmp/SHA256SUMS.sig" "$raw/SHA256SUMS.sig" \
|
||||
|| die "Не удалось скачать подпись релиза – ничего не менял."
|
||||
for k in "${KIT_SIGNERS[@]}"; do printf '%s namespaces="%s" %s\n' "$KIT_SIG_ID" "$KIT_SIG_NS" "$k" >>"$tmp/allowed_signers"; done
|
||||
ssh-keygen -Y verify -f "$tmp/allowed_signers" -I "$KIT_SIG_ID" -n "$KIT_SIG_NS" -s "$tmp/SHA256SUMS.sig" <"$tmp/SHA256SUMS" >/dev/null 2>&1 \
|
||||
|| die "Подпись релиза $latest не сошлась с ключом автора – это не наш релиз. Ничего не менял."
|
||||
grep -qx "# 3X-UI KIT $latest" "$tmp/SHA256SUMS" || die "Подписанный релиз не той версии – ничего не менял."
|
||||
sum=$(awk '$2 == "scripts/hysteria2.sh" {print $1}' "$tmp/SHA256SUMS")
|
||||
curl -fsSL --retry 3 -o "$tmp/hysteria2.sh" "$raw/scripts/hysteria2.sh" || die "Не удалось скачать скрипт – ничего не менял."
|
||||
[[ $sum =~ ^[0-9a-f]{64}$ && $(sha256sum "$tmp/hysteria2.sh" | awk '{print $1}') == "$sum" ]] \
|
||||
|| die "Скрипт не совпал с подписанным SHA256 – ничего не менял."
|
||||
bash -n "$tmp/hysteria2.sh" || die "Скачанный скрипт повреждён – ничего не менял."
|
||||
bash "$tmp/hysteria2.sh" __update_binary
|
||||
install -m 755 "$tmp/hysteria2.sh" "$CLI"
|
||||
}
|
||||
|
||||
cmd_update_binary() {
|
||||
@@ -485,7 +507,7 @@ cmd_uninstall() {
|
||||
|
||||
cmd_help() {
|
||||
cat <<EOF
|
||||
${B}hy2${N} — управление Hysteria2
|
||||
${B}hy2${N} – управление Hysteria2
|
||||
|
||||
hy2 add имя добавить пользователя и показать его ссылку
|
||||
hy2 del имя удалить пользователя
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Подписка с учётом приложения — посредник перед подпиской 3X-UI.
|
||||
"""Подписка с учётом приложения – посредник перед подпиской 3X-UI.
|
||||
|
||||
https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
|
||||
Слушает публичный адрес подписки (HTTPS) и ходит в подписку 3X-UI на 127.0.0.1:
|
||||
* Clash / Mihomo (Clash Verge, FlClash, Mihomo Party…) — конфиг 3X-UI плюс AmneziaWG
|
||||
* Clash / Mihomo (Clash Verge, FlClash, Mihomo Party…) – конфиг 3X-UI плюс AmneziaWG
|
||||
из подписки «<id>-awg»: Mihomo умеет AmneziaWG, а остальные приложения нет;
|
||||
* остальные приложения и браузер — ответ 3X-UI как есть (ссылки или страница);
|
||||
* заголовок Subscription-Userinfo: expire=0 («бессрочно») убирается — иначе
|
||||
* остальные приложения и браузер – ответ 3X-UI как есть (ссылки или страница);
|
||||
* заголовок Subscription-Userinfo: expire=0 («бессрочно») убирается – иначе
|
||||
приложения показывают срок «01.01.1970».
|
||||
|
||||
Настройки — /etc/kit-sub/config.json. Сертификат перечитывается сам после продления.
|
||||
Настройки – /etc/kit-sub/config.json. Сертификат перечитывается сам после продления.
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -64,14 +64,14 @@ def upstream(sub_id, ua, host, accept):
|
||||
|
||||
|
||||
def fix_userinfo(value):
|
||||
# «expire=0» значит «бессрочно», но приложения рисуют 01.01.1970 — убираем.
|
||||
# «expire=0» значит «бессрочно», но приложения рисуют 01.01.1970 – убираем.
|
||||
parts = [p.strip() for p in value.split(";") if p.strip() and p.strip() != "expire=0"]
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def strip_links(body):
|
||||
"""Список ссылок (base64 или текст) без vpn:// и tg:// — их не умеет ни одно VPN-приложение
|
||||
со ссылками: vpn:// — конфиг для AmneziaVPN, tg:// — прокси для Telegram."""
|
||||
"""Список ссылок (base64 или текст) без vpn:// и tg:// – их не умеет ни одно VPN-приложение
|
||||
со ссылками: vpn:// – конфиг для AmneziaVPN, tg:// – прокси для Telegram."""
|
||||
text = body.decode("utf-8", "replace").strip()
|
||||
encoded = "://" not in text
|
||||
if encoded:
|
||||
@@ -85,7 +85,7 @@ def strip_links(body):
|
||||
|
||||
|
||||
def strip_awg(clash_yaml):
|
||||
"""Clash-конфиг без AmneziaWG — для приложений, которые его не умеют."""
|
||||
"""Clash-конфиг без AmneziaWG – для приложений, которые его не умеют."""
|
||||
cfg = yaml.safe_load(clash_yaml)
|
||||
if not isinstance(cfg, dict):
|
||||
return clash_yaml
|
||||
@@ -110,7 +110,7 @@ def merge_awg(main_yaml, awg_yaml):
|
||||
return main_yaml
|
||||
names = {p.get("name") for p in main.get("proxies") or []}
|
||||
for p in extra:
|
||||
# 3X-UI дописывает к имени запись-«двойника» («AmneziaWG-3.1-sasha-awg») — убираем хвост.
|
||||
# 3X-UI дописывает к имени запись-«двойника» («AmneziaWG-3.1-sasha-awg») – убираем хвост.
|
||||
p["name"] = re.sub(r"-[^-\s]+-awg\d*$", "", p["name"]) or p["name"]
|
||||
base, n = p["name"], 2
|
||||
while p["name"] in names:
|
||||
@@ -133,7 +133,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
timeout = 20 # зависшие соединения не держим
|
||||
|
||||
def setup(self):
|
||||
# TLS-рукопожатие — в потоке запроса, а не в общем цикле приёма соединений.
|
||||
# TLS-рукопожатие – в потоке запроса, а не в общем цикле приёма соединений.
|
||||
# Без сертификата (за nginx, на 127.0.0.1) работаем по обычному HTTP.
|
||||
self.request.settimeout(self.timeout)
|
||||
if self.server.ssl_ctx is not None:
|
||||
@@ -177,13 +177,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
clash = bool(CLASH_UA.search(ua)) and "yaml" in headers.get("content-type", "")
|
||||
awg = clash and not NO_AWG_UA.search(ua)
|
||||
# В журнал — только приложение и что ему отдали, без IP.
|
||||
# В журнал – только приложение и что ему отдали, без IP.
|
||||
log(f"{ua[:80]!r} → {'clash+awg' if awg else 'clash' if clash else headers.get('content-type', '?').split(';')[0]}")
|
||||
try:
|
||||
if code == 200 and clash and not awg:
|
||||
body = strip_awg(body)
|
||||
elif code == 200 and awg and not sub_id.endswith(("-awg", "-tg")):
|
||||
# Установки до kit 1.1 держали AmneziaWG в подписке «<id>-awg» — подмешиваем её.
|
||||
# Установки до kit 1.1 держали AmneziaWG в подписке «<id>-awg» – подмешиваем её.
|
||||
acode, _, abody = upstream(sub_id + "-awg", ua, host, accept)
|
||||
if acode == 200 and abody:
|
||||
body = merge_awg(body, abody)
|
||||
@@ -208,7 +208,7 @@ class Server(http.server.ThreadingHTTPServer):
|
||||
daemon_threads = True
|
||||
ssl_ctx = None
|
||||
|
||||
def handle_error(self, request, client_address): # обрывы TLS от сканеров — не ошибка
|
||||
def handle_error(self, request, client_address): # обрывы TLS от сканеров – не ошибка
|
||||
pass
|
||||
address_family = socket.AF_INET6 if ":" in CONF.get("listen", "") else socket.AF_INET
|
||||
|
||||
@@ -228,7 +228,7 @@ def main():
|
||||
stamp = [os.path.getmtime(cert)]
|
||||
|
||||
def reload_cert():
|
||||
# Let's Encrypt на IP живёт 6 дней — после продления берём новый сертификат без перезапуска.
|
||||
# Let's Encrypt на IP живёт 6 дней – после продления берём новый сертификат без перезапуска.
|
||||
while True:
|
||||
time.sleep(600)
|
||||
try:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# kit — управление сервером 3X-UI KIT: пользователи и обновление.
|
||||
# kit – управление сервером 3X-UI KIT: пользователи, обновление, резервная копия.
|
||||
# https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
#
|
||||
# kit user add имя [--gb 50] [--days 30] [--devices 3]
|
||||
# kit user list | link имя | limit имя [--gb N] [--days N] | off имя | on имя | del имя
|
||||
# kit update | kit version
|
||||
# kit update [--auto | --manual] | kit backup | kit version
|
||||
|
||||
set -Eeuo pipefail
|
||||
export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам
|
||||
@@ -28,7 +28,7 @@ warn() { printf '%s\n' "${Y}!${N} $*" >&2; }
|
||||
die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз."
|
||||
[[ -f $XUI_ENV && -f $KIT_ENV ]] || die "Не найдена установка — сначала поставьте сервер скриптом 3x-ui.sh."
|
||||
[[ -f $XUI_ENV && -f $KIT_ENV ]] || die "Не найдена установка – сначала поставьте сервер скриптом 3x-ui.sh."
|
||||
# shellcheck disable=SC1090
|
||||
. "$XUI_ENV"; . "$KIT_ENV"
|
||||
|
||||
@@ -52,7 +52,7 @@ api() { # METHOD path [json]
|
||||
# В 3X-UI 3.x у клиента одна запись и в ней одна пара ключей WireGuard и один адрес. Если
|
||||
# клиент подключён к двум AmneziaWG, в подписку для обоих уходят ключ и адрес одного из них,
|
||||
# и второй сервер клиента не узнаёт (проверено 2026-09-27). Поэтому к первому AmneziaWG
|
||||
# подключаем основную запись, а ко второму — запись-«двойник» «имя-awg» с подпиской «<id>-awg»
|
||||
# подключаем основную запись, а ко второму – запись-«двойник» «имя-awg» с подпиской «<id>-awg»
|
||||
# (subId в 3X-UI обязан быть уникальным) и теми же лимитами; kit-sub подмешивает её в Clash.
|
||||
awg_ids() { api GET inbounds/list | jq -r '[.[] | select(.protocol == "amneziawg") | .id] | sort | .[]'; }
|
||||
non_awg_ids() { api GET inbounds/list | jq -c '[.[] | select(.protocol != "amneziawg") | .id]'; }
|
||||
@@ -98,13 +98,13 @@ show_link() { # имя subId
|
||||
local url
|
||||
url=$(sub_url "$2")
|
||||
echo
|
||||
echo "Подписка ${B}$1${N} — все протоколы одной ссылкой. Вставьте в Happ, Hiddify, Karing,"
|
||||
echo "Подписка ${B}$1${N} – все протоколы одной ссылкой. Вставьте в Happ, Hiddify, Karing,"
|
||||
echo "v2rayN, Clash Verge или FlClash:"
|
||||
echo
|
||||
echo "$url"
|
||||
echo
|
||||
command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$url"
|
||||
echo "${D}AmneziaVPN и Telegram: kit user link $1 --all — отдельные ссылки vpn:// и tg://${N}"
|
||||
echo "${D}AmneziaVPN и Telegram: kit user link $1 --all – отдельные ссылки vpn:// и tg://${N}"
|
||||
}
|
||||
|
||||
cmd_add() {
|
||||
@@ -167,13 +167,13 @@ cmd_list() {
|
||||
elif ((exp > 0 && exp < $(date +%s) * 1000)); then st="${Y}истёк${N}"
|
||||
elif ((total > 0 && used >= total)); then st="${Y}лимит${N}"
|
||||
else st="${G}активен${N}"; fi
|
||||
if ((last > 0)); then seen=$(date -d "@$((last / 1000))" '+%d.%m %H:%M'); else seen="—"; fi
|
||||
if ((last > 0)); then seen=$(date -d "@$((last / 1000))" '+%d.%m %H:%M'); else seen="–"; fi
|
||||
printf "%-18s %-22s %-14s %-19s %s\n" "$email" "$tr" "$till" "$st" "$seen"
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
# Меняет все записи пользователя (основную и «двойников» AmneziaWG), каждую — от её собственных данных.
|
||||
# Меняет все записи пользователя (основную и «двойников» AmneziaWG), каждую – от её собственных данных.
|
||||
update_user() { # имя jq-фильтр [аргументы jq...]
|
||||
local name=$1 filter=$2 e rec body
|
||||
shift 2
|
||||
@@ -197,14 +197,14 @@ cmd_limit() {
|
||||
esac
|
||||
done
|
||||
update_user "$name" "$f" --argjson gb "${gb:-0}" --argjson days "${days:-0}" --argjson dev "${dev:-0}"
|
||||
say "Лимиты $name обновлены (0 — без ограничений)."
|
||||
say "Лимиты $name обновлены (0 – без ограничений)."
|
||||
}
|
||||
|
||||
cmd_toggle() { # имя true|false
|
||||
valid_name "$1"
|
||||
[[ -n $(client "$1") ]] || die "Нет пользователя $1"
|
||||
update_user "$1" ".enable = \$v" --argjson v "$2"
|
||||
if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен — подписка и подключения не работают."; fi
|
||||
if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен – подписка и подключения не работают."; fi
|
||||
}
|
||||
|
||||
cmd_del() {
|
||||
@@ -222,12 +222,24 @@ cmd_del() {
|
||||
|
||||
# ---------- версия и обновление ----------
|
||||
|
||||
# Открытый ключ, которым автор подписывает релизы (ssh-keygen -Y sign). Закрытая часть
|
||||
# есть только у автора, поэтому подменить обновление, взломав один GitHub, не выйдет.
|
||||
# Новый ключ приходит только в релизе, подписанном старым.
|
||||
KIT_SIGNERS=(
|
||||
# KIT_SIGNER_KEY
|
||||
)
|
||||
KIT_SIG_NS="3x-ui-kit-release"
|
||||
KIT_SIG_ID="releases@3x-ui-kit"
|
||||
KIT_MANUAL=/etc/kit/manual-update
|
||||
KIT_UPDATE_LOG=/var/log/kit-update.log
|
||||
|
||||
xray_version() {
|
||||
local b
|
||||
for b in /usr/local/x-ui/bin/xray-linux-*; do [[ -x $b ]] && "$b" version 2>/dev/null | awk 'NR==1 {print "v" $2}'; return; done
|
||||
}
|
||||
remote_version() { curl -fsS -m "${1:-5}" "$KIT_RAW/VERSION" 2>/dev/null | tr -d '[:space:]'; }
|
||||
newer() { [[ $1 != "$2" && $(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1) == "$1" ]]; } # $1 новее $2?
|
||||
auto_enabled() { systemctl is-enabled -q kit-update.timer 2>/dev/null; }
|
||||
|
||||
# Раз в сутки узнаём последнюю версию (не дольше 3 секунд) и подсказываем обновиться.
|
||||
update_hint() {
|
||||
@@ -239,13 +251,19 @@ update_hint() {
|
||||
latest=$(cat "$KIT_LATEST" 2>/dev/null || true)
|
||||
if [[ -n $latest ]] && newer "$latest" "$KIT_VERSION"; then
|
||||
echo
|
||||
echo "${Y}↑ Доступна версия $latest${N} (у вас $KIT_VERSION). Обновить: ${B}kit update${N}"
|
||||
if auto_enabled; then
|
||||
echo "${Y}↑ Вышла версия $latest${N} (у вас $KIT_VERSION). Встанет сама этой ночью или сейчас: ${B}kit update${N}"
|
||||
else
|
||||
echo "${Y}↑ Доступна версия $latest${N} (у вас $KIT_VERSION). Обновить: ${B}kit update${N}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_version() {
|
||||
echo "3X-UI KIT $KIT_VERSION"
|
||||
echo "${D}панель 3X-UI $(/usr/local/x-ui/x-ui -v 2>/dev/null | head -1 || echo '?'), ядро Xray $(xray_version)${N}"
|
||||
if auto_enabled; then echo "${D}автообновление: включено (только подписанные релизы), журнал $KIT_UPDATE_LOG${N}"
|
||||
else echo "${D}автообновление: выключено, включить: kit update --auto${N}"; fi
|
||||
update_hint
|
||||
}
|
||||
|
||||
@@ -255,6 +273,31 @@ changelog_of() {
|
||||
index($0, v) == 1 { on = 1; t = substr($0, length(v) + 1); sub(/^[: ]+/, "", t); if (t != "") print t; next } on && /^## / { exit } on' | sed 's/\*\*//g; s/`//g' | grep -v '^[[:space:]]*$' | head -40 || true
|
||||
}
|
||||
|
||||
# Скачивает релиз $1 в каталог $2 и проверяет: подпись SHA256SUMS нашим ключом, версию
|
||||
# внутри подписанного файла и SHA256 каждого файла. Любое несовпадение – отказ.
|
||||
fetch_release() { # версия каталог
|
||||
local v=$1 d=$2 raw f sum k
|
||||
((${#KIT_SIGNERS[@]})) || { warn "В этой сборке kit нет ключа подписи – проверить обновление нечем."; return 1; }
|
||||
command -v ssh-keygen >/dev/null || { warn "Нет ssh-keygen (пакет openssh-client) – подпись не проверить."; return 1; }
|
||||
raw=$(kit_ref_raw "$v")
|
||||
curl -fsSL --retry 3 -o "$d/SHA256SUMS" "$raw/SHA256SUMS" && curl -fsSL --retry 3 -o "$d/SHA256SUMS.sig" "$raw/SHA256SUMS.sig" \
|
||||
|| { warn "Не удалось скачать подпись релиза $v."; return 1; }
|
||||
: >"$d/allowed_signers"
|
||||
for k in "${KIT_SIGNERS[@]}"; do printf '%s namespaces="%s" %s\n' "$KIT_SIG_ID" "$KIT_SIG_NS" "$k" >>"$d/allowed_signers"; done
|
||||
if ! ssh-keygen -Y verify -f "$d/allowed_signers" -I "$KIT_SIG_ID" -n "$KIT_SIG_NS" -s "$d/SHA256SUMS.sig" <"$d/SHA256SUMS" >/dev/null 2>&1; then
|
||||
warn "Подпись релиза $v не сошлась с ключом автора – это не наш релиз. Ничего не ставлю."
|
||||
return 1
|
||||
fi
|
||||
# Версия записана внутри подписанного файла: старый подписанный релиз под видом нового не пройдёт.
|
||||
grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }
|
||||
for f in scripts/kit.sh scripts/kit-sub.py; do
|
||||
sum=$(awk -v f="$f" '$2 == f {print $1}' "$d/SHA256SUMS")
|
||||
[[ $sum =~ ^[0-9a-f]{64}$ ]] || { warn "В подписанном списке нет $f."; return 1; }
|
||||
curl -fsSL --retry 3 -o "$d/${f##*/}" "$raw/$f" || { warn "Не удалось скачать $f."; return 1; }
|
||||
[[ $(sha256sum "$d/${f##*/}" | awk '{print $1}') == "$sum" ]] || { warn "$f не совпал с подписанным SHA256 – ничего не ставлю."; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
# Юнит kit-sub: без root (DynamicUser), конфиг и сертификат – через LoadCredential.
|
||||
# Сертификат Let's Encrypt на IP продлевается раз в несколько дней, поэтому при отдельном
|
||||
# порте kit-sub перезапускается раз в сутки и берёт свежий.
|
||||
@@ -292,37 +335,110 @@ WantedBy=multi-user.target
|
||||
UNIT
|
||||
}
|
||||
|
||||
# Подписка отвечает? Берём настоящего пользователя и спрашиваем kit-sub изнутри сервера.
|
||||
sub_ok() {
|
||||
local cfg=/etc/kit-sub/config.json port path scheme=http sid i
|
||||
port=$(jq -r '.port' "$cfg"); path=$(jq -r '.path' "$cfg")
|
||||
[[ -n $(jq -r '.cert // empty' "$cfg") ]] && scheme=https
|
||||
sid=$(clients 2>/dev/null | jq -r '[.[] | .subId // empty | select(. != "")][0] // empty' 2>/dev/null || true)
|
||||
for i in $(seq 1 10); do
|
||||
if [[ -n $sid ]]; then
|
||||
curl -fsSk -m 5 -o /dev/null -A "Happ/1.0" "$scheme://127.0.0.1:$port$path$sid" 2>/dev/null && return 0
|
||||
else
|
||||
# Пользователей нет – достаточно, что служба жива и слушает порт.
|
||||
systemctl is-active -q kit-sub && ss -Hltn "sport = :$port" | grep -q . && return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Сервер ещё не получил исправления 1.1 (например, kit обновили вручную из 1.0)?
|
||||
needs_migration() {
|
||||
[[ -f /etc/cron.d/kit-xui-menu ]] && return 0
|
||||
[[ -f /etc/systemd/system/kit-sub.service ]] && ! grep -q '^DynamicUser=yes' /etc/systemd/system/kit-sub.service && return 0
|
||||
[[ ! -f $KIT_MANUAL ]] && ! auto_enabled && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Автообновление: раз в сутки ночью со случайной задержкой, чтобы тысячи серверов не шли
|
||||
# на GitHub в одну минуту. Ставит только подписанные релизы и только kit и kit-sub.
|
||||
auto_on() {
|
||||
cat >/etc/systemd/system/kit-update.service <<UNIT
|
||||
[Unit]
|
||||
Description=3X-UI KIT: автообновление kit и kit-sub (только подписанные релизы)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/kit update --unattended
|
||||
StandardOutput=append:$KIT_UPDATE_LOG
|
||||
StandardError=append:$KIT_UPDATE_LOG
|
||||
UNIT
|
||||
cat >/etc/systemd/system/kit-update.timer <<'UNIT'
|
||||
[Unit]
|
||||
Description=3X-UI KIT: проверка обновлений раз в сутки
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
RandomizedDelaySec=3h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
UNIT
|
||||
rm -f "$KIT_MANUAL"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now kit-update.timer >/dev/null 2>&1
|
||||
}
|
||||
|
||||
auto_off() {
|
||||
systemctl disable --now kit-update.timer >/dev/null 2>&1 || true
|
||||
install -d -m 700 /etc/kit
|
||||
touch "$KIT_MANUAL"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
local force=${1:-} latest tmp
|
||||
local force="" unattended=no latest tmp
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--force) force=yes ;;
|
||||
--auto) auto_on; say "Автообновление включено: раз в сутки ночью, только подписанные релизы. Журнал: $KIT_UPDATE_LOG"; return ;;
|
||||
--manual) auto_off; say "Автообновление выключено. Обновляться вручную: kit update, включить снова: kit update --auto"; return ;;
|
||||
--unattended) unattended=yes ;;
|
||||
*) die "Неизвестный параметр: $1 (kit update [--force | --auto | --manual])" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
# Ночной запуск и ручной не должны встретиться.
|
||||
exec 9>/run/kit-update.lock
|
||||
flock -n 9 || die "Обновление уже идёт."
|
||||
[[ $unattended == yes ]] && echo "--- $(date '+%F %T') kit $KIT_VERSION: проверяю обновления"
|
||||
|
||||
latest=$(remote_version) || true
|
||||
[[ $latest =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "Не удалось узнать последнюю версию: GitHub недоступен с сервера. Попробуйте позже."
|
||||
echo "$latest" >"$KIT_LATEST"
|
||||
if ! newer "$latest" "$KIT_VERSION" && [[ $force != --force ]] && ! needs_migration; then
|
||||
# Откатить на старую версию нельзя даже с подписью: только вперёд или та же.
|
||||
newer "$KIT_VERSION" "$latest" && die "На GitHub версия $latest старше вашей $KIT_VERSION – ничего не делаю."
|
||||
if ! newer "$latest" "$KIT_VERSION" && [[ $force != yes ]] && ! needs_migration; then
|
||||
say "У вас последняя версия: $KIT_VERSION."
|
||||
echo "${D}Переустановить файлы kit той же версии: kit update --force${N}"
|
||||
[[ $unattended == yes ]] || echo "${D}Переустановить файлы kit той же версии: kit update --force${N}"
|
||||
return
|
||||
fi
|
||||
if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI KIT $KIT_VERSION → $latest"; fi
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' RETURN
|
||||
curl -fsSL --retry 3 -o "$tmp/kit.sh" "$(kit_ref_raw "$latest")/scripts/kit.sh" && bash -n "$tmp/kit.sh" \
|
||||
|| die "Не удалось скачать новую версию kit – сервер не тронут."
|
||||
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
||||
trap "rm -rf -- '$tmp'" EXIT
|
||||
fetch_release "$latest" "$tmp" || die "Сервер не тронут."
|
||||
bash -n "$tmp/kit.sh" && python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" "$tmp/kit-sub.py" \
|
||||
|| die "Файлы релиза не прошли проверку синтаксиса – сервер не тронут."
|
||||
|
||||
# Подписка kit-sub: ставим новую, при сбое возвращаем старую.
|
||||
# Подписка kit-sub: ставим новую, проверяем, что отвечает, иначе возвращаем старую.
|
||||
if [[ -f /usr/local/lib/kit-sub/kit_sub.py ]]; then
|
||||
curl -fsSL --retry 3 -o "$tmp/kit_sub.py" "$(kit_ref_raw "$latest")/scripts/kit-sub.py" \
|
||||
&& python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" "$tmp/kit_sub.py" \
|
||||
|| die "Не удалось скачать kit-sub – сервер не тронут."
|
||||
cp /usr/local/lib/kit-sub/kit_sub.py "$tmp/kit_sub.old"
|
||||
cp /etc/systemd/system/kit-sub.service "$tmp/kit-sub.service.old"
|
||||
install -m 644 "$tmp/kit_sub.py" /usr/local/lib/kit-sub/kit_sub.py
|
||||
install -m 644 "$tmp/kit-sub.py" /usr/local/lib/kit-sub/kit_sub.py
|
||||
# С 1.1 kit-sub работает без root: переписываем юнит под DynamicUser и LoadCredential.
|
||||
local c k
|
||||
c=$(jq -r '.cert // empty' /etc/kit-sub/config.json); k=$(jq -r '.key // empty' /etc/kit-sub/config.json)
|
||||
@@ -330,16 +446,16 @@ cmd_update() {
|
||||
[[ -n $c ]] && echo '19 4 * * * root systemctl restart kit-sub >/dev/null 2>&1' >/etc/cron.d/kit-sub-cert
|
||||
systemctl daemon-reload
|
||||
systemctl restart kit-sub
|
||||
sleep 3
|
||||
if systemctl is-active -q kit-sub; then
|
||||
say "Подписка kit-sub обновлена"
|
||||
sleep 2
|
||||
if sub_ok; then
|
||||
say "Подписка kit-sub обновлена и отвечает"
|
||||
else
|
||||
install -m 644 "$tmp/kit_sub.old" /usr/local/lib/kit-sub/kit_sub.py
|
||||
install -m 644 "$tmp/kit-sub.service.old" /etc/systemd/system/kit-sub.service
|
||||
rm -f /etc/cron.d/kit-sub-cert
|
||||
grep -q '^LoadCredential=cert.pem' "$tmp/kit-sub.service.old" || rm -f /etc/cron.d/kit-sub-cert
|
||||
systemctl daemon-reload
|
||||
systemctl restart kit-sub
|
||||
warn "Новая kit-sub не запустилась – вернул прежнюю. Лог: journalctl -u kit-sub -n 30"
|
||||
die "Новая подписка не ответила – вернул прежнюю, kit остался версии $KIT_VERSION. Лог: journalctl -u kit-sub -n 30"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -353,16 +469,67 @@ cmd_update() {
|
||||
fi
|
||||
# 1.0 ставил cron, который каждый день правил файлы x-ui, – убираем.
|
||||
rm -f /etc/cron.d/kit-xui-menu
|
||||
# Автообновление включено по умолчанию, пока его не выключили командой kit update --manual.
|
||||
[[ -f $KIT_MANUAL ]] || auto_enabled || { auto_on; say "Включил автообновление: kit update --manual, чтобы выключить"; }
|
||||
|
||||
# Через rename: bash дочитывает текущий kit по ходу работы, его файл трогать нельзя.
|
||||
install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit
|
||||
echo
|
||||
echo "${G}✓ Готово: 3X-UI KIT $latest.${N} Пользователи, ссылки и подписки не менялись."
|
||||
[[ $unattended == yes ]] && return
|
||||
local news
|
||||
news=$(changelog_of "$latest")
|
||||
if [[ -n $news ]]; then echo; echo "${B}Что нового в $latest${N}"; echo "$news"; fi
|
||||
}
|
||||
|
||||
# ---------- резервная копия ----------
|
||||
|
||||
# Всё, что нужно, чтобы поднять тот же сервер в другом месте: база панели (пользователи,
|
||||
# ключи, подключения), настройки kit и kit-sub, nginx, сайт-заглушка, свои сертификаты.
|
||||
# Сертификат Let's Encrypt на IP не берём: на новом сервере он выпускается заново.
|
||||
BACKUP_PATHS=(/etc/x-ui/install-result.env /etc/kit/kit.env /etc/kit-sub/config.json
|
||||
/etc/nginx/kit-stream.conf /etc/nginx/conf.d/kit.conf /var/www/kit /root/cert/self /root/cert/custom /root/3x-ui.txt)
|
||||
|
||||
cmd_backup() {
|
||||
local out tmp p ssl=none c
|
||||
out=/root/kit-backup-$(date +%Y%m%d-%H%M).tar.gz
|
||||
tmp=$(mktemp -d)
|
||||
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
||||
trap "rm -rf -- '$tmp'" EXIT
|
||||
install -d -m 700 "$tmp/etc/x-ui"
|
||||
# Снимок базы средствами SQLite: панель продолжает работать, копия целая.
|
||||
python3 - /etc/x-ui/x-ui.db "$tmp/etc/x-ui/x-ui.db" <<'PY' || die "Не удалось скопировать базу панели."
|
||||
import sqlite3, sys
|
||||
src = sqlite3.connect("file:%s?mode=ro" % sys.argv[1], uri=True)
|
||||
dst = sqlite3.connect(sys.argv[2])
|
||||
src.backup(dst)
|
||||
dst.close(); src.close()
|
||||
PY
|
||||
for p in "${BACKUP_PATHS[@]}"; do if [[ -e $p ]]; then cp -a --parents "$p" "$tmp"; fi; done
|
||||
# Какой сертификат был у панели и подписки – новый сервер должен получить такой же.
|
||||
c=$(jq -r '.cert // empty' /etc/kit-sub/config.json 2>/dev/null || true)
|
||||
[[ -z $c && -f /etc/nginx/conf.d/kit.conf ]] && c=$(awk '$1 == "ssl_certificate" {sub(/;$/, "", $2); print $2; exit}' /etc/nginx/conf.d/kit.conf)
|
||||
case $c in
|
||||
/root/cert/ip/*) ssl=ip ;;
|
||||
/root/cert/custom/*) ssl=custom ;;
|
||||
esac
|
||||
{
|
||||
printf 'BACKUP_KIT_VERSION=%q\n' "$KIT_VERSION"
|
||||
printf 'BACKUP_HOST=%q\n' "$HOST"
|
||||
printf 'BACKUP_SSL=%q\n' "$ssl"
|
||||
printf 'BACKUP_DATE=%q\n' "$(date +%F)"
|
||||
} >"$tmp/kit-backup.env"
|
||||
(umask 077; tar -czf "$out" -C "$tmp" .)
|
||||
chmod 600 "$out"
|
||||
say "Резервная копия: ${B}$out${N} ($(du -h "$out" | cut -f1))"
|
||||
echo
|
||||
echo "В ней ключи и пароли от сервера, храните её как пароль. Скачать к себе (на компьютере):"
|
||||
echo " ${B}scp root@$HOST:$out .${N}"
|
||||
echo
|
||||
echo "Поднять сервер из копии на новом VPS:"
|
||||
echo " ${B}bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh) --restore ${out##*/}${N}"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${B}kit${N} $KIT_VERSION – управление сервером 3X-UI KIT
|
||||
@@ -376,7 +543,9 @@ ${B}kit${N} $KIT_VERSION – управление сервером 3X-UI KIT
|
||||
kit user del имя удалить
|
||||
|
||||
Сервер:
|
||||
kit update обновить kit и подписку kit-sub (пользователи и ссылки не меняются)
|
||||
kit update обновить kit и подписку kit-sub сейчас (пользователи и ссылки не меняются)
|
||||
kit update --manual выключить автообновление (--auto – включить обратно)
|
||||
kit backup резервная копия для переезда на другой сервер
|
||||
kit version версия kit, панели и ядра
|
||||
EOF
|
||||
}
|
||||
@@ -389,7 +558,8 @@ case "${1:-} ${2:-}" in
|
||||
"user off") cmd_toggle "${3:-}" false ;;
|
||||
"user on") cmd_toggle "${3:-}" true ;;
|
||||
"user del") shift 2; cmd_del "$@" ;;
|
||||
"update "*) cmd_update "${2:-}" ;;
|
||||
"update "*) shift; cmd_update "$@" ;;
|
||||
"backup "*) cmd_backup ;;
|
||||
"version "*|"--version "*|"-v "*) cmd_version ;;
|
||||
*) usage; update_hint ;;
|
||||
esac
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Проверяет настоящим подключением, что каждый протокол 3X-UI работает с каждым клиентом:
|
||||
Xray, Mihomo, sing-box и официальным клиентом AmneziaWG. Прогонять на каждой новой версии 3X-UI и Xray.
|
||||
|
||||
1. Сервер: чистый Ubuntu в Docker с systemd, на нём `scripts/3x-ui.sh`, затем `mk-inbounds.sh` —
|
||||
1. Сервер: чистый Ubuntu в Docker с systemd, на нём `scripts/3x-ui.sh`, затем `mk-inbounds.sh` –
|
||||
создаёт через API подключения VLESS XHTTP, WS+TLS, Trojan gRPC, VMess, Shadowsocks 2022,
|
||||
Hysteria2, TUIC, WireGuard, AmneziaWG и MTProto.
|
||||
2. Ссылки: `GET /panel/api/inbounds/allLinks` → `links.txt`.
|
||||
@@ -22,29 +22,29 @@ Xray, Mihomo, sing-box и официальным клиентом AmneziaWG. П
|
||||
|---|---|---|---|
|
||||
| VLESS REALITY | ✓ | ✓ | ✓ |
|
||||
| Hysteria2 | ✓ | ✓ | ✓ |
|
||||
| VLESS XHTTP + REALITY | ✓ | ✓ | — |
|
||||
| VLESS XHTTP + REALITY | ✓ | ✓ | – |
|
||||
| VLESS WS + TLS | ✓ | ✓ | ✓ |
|
||||
| Trojan gRPC + TLS | ✓ | ✓ | ✓ |
|
||||
| VMess WS + TLS | ✓ | ✓ | ✓ |
|
||||
| Shadowsocks 2022 | ✓ | ✓ | ✓ |
|
||||
| TUIC v5 | — | ✓ | ✓ |
|
||||
| WireGuard | ✓ | ✓ | ✓ | (локально; через DPI в России — ✗ у всех клиентов) |
|
||||
| AmneziaWG (классика) | — | ✓ | — |
|
||||
| AmneziaWG 3.1 | — | ✓ | — |
|
||||
| TUIC v5 | – | ✓ | ✓ |
|
||||
| WireGuard | ✓ | ✓ | ✓ | (локально; через DPI в России – ✗ у всех клиентов) |
|
||||
| AmneziaWG (классика) | – | ✓ | – |
|
||||
| AmneziaWG 3.1 | – | ✓ | – |
|
||||
|
||||
Подписка в формате Clash (User-Agent Clash Verge) — все 11 прокси ✓ в Mihomo.
|
||||
Подписка в формате Clash (User-Agent Clash Verge) – все 11 прокси ✓ в Mihomo.
|
||||
Официальный клиент AmneziaWG 3.1 подключается к обоим вариантам AmneziaWG.
|
||||
MTProto: `mtg doctor` — все дата-центры Telegram доступны.
|
||||
Со своим (самоподписанным) сертификатом подписка Clash от 3X-UI не передаёт Mihomo отпечаток —
|
||||
MTProto: `mtg doctor` – все дата-центры Telegram доступны.
|
||||
Со своим (самоподписанным) сертификатом подписка Clash от 3X-UI не передаёт Mihomo отпечаток –
|
||||
TLS-протоколы в ней не работают; ссылки с `pcs` работают.
|
||||
|
||||
## Через интернет к серверу в России, 2026-09-27
|
||||
|
||||
Сервер Ubuntu 24.04 в России, `scripts/3x-ui.sh` с настоящим сертификатом Let's Encrypt на IP,
|
||||
клиенты — на Mac через интернет. Подписка-ссылки и подписка Clash: все протоколы ✓, кроме:
|
||||
клиенты – на Mac через интернет. Подписка-ссылки и подписка Clash: все протоколы ✓, кроме:
|
||||
|
||||
- **WireGuard** — ✗ у Xray, Mihomo и sing-box: рукопожатие доходит до сервера, ответ режет DPI.
|
||||
- **WireGuard** – ✗ у Xray, Mihomo и sing-box: рукопожатие доходит до сервера, ответ режет DPI.
|
||||
Поэтому из набора по умолчанию убран.
|
||||
- **MTProto** — сервер хостера не достаёт до Telegram, прокси бесполезен; установщик теперь
|
||||
- **MTProto** – сервер хостера не достаёт до Telegram, прокси бесполезен; установщик теперь
|
||||
проверяет это и пропускает MTProto.
|
||||
- AmneziaWG (оба варианта) — ✓ в Mihomo и в официальном клиенте AmneziaWG.
|
||||
- AmneziaWG (оба варианта) – ✓ в Mihomo и в официальном клиенте AmneziaWG.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# awg-set.sh classic|full — выставляет обфускацию AmneziaWG на подключении и печатает ссылку.
|
||||
# awg-set.sh classic|full – выставляет обфускацию AmneziaWG на подключении и печатает ссылку.
|
||||
. /etc/x-ui/install-result.env
|
||||
API="http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"; H=(-H "Authorization: Bearer $XUI_API_TOKEN" -H 'Content-Type: application/json')
|
||||
ID=$(curl -fsS "${H[@]}" "$API/inbounds/list" | jq -r '.obj[] | select(.protocol=="amneziawg") | .id')
|
||||
|
||||
@@ -7,7 +7,7 @@ const { proxies } = L.parseText(fs.readFileSync(linksFile, 'utf8'));
|
||||
fs.rmSync(out, { recursive: true, force: true });
|
||||
const rows = [];
|
||||
proxies.filter((p) => p.type !== 'mtproto').forEach((p, i) => {
|
||||
// Тестовый сервер с самоподписанным сертификатом — разрешаем клиентам его принять.
|
||||
// Тестовый сервер с самоподписанным сертификатом – разрешаем клиентам его принять.
|
||||
if (PIN && p.tls && p.tls.security === "tls") { p.tls.insecure = true; p.tls.pin = PIN; }
|
||||
if (PIN && (p.type === "hysteria2" || p.type === "tuic")) p.insecure = true;
|
||||
if (PIN && p.type === "hysteria2") p.pinSHA256 = PIN;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Прогон матрицы: каждый конфиг — отдельный клиент, через него открываем сайт.
|
||||
# Прогон матрицы: каждый конфиг – отдельный клиент, через него открываем сайт.
|
||||
cd /lab/cases
|
||||
t() { # client port cmd...
|
||||
local port=$1; shift
|
||||
@@ -16,7 +16,7 @@ t() { # client port cmd...
|
||||
printf '%-32s %-40s %-40s %s\n' "протокол" "Xray 26.6.27" "Mihomo" "sing-box"
|
||||
for d in */; do
|
||||
d=${d%/}
|
||||
x='—'; m='—'; s='—'
|
||||
x='–'; m='–'; s='–'
|
||||
[[ -f $d/xray.json ]] && x=$(t 1080 /cl/xray run -c $d/xray.json)
|
||||
[[ -f $d/mihomo.yaml ]] && m=$(t 1081 /cl/mihomo -d /tmp/mh -f $d/mihomo.yaml)
|
||||
[[ -f $d/singbox.json ]] && s=$(t 1082 /cl/sing-box run -c $d/singbox.json)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Разбор ссылок vless:// vmess:// trojan:// ss:// hy2:// в общий формат.
|
||||
// Работает в браузере (window.PM) и в Node (module.exports). Всё локально —
|
||||
// Работает в браузере (window.PM) и в Node (module.exports). Всё локально –
|
||||
// ссылки никуда не отправляются.
|
||||
(function (root) {
|
||||
'use strict';
|
||||
@@ -78,7 +78,7 @@
|
||||
s.fp = q.get('fp') || (security === 'reality' ? 'chrome' : '');
|
||||
s.alpn = splitList(q.get('alpn'));
|
||||
s.insecure = truthy(q.get('allowInsecure')) || truthy(q.get('insecure'));
|
||||
// Отпечаток сертификата: pcs — имя параметра в ссылках Xray 26, pinSHA256 — в ссылках Hysteria.
|
||||
// Отпечаток сертификата: pcs – имя параметра в ссылках Xray 26, pinSHA256 – в ссылках Hysteria.
|
||||
s.pin = (q.get('pcs') || q.get('pinSHA256') || '').replace(/:/g, '').toLowerCase();
|
||||
}
|
||||
if (security === 'reality') {
|
||||
@@ -228,7 +228,7 @@
|
||||
}, hp);
|
||||
}
|
||||
|
||||
// Конфиг wg-quick / AmneziaWG: [Interface] + [Peer]. Параметры Jc…H4 — обфускация AmneziaWG.
|
||||
// Конфиг wg-quick / AmneziaWG: [Interface] + [Peer]. Параметры Jc…H4 – обфускация AmneziaWG.
|
||||
function parseWgConf(text, name) {
|
||||
const sec = {};
|
||||
let cur = null;
|
||||
@@ -261,7 +261,7 @@
|
||||
function parseVpn(link) {
|
||||
let text;
|
||||
try { text = b64decode(link.slice('vpn://'.length).split('#')[0]); } catch (e) { throw new Error('ссылка vpn:// повреждена'); }
|
||||
if (!/\[Interface\]/.test(text)) throw new Error('формат vpn:// не поддерживается — нужен конфиг AmneziaWG');
|
||||
if (!/\[Interface\]/.test(text)) throw new Error('формат vpn:// не поддерживается – нужен конфиг AmneziaWG');
|
||||
return parseWgConf(text, dec(link.split('#')[1] || ''));
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
function parseText(text) {
|
||||
let t = (text || '').trim();
|
||||
if (t && !/:\/\//.test(t) && looksBase64(t)) {
|
||||
try { t = b64decode(t); } catch (e) { /* не base64 — разберём как есть */ }
|
||||
try { t = b64decode(t); } catch (e) { /* не base64 – разберём как есть */ }
|
||||
}
|
||||
const ok = [];
|
||||
const errors = [];
|
||||
@@ -303,7 +303,7 @@
|
||||
errors.push({ line: i + 1, text: line.length > 60 ? line.slice(0, 57) + '…' : line, error: e.message });
|
||||
}
|
||||
});
|
||||
// Уникальные имена — клиенты требуют.
|
||||
// Уникальные имена – клиенты требуют.
|
||||
const seen = {};
|
||||
ok.forEach((p) => {
|
||||
const base = p.name;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
// Сервисы: какие наборы правил отправлять через прокси.
|
||||
const SERVICES = [
|
||||
{ id: 'blocked', group: 'Заблокированное', label: 'Заблокированное в России', hint: 'список re:filter — основные заблокированные сайты', on: true,
|
||||
{ id: 'blocked', group: 'Заблокированное', label: 'Заблокированное в России', hint: 'список re:filter – основные заблокированные сайты', on: true,
|
||||
sets: [['refilter@domain', REFILTER + 'domain-rule.mrs', 'domain'], ['refilter@ipcidr', REFILTER + 'ip-rule.mrs', 'ipcidr']] },
|
||||
{ id: 'youtube', label: 'YouTube', on: true, sets: [['youtube@domain', META + 'geosite/youtube.mrs', 'domain']] },
|
||||
{ id: 'telegram', label: 'Telegram', on: true,
|
||||
@@ -142,7 +142,7 @@
|
||||
o['pre-shared-key'] = p.preSharedKey || undefined; o.mtu = p.mtu;
|
||||
o.reserved = p.reserved || undefined;
|
||||
// Параметры AmneziaWG в именах Mihomo. Для 3.1 (есть ключ защиты заголовков)
|
||||
// обязателен version: 3 — без него Mihomo не подключается к такому серверу.
|
||||
// обязателен 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',
|
||||
@@ -194,7 +194,8 @@
|
||||
'external-controller': '0.0.0.0:9090',
|
||||
secret: opts.secret || randomSecret(),
|
||||
'external-ui': 'zashboard',
|
||||
'external-ui-url': 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip',
|
||||
// Версия панели закреплена: «latest» поставил бы на роутер то, что мы не проверяли.
|
||||
'external-ui-url': 'https://github.com/Zephyruso/zashboard/releases/download/v3.29.1/dist.zip',
|
||||
sniffer: {
|
||||
enable: true,
|
||||
sniff: { HTTP: { __flow: 1, ports: [80, 8080] }, TLS: { __flow: 1, ports: [443, 8443] }, QUIC: { __flow: 1, ports: [443, 8443] } },
|
||||
@@ -221,7 +222,7 @@
|
||||
const target = {};
|
||||
services.forEach((s) => {
|
||||
if (opts.perService) {
|
||||
// В правилах запятая — разделитель, поэтому имя группы без запятых.
|
||||
// В правилах запятая – разделитель, поэтому имя группы без запятых.
|
||||
const g = (s.group || s.label).replace(/,/g, '');
|
||||
groups.push({ name: g, type: 'select', proxies: [PROXY, 'DIRECT'], 'include-all': true });
|
||||
target[s.id] = g;
|
||||
@@ -253,7 +254,7 @@
|
||||
|
||||
const head = '# Сгенерировано: https://itsnotkubrick.github.io/3X-UI_KIT/tools/mihomo/\n' +
|
||||
'# Файл для XKeen: /opt/etc/mihomo/config.yaml, затем xkeen -restart\n' +
|
||||
'# Панель управления: http://IP-роутера:9090/ui (секрет — поле secret ниже)\n';
|
||||
'# Панель управления: http://IP-роутера:9090/ui (секрет – поле secret ниже)\n';
|
||||
return { yaml: head + yaml(cfg).replace(/^\n/, ''), config: cfg, count: proxies.length };
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
throw new Error('sing-box не поддерживает транспорт ' + t.network);
|
||||
}
|
||||
|
||||
// Возвращает { outbound } или { endpoint } (WireGuard в sing-box — endpoint).
|
||||
// Возвращает { outbound } или { endpoint } (WireGuard в sing-box – endpoint).
|
||||
function convert(p) {
|
||||
const base = { tag: p.name, server: p.server, server_port: p.port };
|
||||
switch (p.type) {
|
||||
@@ -45,7 +45,7 @@
|
||||
case 'ss':
|
||||
return { outbound: Object.assign(base, { type: 'shadowsocks', method: p.method, password: p.password }) };
|
||||
case 'hysteria2': {
|
||||
// sing-box не умеет закреплять отпечаток сертификата — при pinSHA256 проверку отключаем.
|
||||
// sing-box не умеет закреплять отпечаток сертификата – при pinSHA256 проверку отключаем.
|
||||
const t = clean({ enabled: true, server_name: p.sni, alpn: p.alpn, insecure: (p.insecure || !!p.pinSHA256) || undefined });
|
||||
const o = clean(Object.assign(base, { type: 'hysteria2', password: p.password, tls: t }));
|
||||
if (p.obfs) o.obfs = { type: p.obfs, password: p.obfsPassword };
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// opts: { inbound: {type:'mixed', port} } — для тестов и десктопных клиентов.
|
||||
// opts: { inbound: {type:'mixed', port} } – для тестов и десктопных клиентов.
|
||||
function build(proxies, opts) {
|
||||
opts = opts || {};
|
||||
const outbounds = [], endpoints = [], skipped = [];
|
||||
|
||||
@@ -71,9 +71,9 @@
|
||||
peers: [clean({ publicKey: p.publicKey, preSharedKey: p.preSharedKey, endpoint: p.server + ':' + p.port, allowedIPs: ['0.0.0.0/0', '::/0'] })] }),
|
||||
};
|
||||
default:
|
||||
throw new Error({ tuic: 'TUIC не поддерживается Xray — возьмите генератор Mihomo',
|
||||
amneziawg: 'AmneziaWG не поддерживается Xray — возьмите генератор Mihomo',
|
||||
mtproto: 'MTProto — это прокси для Telegram, в Xray он не нужен' }[p.type] || 'протокол ' + p.type + ' не поддерживается Xray');
|
||||
throw new Error({ tuic: 'TUIC не поддерживается Xray – возьмите генератор Mihomo',
|
||||
amneziawg: 'AmneziaWG не поддерживается Xray – возьмите генератор Mihomo',
|
||||
mtproto: 'MTProto – это прокси для Telegram, в Xray он не нужен' }[p.type] || 'протокол ' + p.type + ' не поддерживается Xray');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
{ id: 'discord', label: 'Discord', on: true, zkeen: { i: ['discord'] }, v2fly: { d: ['discord'] } },
|
||||
{ id: 'twitter', label: 'X (Twitter)', on: true, v2fly: { d: ['twitter'] } },
|
||||
{ id: 'ai', label: 'ChatGPT, Claude и другие ИИ', on: true, v2fly: { d: ['category-ai-!cn'] } },
|
||||
{ id: 'cdn', label: 'Зарубежные CDN и хостинги', hint: 'Cloudflare, Hetzner, DigitalOcean… — их часто замедляют', on: true,
|
||||
{ id: 'cdn', label: 'Зарубежные CDN и хостинги', hint: 'Cloudflare, Hetzner, DigitalOcean… – их часто замедляют', on: true,
|
||||
zkeen: { i: ['akamai', 'amazon', 'arelion', 'azure', 'bunnycdn', 'cdn77', 'cloudflare', 'cogent', 'colocrossing', 'contabo',
|
||||
'datacamp', 'digitalocean', 'fastly', 'frantech', 'gcore', 'hetzner', 'leaseweb', 'linode', 'liquidweb', 'mega', 'melbicom',
|
||||
'oracle', 'ovh', 'scaleway', 'vodafone', 'vultr'] } },
|
||||
@@ -128,7 +128,7 @@
|
||||
}));
|
||||
const uniq = (a) => Array.from(new Set(a));
|
||||
|
||||
// Политика xkeen_full: всё с этих входов — через прокси (как в стандартном конфиге XKeen).
|
||||
// Политика xkeen_full: всё с этих входов – через прокси (как в стандартном конфиге XKeen).
|
||||
const rules = [{ inboundTag: ['force-proxy-redirect', 'force-proxy-tproxy'], outboundTag: proxyTag }];
|
||||
const dd = domains(opts.directDomains), pd = domains(opts.proxyDomains);
|
||||
if (dd.length) rules.push({ domain: dd.map((d) => 'domain:' + d), outboundTag: 'direct' });
|
||||
@@ -157,7 +157,7 @@
|
||||
const warnings = [];
|
||||
proxies.forEach((p) => {
|
||||
const insecure = (p.tls && p.tls.insecure && !p.tls.pin) || (p.type === 'hysteria2' && p.insecure && !p.pinSHA256);
|
||||
if (insecure) warnings.push(p.name + ': в ссылке отключена проверка сертификата, а Xray 26 так не умеет — нужен отпечаток (pcs) или настоящий сертификат');
|
||||
if (insecure) warnings.push(p.name + ': в ссылке отключена проверка сертификата, а Xray 26 так не умеет – нужен отпечаток (pcs) или настоящий сертификат');
|
||||
try {
|
||||
const tag = outs.length === 0 ? mainTag : mainTag + '-' + (outs.length + 1);
|
||||
outs.push(outbound(p, tag));
|
||||
@@ -167,7 +167,7 @@
|
||||
});
|
||||
if (!outs.length) throw new Error(skipped.length ? skipped[0].error : 'Добавьте хотя бы одну ссылку.');
|
||||
|
||||
// Несколько серверов — балансировщик по пингу выбирает лучший.
|
||||
// Несколько серверов – балансировщик по пингу выбирает лучший.
|
||||
const multi = outs.length > 1;
|
||||
let obs = null;
|
||||
const proxyTarget = mainTag;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Генератор Mihomo для XKeen</title>
|
||||
<meta name="description" content="Готовый config.yaml Mihomo для XKeen из ссылок vless, hy2, trojan, ss или подписки — прямо в браузере">
|
||||
<meta name="description" content="Готовый config.yaml Mihomo для XKeen из ссылок vless, hy2, trojan, ss или подписки – прямо в браузере">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🕶️</text></svg>">
|
||||
<link rel="stylesheet" href="../lib/ui.css">
|
||||
</head>
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
<main>
|
||||
<h1>Конфиг Mihomo для XKeen</h1>
|
||||
<p class="lead">Ссылки на серверы или подписка — и готовый <code>config.yaml</code> с правилами для нужных сервисов,
|
||||
<p class="lead">Ссылки на серверы или подписка – и готовый <code>config.yaml</code> с правилами для нужных сервисов,
|
||||
автовыбором быстрого сервера и веб-панелью. Плюс команда, которая сама положит его на роутер.</p>
|
||||
<div class="privacy">🔒 <span>Всё считается <b>в вашем браузере</b> — ссылки никуда не отправляются</span></div>
|
||||
<div class="privacy">🔒 <span>Всё считается <b>в вашем браузере</b> – ссылки никуда не отправляются</span></div>
|
||||
|
||||
<section class="step">
|
||||
<h2><span class="num">1</span>Серверы</h2>
|
||||
@@ -36,10 +36,10 @@
|
||||
<div class="grid" id="services"></div>
|
||||
<div class="sub">Дополнительно</div>
|
||||
<div class="grid">
|
||||
<label class="opt"><input type="checkbox" id="perService"><span>Свой выбор сервера для каждого сервиса<small>в панели можно пустить YouTube через один сервер, а Telegram — через другой</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="perService"><span>Свой выбор сервера для каждого сервиса<small>в панели можно пустить YouTube через один сервер, а Telegram – через другой</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="blockQuic" checked><span>Блокировать QUIC<small>видео через прокси грузится стабильнее</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="blockAds"><span>Блокировать рекламу</span></label>
|
||||
<label class="opt"><input type="checkbox" id="finalProxy"><span>Всё остальное — тоже через прокси<small>иначе остальное идёт напрямую</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="finalProxy"><span>Всё остальное – тоже через прокси<small>иначе остальное идёт напрямую</small></span></label>
|
||||
</div>
|
||||
<div class="two">
|
||||
<div><label class="field" for="proxyDomains">Свои сайты через прокси</label>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<section class="step">
|
||||
<h2><span class="num">3</span>Готовый конфиг</h2>
|
||||
<div id="result"><div class="empty">Добавьте ссылку или подписку в шаг 1 — здесь появится конфиг.</div></div>
|
||||
<div id="result"><div class="empty">Добавьте ссылку или подписку в шаг 1 – здесь появится конфиг.</div></div>
|
||||
</section>
|
||||
</main>
|
||||
<footer><a href="https://github.com/itsnotkubrick/3X-UI_KIT/tree/main/tools">Исходный код на GitHub</a> · Проверено на Mihomo 1.19 и XKeen 2.0</footer>
|
||||
@@ -101,7 +101,7 @@
|
||||
if ($('#sub').value.trim() && !o.subscription) err = 'Подписка должна начинаться с http:// или https://';
|
||||
if (!r) {
|
||||
yaml = '';
|
||||
$('#result').innerHTML = err ? '<div class="msg err">' + esc(err) + '</div>' : '<div class="empty">Добавьте ссылку или подписку в шаг 1 — здесь появится конфиг.</div>';
|
||||
$('#result').innerHTML = err ? '<div class="msg err">' + esc(err) + '</div>' : '<div class="empty">Добавьте ссылку или подписку в шаг 1 – здесь появится конфиг.</div>';
|
||||
return;
|
||||
}
|
||||
yaml = r.yaml;
|
||||
@@ -110,10 +110,10 @@
|
||||
'<div class="tabs" id="tabs"><button data-tab="cmd" class="on">Команда для роутера</button><button data-tab="yaml">config.yaml</button></div>' +
|
||||
'<div class="out"><pre class="code" id="out"></pre></div>' +
|
||||
'<div class="actions"><button class="btn" id="cp">Скопировать</button><button class="btn ghost" id="dl">Скачать config.yaml</button></div>' +
|
||||
'<ol class="how"><li>Подключитесь к роутеру: <code>ssh root@192.168.1.1 -p 222</code></li>' +
|
||||
'<ol class="how"><li>Подключитесь к роутеру: <code>ssh root@192.168.x.x -p 222</code></li>' +
|
||||
'<li>Если XKeen сейчас работает на Xray, сначала выполните <code>xkeen -mihomo</code></li>' +
|
||||
'<li>Вставьте команду целиком и нажмите Enter — конфиг запишется, XKeen перезапустится.</li>' +
|
||||
'<li>Панель управления: <code>http://192.168.1.1:9090/ui</code>, секрет: <code id="sec"></code></li></ol>';
|
||||
'<li>Вставьте команду целиком и нажмите Enter – конфиг запишется, XKeen перезапустится.</li>' +
|
||||
'<li>Панель управления: <code>http://192.168.x.x:9090/ui</code>, секрет: <code id="sec"></code></li></ol>';
|
||||
UI.tabs($('#tabs'), (t) => { tab = t; show(); });
|
||||
$('#cp').onclick = (e) => UI.copy($('#out').textContent, e.target);
|
||||
$('#dl').onclick = () => UI.download('config.yaml', yaml);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Подпись релиза 3X-UI KIT. Запускает только автор, на своём компьютере, перед тегом:
|
||||
#
|
||||
# tools/release.sh [~/.ssh/3xui_kit_release]
|
||||
#
|
||||
# Проверяет, что версии сходятся и установщик 3X-UI не подменили, пишет SHA256SUMS,
|
||||
# подписывает его закрытым ключом (ssh-keygen спросит пароль ключа) и сразу проверяет
|
||||
# подпись открытым ключом, записанным в kit. Сервер 3X-UI KIT ставит обновление, только
|
||||
# если эта проверка у него тоже сошлась. Закрытый ключ никуда не отправляется.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
die() { printf '✗ %s\n' "$*" >&2; exit 1; }
|
||||
ok() { printf '✓ %s\n' "$*"; }
|
||||
sha() { if command -v sha256sum >/dev/null; then sha256sum "$@"; else shasum -a 256 "$@"; fi; }
|
||||
|
||||
key=${1:-$HOME/.ssh/3xui_kit_release}
|
||||
[[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI KIT releases\" -f $key"
|
||||
NS="3x-ui-kit-release"
|
||||
ID="releases@3x-ui-kit"
|
||||
FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py VERSION)
|
||||
|
||||
v=$(tr -d '[:space:]' <VERSION)
|
||||
[[ $v =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "VERSION: $v"
|
||||
for f in scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh; do
|
||||
grep -qx "KIT_VERSION=\"$v\"" "$f" || die "$f: KIT_VERSION не $v"
|
||||
done
|
||||
grep -q "^## v$v" CHANGELOG.md || die "В CHANGELOG.md нет раздела ## v$v"
|
||||
ok "Версия $v во всех скриптах и в CHANGELOG"
|
||||
|
||||
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
|
||||
|
||||
# Установщик 3X-UI: хеш в скрипте должен совпадать с тем, что сейчас лежит на GitHub.
|
||||
xv=$(sed -n 's/^XUI_VERSION="\(.*\)"/\1/p' scripts/3x-ui.sh)
|
||||
xs=$(sed -n 's/^XUI_INSTALL_SHA256="\(.*\)"/\1/p' scripts/3x-ui.sh)
|
||||
got=$(curl -fsSL "https://raw.githubusercontent.com/MHSanaei/3x-ui/$xv/install.sh" | sha | awk '{print $1}')
|
||||
[[ $got == "$xs" ]] || die "Установщик 3X-UI $xv изменился: $got вместо $xs. Проверьте, что поменялось, прежде чем выпускать."
|
||||
ok "Установщик 3X-UI $xv совпадает с закреплённым SHA256"
|
||||
|
||||
# Открытые ключи из kit и hy2 – одинаковые, и среди них есть ключ, которым подписываем.
|
||||
signers() { awk '/^KIT_SIGNERS=\(/ {on = 1; next} on && /^\)/ {exit} on && /^ *"/ {gsub(/^ *"|"$/, ""); print}' "$1"; }
|
||||
[[ -n $(signers scripts/kit.sh) ]] || die "В scripts/kit.sh пустой KIT_SIGNERS: впишите туда открытую часть ключа ($key.pub)."
|
||||
[[ $(signers scripts/kit.sh) == "$(signers scripts/hysteria2.sh)" ]] || die "KIT_SIGNERS в kit.sh и hysteria2.sh различаются."
|
||||
pub=$(ssh-keygen -y -f "$key" | awk '{print $1, $2}')
|
||||
signers scripts/kit.sh | awk '{print $1, $2}' | grep -qxF "$pub" || die "Ключа $key нет в KIT_SIGNERS – серверы не примут такую подпись."
|
||||
ok "Ключ подписи записан в kit и hy2"
|
||||
|
||||
{ echo "# 3X-UI KIT $v"; sha "${FILES[@]}"; } >SHA256SUMS
|
||||
rm -f SHA256SUMS.sig
|
||||
ssh-keygen -Y sign -f "$key" -n "$NS" SHA256SUMS >/dev/null
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
signers scripts/kit.sh | while read -r k; do printf '%s namespaces="%s" %s\n' "$ID" "$NS" "$k"; done >"$tmp"
|
||||
ssh-keygen -Y verify -f "$tmp" -I "$ID" -n "$NS" -s SHA256SUMS.sig <SHA256SUMS >/dev/null || die "Подпись не прошла проверку."
|
||||
ok "SHA256SUMS подписан и проверен так же, как это сделает сервер"
|
||||
echo
|
||||
cat SHA256SUMS
|
||||
echo
|
||||
echo "Дальше:"
|
||||
echo " git add SHA256SUMS SHA256SUMS.sig && git commit -m \"Релиз v$v: подпись\""
|
||||
echo " git tag v$v && git push origin main v$v"
|
||||
@@ -109,7 +109,7 @@ test('TUIC, WireGuard, AmneziaWG (vpn://) и MTProto', () => {
|
||||
assert.equal(parseLink('https://t.me/proxy?server=m.example.com&port=8445&secret=ee00').port, 8445);
|
||||
});
|
||||
|
||||
test('Xray 26: вместо allowInsecure — отпечаток или предупреждение', () => {
|
||||
test('Xray 26: вместо allowInsecure – отпечаток или предупреждение', () => {
|
||||
const insecure = WS_TLS.replace('#', '&allowInsecure=1#');
|
||||
const r = buildXray(parseText(insecure).proxies, {});
|
||||
assert.ok(!JSON.stringify(r.files).includes('allowInsecure'));
|
||||
@@ -119,7 +119,7 @@ test('Xray 26: вместо allowInsecure — отпечаток или пред
|
||||
assert.equal(pinned.warnings.length, 0);
|
||||
});
|
||||
|
||||
test('Mihomo: AmneziaWG 3.1 — имена параметров Mihomo и version: 3', () => {
|
||||
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'];
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Генератор Xray для XKeen</title>
|
||||
<meta name="description" content="Готовые 04_outbounds.json и 05_routing.json для XKeen из ссылки vless://, vmess://, trojan://, ss:// — прямо в браузере">
|
||||
<meta name="description" content="Готовые 04_outbounds.json и 05_routing.json для XKeen из ссылки vless://, vmess://, trojan://, ss:// – прямо в браузере">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🕶️</text></svg>">
|
||||
<link rel="stylesheet" href="../lib/ui.css">
|
||||
</head>
|
||||
@@ -16,30 +16,30 @@
|
||||
|
||||
<main>
|
||||
<h1>Конфиг Xray для XKeen</h1>
|
||||
<p class="lead">Вставьте ссылку на свой сервер — получите готовые <code>04_outbounds.json</code> и <code>05_routing.json</code>
|
||||
<p class="lead">Вставьте ссылку на свой сервер – получите готовые <code>04_outbounds.json</code> и <code>05_routing.json</code>
|
||||
и одну команду, которая сама положит их на роутер.</p>
|
||||
<div class="privacy">🔒 <span>Всё считается <b>в вашем браузере</b> — ссылки никуда не отправляются</span></div>
|
||||
<div class="privacy">🔒 <span>Всё считается <b>в вашем браузере</b> – ссылки никуда не отправляются</span></div>
|
||||
|
||||
<section class="step">
|
||||
<h2><span class="num">1</span>Ссылки на серверы</h2>
|
||||
<textarea id="links" class="mono" spellcheck="false" placeholder="vless://… По одной на строку. Можно вставить подписку в base64 целиком."></textarea>
|
||||
<div class="parsed" id="parsed"></div>
|
||||
<p class="hint">Поддерживаются vless (REALITY, TLS, ws, gRPC, xhttp, httpupgrade), vmess, trojan, shadowsocks, Hysteria2 и WireGuard.
|
||||
Несколько серверов — Xray сам выберет самый быстрый. TUIC и AmneziaWG работают через <a href="../mihomo/">Mihomo</a>.</p>
|
||||
Несколько серверов – Xray сам выберет самый быстрый. TUIC и AmneziaWG работают через <a href="../mihomo/">Mihomo</a>.</p>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
<h2><span class="num">2</span>Что пускать через прокси</h2>
|
||||
<div class="sub">Геобазы на роутере</div>
|
||||
<div class="grid" id="bases"></div>
|
||||
<p class="hint">Какие базы вы выбрали при установке XKeen. Не уверены — оставьте ZKeen: её XKeen ставит по умолчанию.</p>
|
||||
<p class="hint">Какие базы вы выбрали при установке XKeen. Не уверены – оставьте ZKeen: её XKeen ставит по умолчанию.</p>
|
||||
<div class="sub">Сервисы</div>
|
||||
<div class="grid" id="services"></div>
|
||||
<div class="sub">Дополнительно</div>
|
||||
<div class="grid">
|
||||
<label class="opt"><input type="checkbox" id="blockQuic" checked><span>Блокировать QUIC<small>видео через прокси грузится стабильнее</small></span></label>
|
||||
<label class="opt" id="adsOpt"><input type="checkbox" id="blockAds"><span>Блокировать рекламу<small>нужна база V2Fly</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="finalProxy"><span>Всё остальное — тоже через прокси<small>иначе остальное идёт напрямую</small></span></label>
|
||||
<label class="opt"><input type="checkbox" id="finalProxy"><span>Всё остальное – тоже через прокси<small>иначе остальное идёт напрямую</small></span></label>
|
||||
</div>
|
||||
<div class="two">
|
||||
<div><label class="field" for="proxyDomains">Свои сайты через прокси</label>
|
||||
@@ -49,12 +49,12 @@
|
||||
</div>
|
||||
<label class="field" for="tag">Тег основного сервера</label>
|
||||
<input type="text" id="tag" value="vless-reality" spellcheck="false">
|
||||
<p class="hint">XKeen по умолчанию ожидает <code>vless-reality</code> — менять стоит, только если вы правили правила вручную.</p>
|
||||
<p class="hint">XKeen по умолчанию ожидает <code>vless-reality</code> – менять стоит, только если вы правили правила вручную.</p>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
<h2><span class="num">3</span>Готовый конфиг</h2>
|
||||
<div id="result"><div class="empty">Вставьте ссылку в шаг 1 — здесь появятся файлы.</div></div>
|
||||
<div id="result"><div class="empty">Вставьте ссылку в шаг 1 – здесь появятся файлы.</div></div>
|
||||
</section>
|
||||
</main>
|
||||
<footer><a href="https://github.com/itsnotkubrick/3X-UI_KIT/tree/main/tools">Исходный код на GitHub</a> · Проверено на Xray 26 и XKeen 2.0</footer>
|
||||
@@ -93,7 +93,7 @@
|
||||
const el = document.querySelector('[data-svc="' + s.id + '"]');
|
||||
const ok = PM.xrayServiceAvailable(s, o.bases);
|
||||
const box = el.querySelector('input');
|
||||
// Без нужной базы галочку снимаем, но помним выбор — вернём, когда база появится.
|
||||
// Без нужной базы галочку снимаем, но помним выбор – вернём, когда база появится.
|
||||
if (!ok && !box.disabled) { box.dataset.want = box.checked ? '1' : ''; box.checked = false; }
|
||||
if (ok && box.disabled) box.checked = box.dataset.want === '1';
|
||||
el.classList.toggle('off', !ok);
|
||||
@@ -132,7 +132,7 @@
|
||||
UI.renderParsed($('#parsed'), parsed, r ? r.skipped : []);
|
||||
if (!r) {
|
||||
files = null;
|
||||
$('#result').innerHTML = err ? '<div class="msg err">' + esc(err) + '</div>' : '<div class="empty">Вставьте ссылку в шаг 1 — здесь появятся файлы.</div>';
|
||||
$('#result').innerHTML = err ? '<div class="msg err">' + esc(err) + '</div>' : '<div class="empty">Вставьте ссылку в шаг 1 – здесь появятся файлы.</div>';
|
||||
return;
|
||||
}
|
||||
files = {};
|
||||
@@ -143,8 +143,8 @@
|
||||
'<button data-tab="04_outbounds.json">04_outbounds.json</button><button data-tab="05_routing.json">05_routing.json</button></div>' +
|
||||
'<div class="out"><pre class="code" id="out"></pre></div>' +
|
||||
'<div class="actions"><button class="btn" id="cp">Скопировать</button><button class="btn ghost" id="dl">Скачать файл</button></div>' +
|
||||
'<ol class="how" id="how"><li>Подключитесь к роутеру: <code>ssh root@192.168.1.1 -p 222</code></li>' +
|
||||
'<li>Вставьте команду целиком и нажмите Enter — файлы запишутся, XKeen перезапустится.</li>' +
|
||||
'<ol class="how" id="how"><li>Подключитесь к роутеру: <code>ssh root@192.168.x.x -p 222</code></li>' +
|
||||
'<li>Вставьте команду целиком и нажмите Enter – файлы запишутся, XKeen перезапустится.</li>' +
|
||||
'<li>Готово. Проверить: <code>xkeen -status</code></li></ol><div id="note"></div>';
|
||||
UI.tabs($('#tabs'), (t) => { tab = t; show(); });
|
||||
$('#cp').onclick = (e) => UI.copy($('#out').textContent, e.target);
|
||||
@@ -153,7 +153,7 @@
|
||||
show();
|
||||
const notes = [];
|
||||
if (r.count > 1) notes.push('Серверов: ' + r.count + '. Xray будет каждую минуту проверять их и выбирать самый быстрый.');
|
||||
if (r.skipped.length) notes.push('Пропущено: ' + r.skipped.map((s) => s.name + ' — ' + s.error).join('; '));
|
||||
if (r.skipped.length) notes.push('Пропущено: ' + r.skipped.map((s) => s.name + ' – ' + s.error).join('; '));
|
||||
(r.warnings || []).forEach((w) => notes.push(w));
|
||||
$('#note').innerHTML = notes.map((n) => '<div class="msg warn">' + esc(n) + '</div>').join('');
|
||||
}
|
||||
|
||||