Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31dd4d575b | ||
|
|
c4f7c4f710 | ||
|
|
19eb77be68 | ||
|
|
333b8edc9a | ||
|
|
13dfb44f3f | ||
|
|
3a8518651c | ||
|
|
915dd35f42 | ||
|
|
edabfd9a83 | ||
|
|
741a70c9bf | ||
|
|
0d2af45bdb | ||
|
|
a02650311e | ||
|
|
2b57e6f6d7 | ||
|
|
df8a99bb78 | ||
|
|
47328b33b3 | ||
|
|
776b7a8c5e | ||
|
|
2d63136853 | ||
|
|
1b85a4bb23 | ||
|
|
b80c44077c | ||
|
|
f894b48cd2 | ||
|
|
bddef5b8d3 |
@@ -1,3 +1,141 @@
|
|||||||
|
## v1.2.1.9: kit fix снова выполняется при установке
|
||||||
|
|
||||||
|
* В 1.2.1.8 я понял просьбу слишком широко и **выключил сам `kit fix`** — вместе с ним пропала и
|
||||||
|
синхронизация времени. Возвращено: `kit fix` при установке выполняется как раньше (время, службы, права),
|
||||||
|
ключ `--no-time` его пропускает.
|
||||||
|
* Убрана только та строка, о которой шла речь: описание команды «kit fix – безопасные исправления
|
||||||
|
(службы, права, автообновление, сертификат, время)» больше не печатается — результат («Чинить нечего»
|
||||||
|
или список найденного) остаётся.
|
||||||
|
* Наша строка шага сокращена: «kit fix: безопасные починки (время, службы, права)».
|
||||||
|
|
||||||
|
## v1.2.1.8: подсказка меню исправлена, установка стала тише
|
||||||
|
|
||||||
|
* **«Выбор [0-10]» при 11 пунктах** — подсказка главного меню отстала, когда мы добавили пункт «Доступ к
|
||||||
|
панели, ссылки и заглушка». Теперь «Выбор [0-11]». Чтобы это не повторилось, добавлен тест
|
||||||
|
`tools/check-menus.py`: он сверяет подсказку каждого меню с числом пунктов и запускается в релизном
|
||||||
|
гейте — если мы снова добавим пункт и забудем подсказку, выпуск остановится.
|
||||||
|
* **`kit fix` больше не выполняется при установке** (в выводе это выглядело как «Чинить нечего: всё в
|
||||||
|
порядке»). Нужен — запускается из меню или ключом `--time`.
|
||||||
|
* **Вывод про автообновления безопасности сокращён до одной строки** («автообновления безопасности
|
||||||
|
включены…»): три строки про ядро и перезагрузку убраны из установки.
|
||||||
|
|
||||||
|
## v1.2.1.7: исправлена установка (моя регрессия в 1.2.1.6)
|
||||||
|
|
||||||
|
* В 1.2.1.6 я переписал загрузку файлов релиза и оставил проверку SHA256 установщика **раньше**
|
||||||
|
скачивания — установка обрывалась на первой же строке: `sha256sum: .../scripts/3x-ui.sh: No such file
|
||||||
|
or directory`. Теперь сначала скачиваются все файлы релиза (каждый сверяется с подписанным списком),
|
||||||
|
и только потом проверяется установщик.
|
||||||
|
* Проверено не на глаз: воспроизвёл ошибку у себя тем же путём (`--verify-only`) — красное, затем после
|
||||||
|
правки зелёное. Этот прогон добавлен в процедуру выпуска: перед подписью я запускаю установщик в режиме
|
||||||
|
`--verify-only` и убеждаюсь, что путь «скачать и проверить релиз» проходит целиком.
|
||||||
|
|
||||||
|
## v1.2.1.6: доступы к панели в конце установки и рабочая заглушка
|
||||||
|
|
||||||
|
* **Исправлено: доступы к панели не выводились.** Автор пишет строки в `/root/3x-ui.txt` без ведущих
|
||||||
|
пробелов, а наш фильтр требовал два пробела — отсюда «данных панели нет». Теперь адрес панели, логин и
|
||||||
|
пароль печатаются в конце установки (и в пункте меню «9. Доступ к панели, ссылки и заглушка»).
|
||||||
|
Рядом указывается, что подписка и ссылки по протоколам лежат в том же файле.
|
||||||
|
* **Исправлено: наша заглушка не готовилась до установщика.** Обёртка скачивала из релиза только три файла,
|
||||||
|
а `scripts/kit-local.sh` (там функция страницы) не качала — падало с «kl_site_html: command not found».
|
||||||
|
Теперь качаются все файлы релиза, и каждый сверяется с подписанным списком (проверка целостности стала
|
||||||
|
полной, а не только для установщика).
|
||||||
|
|
||||||
|
## v1.2.1.5: доступ к панели, ссылки и заглушка — одним пунктом меню
|
||||||
|
|
||||||
|
* **В меню kit пункт «9. Доступ к панели, ссылки и заглушка»** — открывает одним экраном: адрес панели,
|
||||||
|
логин и пароль (из /root/3x-ui.txt), состояние сертификата панели (доверенный на IP или
|
||||||
|
самоподписанный, с датой повтора при лимите Let's Encrypt), ссылки на подключения с QR-кодом и что
|
||||||
|
сейчас стоит заглушкой. Раньше доступ к панели в меню не показывался вообще, а про заглушку было
|
||||||
|
непонятно, как её посмотреть.
|
||||||
|
* Меню перенумеровано: «10. Панель x-ui», «11. Удалить KIT с сервера» (логика удаления — авторская).
|
||||||
|
* То же самое доступно из консоли: `kit local access [имя]`.
|
||||||
|
|
||||||
|
## v1.2.1.4: короткий итог установки, автовыбор в общедоступных сайтах, команда про порты
|
||||||
|
|
||||||
|
* **Итог установки вместо простыни текста** — компактный блок: сборка, сервер, протоколы, состояние
|
||||||
|
сертификата панели, одна строка проверки. Дальше только то, что нужно человеку: доступ к панели (адрес,
|
||||||
|
логин, пароль — по просьбе из ранних правок), указатель на меню kit для ссылок и QR, открытые порты и
|
||||||
|
команда для закрытия лишних. Длинные отчёты (состояние защиты, полный вывод kit check) убраны из
|
||||||
|
установки — они остались доступны командами, в том числе `kit local hardening`.
|
||||||
|
* **Ссылки на подключения больше не попадают в вывод установки даже косвенно**: раньше печатался весь
|
||||||
|
файл /root/3x-ui.txt, а в нём лежат и ссылки vless://. Теперь из него берутся только строки панели —
|
||||||
|
адрес, логин, пароль.
|
||||||
|
* **В меню общедоступных сайтов последним пунктом добавлен автовыбор** (раньше можно было только выбрать
|
||||||
|
номером или вернуться).
|
||||||
|
* **Новая команда `kit local ports`** — что открыто в ufw, что реально нужно (порты подключений и SSH),
|
||||||
|
что лишнее; `kit local ports --close-extra` закрывает лишнее.
|
||||||
|
* Ошибка подготовки нашей заглушки теперь печатает причину, а не просто «не смог».
|
||||||
|
|
||||||
|
## v1.2.1.3: ссылки и QR — в меню kit; сертификат панели видно в итоге
|
||||||
|
|
||||||
|
* **Ссылки для подключения и QR-код больше не печатаются в выводе установки.** Их смотрят позже, когда
|
||||||
|
настраивают телефон, и они всегда доступны заново: `kit → Пользователи → Показать ссылки`. В итоговом
|
||||||
|
блоке вместо них — короткий путь к этому пункту и команды `kit local links` (подписка и QR) и
|
||||||
|
`kit user link <имя> --all` (все протоколы). Доступы к панели (адрес, логин, пароль) по-прежнему
|
||||||
|
в конце установки: их вы просили показывать один раз и сразу.
|
||||||
|
* **Добавлена команда `kit local links`** — подписка, QR-код и ссылки по протоколам одной командой;
|
||||||
|
пользователя можно указать, иначе берётся первый.
|
||||||
|
* **В итоговом блоке видно состояние сертификата панели.** Панель и подписка висят на IP, поэтому
|
||||||
|
печатается: «Let's Encrypt на IP — доверенный, браузер не ругается» либо «самоподписанный» с датой,
|
||||||
|
после которой Let's Encrypt выпустит сертификат на IP (тот же лимит, что и для домена).
|
||||||
|
|
||||||
|
## v1.2.1.2: сертификат на IP — с датой повтора; «без домена» продолжает установку
|
||||||
|
|
||||||
|
* **Отказ Let's Encrypt на IP теперь объясняется честно.** Как и для домена, при лимите в выводе дата,
|
||||||
|
после которой выпуск возможен, и прямо сказано, что порт 80 и файрвол тут не при чём. Раньше печатались
|
||||||
|
«частые причины» и человек шёл проверять хостера.
|
||||||
|
* **Пункт «без своего домена» больше не останавливает установку.** При отказе с сертификатом домена выбор
|
||||||
|
«3» продолжает работу сразу: маскировка берётся из соседей по подсети (или известный сайт, если соседей
|
||||||
|
нет), панель и подписка переключаются на IP, дальше установка идёт своим ходом. Перезапускать скрипт
|
||||||
|
и заново отвечать на вопросы не нужно.
|
||||||
|
|
||||||
|
## v1.2.1.1: сборка на новой базе автора (3X-UI KIT 1.2.1)
|
||||||
|
|
||||||
|
**Правило нумерации:** берём версию автора и через точку добавляем свою, начиная с 1. У автора 1.2.1 —
|
||||||
|
значит наша сборка 1.2.1.1; когда выйдет его 1.2.2, наша будет 1.2.2.1.
|
||||||
|
|
||||||
|
**Взяли от автора (его код теперь работает вместо наших заплаток):**
|
||||||
|
* проверка A-записи домена сверяется с **публичным DNS**, а не с кэшем самого сервера — та самая причина,
|
||||||
|
когда запись уже есть, а сервер её «не видит»;
|
||||||
|
* пропал наш шов, открывавший порт 80: автор делает это сам перед выпуском сертификата **и закрывает обратно**;
|
||||||
|
* пропал наш шов переиспользования сертификата: автор тоже берёт прошлый;
|
||||||
|
* повторный запуск после неудачной установки: автор заново спрашивает протоколы и сайт маскировки и не
|
||||||
|
оставляет сервер на полпути — прежнее наше «доустанавливаю» больше не нужно;
|
||||||
|
* полное удаление: наш `kit local uninstall` теперь **делегирует авторской команде `kit uninstall`**
|
||||||
|
(она снимает и сам kit) и остаётся синонимом;
|
||||||
|
* меню `kit` — авторское (в том числе «9. Панель 3X-UI (меню x-ui)», «10. Удалить KIT с сервера»);
|
||||||
|
* попутно приехали его починки: Hiddify 4.x снова принимает подписку, ссылка MTProto ведёт на 443, смена
|
||||||
|
сайта маскировки не обрывает клиентов (прежнее имя живёт неделю), `nginx -t` в проверке сервера,
|
||||||
|
устойчивость к битому apt-репозиторию, длинные имена сайта в nginx, проверка копии (в т.ч. U+202E),
|
||||||
|
честная глубокая проверка, предупреждение «обновлять панель и ядро только через kit».
|
||||||
|
|
||||||
|
**Сохранили своё:**
|
||||||
|
* наш файл `scripts/kit-local.sh` и наша обёртка установщика — автор их не трогает;
|
||||||
|
* наш канал обновлений (GitHub → наш Gitea) и **только наш ключ подписи** в `KIT_SIGNERS`: сервер обязан
|
||||||
|
принимать обновления лишь из нашего канала;
|
||||||
|
* наш экран выбора протоколов (13 пунктов, дефолт REALITY + AmneziaWG-3.1) — теперь номера вводятся
|
||||||
|
**через пробел** («1 12»), как вы просили; запятые по-прежнему принимаются;
|
||||||
|
* наши меню маскировки REALITY и XHTTP/MTProto, вопрос «панель по IP или по домену», возврат «ввести
|
||||||
|
другой домен» без обрыва установки;
|
||||||
|
* наша заглушка ставится сразу, авторская не устанавливается;
|
||||||
|
* финальный блок: доступы, подписка и QR печатает наш установщик один раз, в самом конце (авторский дубль
|
||||||
|
«Готово! Сервер работает» убран, предупреждения остаются).
|
||||||
|
|
||||||
|
**Технически:** из 77 прежних швов 66 легли без изменений, 8 больше не нужны (их работу делает автор),
|
||||||
|
3 перенастроены на его новый код (ключ подписи, защита наших меню, финальный блок).
|
||||||
|
|
||||||
|
## v1.2.27: ошибка с доменом больше не обрывает установку
|
||||||
|
|
||||||
|
* **Свой домен вводится заново, пока не заработает.** Раньше при промахе (A-запись ещё не разошлась
|
||||||
|
по DNS, опечатка в имени) установка молча маскировалась под чужой сайт, а при домене из флага —
|
||||||
|
обрывалась с «исправьте причину и запустите скрипт снова». Теперь сразу после ввода домена идёт
|
||||||
|
проверка и при промахе — выбор: «проверить снова» (DNS расходится от минут до часов), «ввести другой
|
||||||
|
домен», «без своего домена — маскировка соседом по подсети».
|
||||||
|
* **Провал сертификата на шаге подключений не прерывает установку.** Предлагается повторить попытку
|
||||||
|
(пока ждали обновления DNS или лимит Let's Encrypt), ввести другое имя (с той же проверкой A-записи)
|
||||||
|
или остановиться с понятной инструкцией, как поставить без домена. Причина и дата повтора при лимите
|
||||||
|
печатаются в самом отказе, лог остаётся в /var/log/kit-domain-cert.log.
|
||||||
|
|
||||||
## v1.2.26: лимит Let's Encrypt виден прямо, готовый сертификат переиспользуется
|
## v1.2.26: лимит Let's Encrypt виден прямо, готовый сертификат переиспользуется
|
||||||
|
|
||||||
* **Причина отказа теперь называется честно.** Let's Encrypt разрешает 5 сертификатов на точный набор имён
|
* **Причина отказа теперь называется честно.** Let's Encrypt разрешает 5 сертификатов на точный набор имён
|
||||||
|
|||||||
@@ -191,6 +191,20 @@ bash tools/fork-sync.sh # подтянуть автора, перен
|
|||||||
|
|
||||||
bash tools/release.sh ~/.ssh/3x-ui-kit-fork/release_key # SHA256SUMS + подпись нашим ключом
|
bash tools/release.sh ~/.ssh/3x-ui-kit-fork/release_key # SHA256SUMS + подпись нашим ключом
|
||||||
|
|
||||||
|
**Перед подписью — обязательный прогон.** `bash scripts/3x-ui-local.sh --verify-only` проверяет ровно тот
|
||||||
|
путь установки, которым пойдут серверы: скачивание всех файлов релиза и сверка каждого с подписанным
|
||||||
|
списком. На Windows запускать с `TMPDIR=C:/…` (нативные утилиты не понимают MSYS-пути).
|
||||||
|
|
||||||
|
**Нумерация версий.** Основа — версия автора, наша надстройка добавляется через точку с единицы:
|
||||||
|
у автора 1.2.1 → наша сборка 1.2.1.1; вышла его 1.2.2 → наша 1.2.2.1. Так на сервере всегда видно,
|
||||||
|
на чьей базе собрана сборка.
|
||||||
|
|
||||||
|
**Обновление базы автора.** Когда автор выпускает новую версию: перенести его файлы
|
||||||
|
(`git checkout upstream -- scripts/kit.sh scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit-sub.py`),
|
||||||
|
прогнать `python tools/fork_apply.py` и посмотреть, какие якоря не нашли себя. Те, чью работу автор уже
|
||||||
|
делает сам, — вычеркнуть; остальные перенастроить на его новый код и пересобрать таблицу швов
|
||||||
|
`git branch -f upstream <коммит автора>`.
|
||||||
|
|
||||||
Скрипт сам приводит переводы строк к LF, проверяет справку и то, что установщик не ругается на пустые
|
Скрипт сам приводит переводы строк к LF, проверяет справку и то, что установщик не ругается на пустые
|
||||||
строки, сверяет панель и ядро Xray с официальными хешами GitHub (если GitHub недоступен — предупреждает и
|
строки, сверяет панель и ядро Xray с официальными хешами GitHub (если GitHub недоступен — предупреждает и
|
||||||
пропускает эти проверки за секунды, а не висит; расхождение хешей при живом GitHub по-прежнему останавливает
|
пропускает эти проверки за секунды, а не висит; расхождение хешей при живом GitHub по-прежнему останавливает
|
||||||
|
|||||||
+9
-9
@@ -1,9 +1,9 @@
|
|||||||
# 3X-UI KIT 1.2.26
|
# 3X-UI KIT 1.2.1.9
|
||||||
# 3X-UI_autoVPS 1.2.26
|
# 3X-UI_autoVPS 1.2.1.9
|
||||||
57d72445582d8399253827471a1c31d284f22d912bf0712a0d442881cc41ecbf scripts/3x-ui.sh
|
840b814246e0676a474511a26466b7b3406a8210c2b1e7097579b578d7c71257 scripts/3x-ui.sh
|
||||||
da37ba9b4c0b287e2dd5c768ebf3b94c461ca0385b44ad310380dc86f2fc4369 scripts/hysteria2.sh
|
a85f01f285156131e0a51a9de5fb2bc8ea76d5b49763bf0d544ca294b69173ed scripts/hysteria2.sh
|
||||||
0fbd3440495d2fee85116d722d588b768a068d16b9c4ddfa287e636cd09c2343 scripts/kit.sh
|
89b6c8635d77558dd361cf64c5a01cde08fbe2964ad6f63ba22c17599f1e620c scripts/kit.sh
|
||||||
4027ea2c6dbc9fd176356acac3f676445419c2f021146ed25dfe5a304c3ac5fb scripts/kit-sub.py
|
f214671464e1c2e174e5af9df42c95567a9b4a9141f31a39f7df2e971e89d240 scripts/kit-sub.py
|
||||||
2b93ecc5901f7ea2033749a7e395eb7c16ce1d2461a9f3690b7ee407f00056ba scripts/kit-local.sh
|
5d07c87452057e1c73e0f3af008074f94f5be6175817b2d6f94a93f69b0533b7 scripts/kit-local.sh
|
||||||
e47dbd7042e9cd5250477ff1e8d19d652e1a217867fd489867acd855fcfbcda4 scripts/3x-ui-local.sh
|
bfaac8949c1e9f92fe92f1f0f6ff59ac7f37fad74cbf62439f327bc3144a8a06 scripts/3x-ui-local.sh
|
||||||
5e649074bdab71dd8d8ba9d69ae72cc2d27fd4c49fe300314ff8f48d162391d5 VERSION
|
bb43cbd01c2071d4aacdce23a00da7d86a126799e511a521c8a3c6818001a670 VERSION
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
-----BEGIN SSH SIGNATURE-----
|
-----BEGIN SSH SIGNATURE-----
|
||||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV
|
||||||
FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA
|
FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA
|
||||||
AAALc3NoLWVkMjU1MTkAAABAED/Hyzn6Gi/tDZkM8CsLJzsQXp2NmUKAzQW3i4wkrOgTA4
|
AAALc3NoLWVkMjU1MTkAAABA+3t9MmBZ+iwMvnKIfkihiqb0dX/qo8Ca5pTjBFec4v5ulG
|
||||||
Ny9AEobiOp3xXb/xcK5nYBzYeE7Xj28vZye9vbCg==
|
L4RtQ+cG1mBiv66u5h6Lh+yI/JgNzd/0Yccr5NBw==
|
||||||
-----END SSH SIGNATURE-----
|
-----END SSH SIGNATURE-----
|
||||||
|
|||||||
+81
-79
@@ -32,7 +32,6 @@
|
|||||||
# --no-harden не трогать ping и автообновления
|
# --no-harden не трогать ping и автообновления
|
||||||
# --ssh-port N добавить порт SSH N (прежний порт продолжает работать)
|
# --ssh-port N добавить порт SSH N (прежний порт продолжает работать)
|
||||||
# --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет)
|
# --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет)
|
||||||
# --no-time не запускать kit fix
|
|
||||||
# --no-check не запускать в конце kit check
|
# --no-check не запускать в конце kit check
|
||||||
# --clean убрать прошлую установку и поставить заново (SSH не трогает)
|
# --clean убрать прошлую установку и поставить заново (SSH не трогает)
|
||||||
# --auto-sni не предлагать выбор сайта маскировки (иначе покажем меню по скану подсети)
|
# --auto-sni не предлагать выбор сайта маскировки (иначе покажем меню по скану подсети)
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
KIT_VERSION="1.2.26" # версия сборки форка; должна совпадать с VERSION релиза
|
KIT_VERSION="1.2.1.9" # версия сборки форка; должна совпадать с VERSION релиза
|
||||||
GITEA="${KIT_FORK_BASE:-https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS}"
|
GITEA="${KIT_FORK_BASE:-https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS}"
|
||||||
NS="3x-ui-kit-release"
|
NS="3x-ui-kit-release"
|
||||||
SIG_ID="releases@3x-ui-kit"
|
SIG_ID="releases@3x-ui-kit"
|
||||||
@@ -83,7 +82,7 @@ usage() {
|
|||||||
--no-sub не показывать в конце подписку, доступ к панели и QR-код
|
--no-sub не показывать в конце подписку, доступ к панели и QR-код
|
||||||
--clean снести прошлую установку и поставить заново (если прошлый запуск оборвался)
|
--clean снести прошлую установку и поставить заново (если прошлый запуск оборвался)
|
||||||
--auto-sni не предлагать выбор сайта маскировки (автоподбор автора)
|
--auto-sni не предлагать выбор сайта маскировки (автоподбор автора)
|
||||||
--no-time не запускать kit fix (время и безопасные починки)
|
--no-time не запускать kit fix при установке (время, службы, права)
|
||||||
--no-check не запускать в конце kit check
|
--no-check не запускать в конце kit check
|
||||||
--verify-only только проверить релиз (подпись и SHA256) и выйти
|
--verify-only только проверить релиз (подпись и SHA256) и выйти
|
||||||
--dry-run показать план и ничего не делать
|
--dry-run показать план и ничего не делать
|
||||||
@@ -162,7 +161,7 @@ if [[ $dry == yes ]]; then
|
|||||||
random) pre "заглушка: случайный ИТ-сайт из нашего набора (10 штук)" ;;
|
random) pre "заглушка: случайный ИТ-сайт из нашего набора (10 штук)" ;;
|
||||||
*) pre "заглушка: сайт №$site из нашего набора (список: kit local site --list)" ;;
|
*) pre "заглушка: сайт №$site из нашего набора (список: kit local site --list)" ;;
|
||||||
esac
|
esac
|
||||||
[[ $fix_time == yes ]] && pre "после: kit fix (время, безопасные починки)"
|
[[ $fix_time == yes ]] && pre "после: kit fix (время, службы, права) — тихо, без описания команды"
|
||||||
[[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening"
|
[[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening"
|
||||||
[[ $show_sub == yes ]] && pre "в самом конце: доступ к панели 3X-UI, подписка, QR-код и ссылки (--no-sub — не надо)"
|
[[ $show_sub == yes ]] && pre "в самом конце: доступ к панели 3X-UI, подписка, QR-код и ссылки (--no-sub — не надо)"
|
||||||
echo
|
echo
|
||||||
@@ -179,7 +178,7 @@ say "беру релиз v$KIT_VERSION и проверяю подпись"
|
|||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
raw="$GITEA/raw/tag/v$KIT_VERSION"
|
raw="$GITEA/raw/tag/v$KIT_VERSION"
|
||||||
for f in SHA256SUMS SHA256SUMS.sig scripts/3x-ui.sh; do
|
for f in SHA256SUMS SHA256SUMS.sig; do
|
||||||
mkdir -p "$tmp/$(dirname "$f")"
|
mkdir -p "$tmp/$(dirname "$f")"
|
||||||
curl -fsSL --retry 3 -m 60 -o "$tmp/$f" "$raw/$f" || die "не скачался $f из $raw (Gitea доступна с сервера?)"
|
curl -fsSL --retry 3 -m 60 -o "$tmp/$f" "$raw/$f" || die "не скачался $f из $raw (Gitea доступна с сервера?)"
|
||||||
done
|
done
|
||||||
@@ -188,9 +187,21 @@ ssh-keygen -Y verify -f "$tmp/allowed_signers" -I "$SIG_ID" -n "$NS" -s "$tmp/SH
|
|||||||
|| die "подпись релиза v$KIT_VERSION не сошлась с ключом форка — это не наш релиз, ничего не запускаю"
|
|| die "подпись релиза v$KIT_VERSION не сошлась с ключом форка — это не наш релиз, ничего не запускаю"
|
||||||
sum=$(awk '{f = $2; sub(/^\*/, "", f)} f == "scripts/3x-ui.sh" {print $1}' "$tmp/SHA256SUMS")
|
sum=$(awk '{f = $2; sub(/^\*/, "", f)} f == "scripts/3x-ui.sh" {print $1}' "$tmp/SHA256SUMS")
|
||||||
[[ $(printf '%s' "$sum" | wc -c) -eq 64 ]] || die "в SHA256SUMS нет установщика — релиз собран неверно"
|
[[ $(printf '%s' "$sum" | wc -c) -eq 64 ]] || die "в SHA256SUMS нет установщика — релиз собран неверно"
|
||||||
|
# Качаем ВСЕ файлы релиза и сверяем каждый с подписанным списком. Так у нас есть и scripts/3x-ui.sh
|
||||||
|
# (установщик), и scripts/kit-local.sh (наша страница-заглушка), и всё остальное. Раньше брали только
|
||||||
|
# установщик: заглушка не готовилась («kl_site_html: command not found»), а проверка целостности была неполной.
|
||||||
|
while read -r sum2 name2; do
|
||||||
|
case $sum2 in \#* | "") continue ;; esac
|
||||||
|
case $name2 in SHA256SUMS | SHA256SUMS.sig) continue ;; esac
|
||||||
|
mkdir -p "$tmp/$(dirname "$name2")"
|
||||||
|
curl -fsSL --retry 3 -m 60 -o "$tmp/$name2" "$raw/$name2" || die "не скачался $name2 из $raw (Gitea доступна с сервера?)"
|
||||||
|
got2=$(sha256sum "$tmp/$name2" | awk '{print $1}')
|
||||||
|
[[ $got2 == "$sum2" ]] || die "SHA256 не совпал у $name2 — не запускаю"
|
||||||
|
done <"$tmp/SHA256SUMS"
|
||||||
got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}')
|
got=$(sha256sum "$tmp/scripts/3x-ui.sh" | awk '{print $1}')
|
||||||
[[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю"
|
[[ $got == "$sum" ]] || die "SHA256 установщика не совпал с подписанным — не запускаю"
|
||||||
bash -n "$tmp/scripts/3x-ui.sh" || die "установщик не проходит проверку синтаксиса"
|
bash -n "$tmp/scripts/3x-ui.sh" || die "установщик не проходит проверку синтаксиса"
|
||||||
|
[[ -r $tmp/scripts/kit-local.sh ]] || die "в релизе нет scripts/kit-local.sh — сборка неполная"
|
||||||
printf ' ✓ подпись сошлась, SHA256 установщика совпал\n'
|
printf ' ✓ подпись сошлась, SHA256 установщика совпал\n'
|
||||||
|
|
||||||
if [[ $verify_only == yes ]]; then
|
if [[ $verify_only == yes ]]; then
|
||||||
@@ -276,11 +287,11 @@ ask_protocols() { # печатает итоговый список через з
|
|||||||
interactive || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
|
interactive || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
|
||||||
proto_menu
|
proto_menu
|
||||||
for i in 1 2 3; do
|
for i in 1 2 3; do
|
||||||
printf '\n%sНомера через запятую (например 1,12) или «все»; Enter — REALITY + AmneziaWG-3.1: %s' "$B" "$N" >&2
|
printf '\n%sНомера через пробел (например 1 12) или «все»; Enter — REALITY + AmneziaWG-3.1: %s' "$B" "$N" >&2
|
||||||
ans=""
|
ans=""
|
||||||
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
|
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
|
||||||
if picked=$(proto_parse "$ans"); then printf '%s' "$picked"; return 0; fi
|
if picked=$(proto_parse "$ans"); then printf '%s' "$picked"; return 0; fi
|
||||||
warn "не понял «$ans» — нужны номера из списка через запятую или «все»"
|
warn "не понял «$ans» — нужны номера из списка через пробел или «все»"
|
||||||
done
|
done
|
||||||
warn "оставляю набор по умолчанию"
|
warn "оставляю набор по умолчанию"
|
||||||
printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"
|
printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"
|
||||||
@@ -457,13 +468,13 @@ if [[ $site != off ]]; then
|
|||||||
esac
|
esac
|
||||||
if [[ -n $stub_n ]]; then
|
if [[ -n $stub_n ]]; then
|
||||||
install -d -m 755 /var/www/kit
|
install -d -m 755 /var/www/kit
|
||||||
if ( source "$tmp/scripts/kit-local.sh" 2>/dev/null; kl_site_html "$stub_n" ) >/var/www/kit/index.html 2>/dev/null \
|
if ( source "$tmp/scripts/kit-local.sh" 2>/dev/null; kl_site_html "$stub_n" ) >/var/www/kit/index.html 2>"$tmp/stub.err" \
|
||||||
&& [[ -s /var/www/kit/index.html ]]; then
|
&& [[ -s /var/www/kit/index.html ]]; then
|
||||||
stub_staged=yes
|
stub_staged=yes
|
||||||
rm -f "$tmp/var/www/kit/index.html"
|
rm -f "$tmp/var/www/kit/index.html"
|
||||||
say "заглушка: наш сайт №$((stub_n + 1)) — ставится сразу, авторская страница не используется"
|
say "заглушка: наш сайт №$((stub_n + 1)) — ставится сразу, авторская страница не используется"
|
||||||
else
|
else
|
||||||
warn "не смог подготовить нашу заглушку — поставлю после установки (kit local site)"
|
warn "не смог подготовить нашу заглушку — поставлю после установки (kit local site). Причина: $(head -c 200 "$tmp/stub.err" 2>/dev/null | tr '\n' ' ')"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -500,7 +511,7 @@ case ${stub_staged:-no} in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ $fix_time == yes ]]; then
|
if [[ $fix_time == yes ]]; then
|
||||||
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
|
say "kit fix: безопасные починки (время, службы, права)"
|
||||||
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
|
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -525,77 +536,68 @@ if [[ -n $ssh_port ]]; then
|
|||||||
kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port"
|
kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------ проверка
|
# ------------------------------------------------------------------ итог: коротко и по делу
|
||||||
|
|
||||||
if [[ $do_check == yes ]]; then
|
if [[ $do_check == yes || $show_sub == yes ]]; then
|
||||||
say "проверяю, что получилось"
|
# kit check печатает много строк: человеку нужны только замечания и одна строка «всё ли в порядке».
|
||||||
kit check || warn "kit check нашёл замечания — читайте выше; безопасные починки: kit fix"
|
chk=$(kit check 2>&1 || true)
|
||||||
kit local hardening
|
problems=$(printf '%s\n' "$chk" | grep -cE '^❌|^⚠|^!' || true)
|
||||||
fi
|
[[ $problems == 0 ]] || printf '%s\n' "$chk" | grep -E '^❌|^⚠|^!' | sed 's/^/ /'
|
||||||
|
ports_open=""
|
||||||
cat <<EOF
|
command -v ufw >/dev/null 2>&1 && ports_open=$(ufw status 2>/dev/null | awk '$2 == "ALLOW" { printf "%s ", $1 }')
|
||||||
|
ip_show=$(hostname -I 2>/dev/null | awk '{ print $1 }')
|
||||||
${G}${B}Готово.${N} Сервер поставлен из нашей сборки (3X-UI_autoVPS $KIT_VERSION, канал: наш Gitea).
|
os_show=$( . /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}" )
|
||||||
|
cert_show="без сертификата: панель открывается через SSH-туннель"
|
||||||
подписка и ссылки: kit user link admin (или страница подписки из вывода установщика)
|
if [[ -s /root/cert/ip/fullchain.pem ]]; then
|
||||||
если что-то не так: kit check --deep, затем kit fix
|
if openssl x509 -in /root/cert/ip/fullchain.pem -noout -issuer 2>/dev/null | grep -q "Let's Encrypt"; then
|
||||||
вернуться к авторскому каналу: положить авторский kit поверх и выполнить kit update --force
|
cert_show="${G}Let's Encrypt на IP — доверенный, браузер не ругается${N}"
|
||||||
EOF
|
else
|
||||||
|
cert_show="${Y}самоподписанный — браузер предупредит${N}"
|
||||||
# ------------------------------------------------------------------ SSH: как закрыть прежние порты
|
fi
|
||||||
|
fi
|
||||||
if [[ -n ${ssh_port:-} ]]; then
|
protos_show=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
${Y}${B}SSH переведён на порт $ssh_port.${N} Прежние порты (${ssh_ports_before:-22}) пока работают — это
|
${B}────────────── Установка завершена ──────────────${N}
|
||||||
сделано специально, чтобы вы не потеряли доступ. Порядок обязательный:
|
сборка: 3X-UI_autoVPS $KIT_VERSION (канал: наш Gitea)
|
||||||
1) во ВТОРОМ окне проверьте вход: ssh -p $ssh_port root@${HOST:-<IP сервера>}
|
сервер: ${ip_show:-?}${os_show:+ · $os_show}
|
||||||
2) вход работает — закройте прежние порты: ${B}kit local ssh --close-old $ssh_port${N}
|
протоколы: ${protos_show:-—}
|
||||||
3) поправьте порт у себя: ~/.ssh/config, терминалы, мониторинг
|
сертификат: $cert_show
|
||||||
|
проверка: $([[ $problems == 0 ]] && echo 'замечаний нет' || echo "замечаний: $problems (подробности: kit check)")
|
||||||
|
${B}───────────────────────────────────────────────────${N}
|
||||||
|
|
||||||
|
${B}Доступ к панели${N} (файл /root/3x-ui.txt, виден только root)
|
||||||
|
EOF
|
||||||
|
# Автор пишет эти строки без ведущих пробелов (RESULT=/root/3x-ui.txt), поэтому ищем их как есть.
|
||||||
|
if grep -aqE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
|
||||||
|
grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /'
|
||||||
|
printf ' файл целиком (там же подписка и ссылки по протоколам): /root/3x-ui.txt\n'
|
||||||
|
else
|
||||||
|
warn "данных панели в /root/3x-ui.txt нет — смотрите вывод установщика выше"
|
||||||
|
fi
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${B}Ссылки для подключения и QR-код${N} — в меню kit
|
||||||
|
kit → Пользователи → Показать ссылки из консоли: kit local links
|
||||||
|
EOF
|
||||||
|
if [[ -n $ports_open ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${B}Порты${N}
|
||||||
|
открыто: $ports_open
|
||||||
|
что нужно и что лишнее: kit local ports закрыть лишние: kit local ports --close-extra
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
if [[ -n ${ssh_port:-} ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
${Y}SSH переведён на порт $ssh_port.${N} Прежние (${ssh_ports_before:-22}) пока открыты намеренно:
|
||||||
|
1) проверьте вход во втором окне: ssh -p $ssh_port root@${ip_show:-<IP>}
|
||||||
|
2) закройте прежние порты: kit local ssh --close-old $ssh_port
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
если что-то не так: kit check --deep, затем kit fix
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------ доступ к панели (перед подпиской)
|
|
||||||
|
|
||||||
if [[ $show_sub == yes ]]; then
|
|
||||||
say "панель 3X-UI: адрес, логин и пароль"
|
|
||||||
if [[ -s /root/3x-ui.txt ]]; then
|
|
||||||
printf ' файл: /root/3x-ui.txt (виден только root). Сохраните данные себе:\n'
|
|
||||||
sed 's/^/ /' /root/3x-ui.txt
|
|
||||||
else
|
|
||||||
warn "файла /root/3x-ui.txt нет — адрес панели и логин были в выводе установщика выше"
|
|
||||||
fi
|
|
||||||
if grep -qaE '^Панель: ?https?://[0-9]' /root/3x-ui.txt 2>/dev/null; then
|
|
||||||
printf ' %sпанель открывается по IP: сертификат самоподписанный, браузер предупредит.\n' "$D"
|
|
||||||
printf ' Завести свой домен и перейти на него: kit net panel domain%s\n' "$N"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ подписка, QR-код и доступы
|
|
||||||
|
|
||||||
# Показываем в самом конце, после всех проверок: это то, что человек уносит с собой.
|
|
||||||
# Печатает авторская команда (подписка + QR-код через qrencode + ссылки по протоколам),
|
|
||||||
# поэтому вывод не расходится с тем, что показывает `kit` потом.
|
|
||||||
if [[ $show_sub == yes ]]; then
|
|
||||||
sub_user=admin
|
|
||||||
for ((_i = 0; _i < ${#INSTALLER_ARGS[@]}; _i++)); do
|
|
||||||
[[ ${INSTALLER_ARGS[_i]} == --user ]] && sub_user=${INSTALLER_ARGS[_i + 1]:-admin}
|
|
||||||
done
|
|
||||||
say "подписка и доступы — это то, что стоит сохранить"
|
|
||||||
printf ' подписка — одна ссылка на все устройства; QR-код ниже наводите камерой в приложении\n'
|
|
||||||
printf ' приложения: Happ, Hiddify, Karing, v2rayN, Clash/Mihomo\n'
|
|
||||||
printf ' отдельные ссылки по протоколам — ниже же (если приложение не умеет подписку)\n'
|
|
||||||
printf ' %sпозже то же самое: kit user link %s --all (или меню: kit)%s\n' "$D" "$sub_user" "$N"
|
|
||||||
[[ -n ${ssh_port:-} ]] && printf ' SSH: новый порт %s, прежние (%s) ещё открыты — закройте их после проверки входа: kit local ssh --close-old %s\n' "$ssh_port" "${ssh_ports_before:-22}" "$ssh_port"
|
|
||||||
# Ссылки на каждый протокол показываем только если протоколов больше одного: при одном это лишний шум.
|
|
||||||
protos_now=$(tr '\n' ' ' < /etc/kit/kl-protos 2>/dev/null || true)
|
|
||||||
if [[ $(wc -w <<<"$protos_now") -gt 1 ]]; then
|
|
||||||
if ! kit user link "$sub_user" --all; then
|
|
||||||
warn "не смог показать ссылки для «$sub_user» — список пользователей: kit user list"
|
|
||||||
fi
|
|
||||||
elif ! kit user link "$sub_user"; then
|
|
||||||
warn "не смог показать подписку для «$sub_user» — список пользователей: kit user list"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
say "ссылки и QR-код не показываю (--no-sub). Посмотреть: kit user link admin --all"
|
|
||||||
fi
|
|
||||||
|
|||||||
+381
-96
@@ -16,6 +16,15 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
# На свежем VPS в фоне идут автообновления системы и держат замок dpkg: ждём его, а не падаем.
|
# На свежем VPS в фоне идут автообновления системы и держат замок dpkg: ждём его, а не падаем.
|
||||||
apt-get() { command apt-get -o DPkg::Lock::Timeout=900 "$@"; }
|
apt-get() { command apt-get -o DPkg::Lock::Timeout=900 "$@"; }
|
||||||
|
# Обновление списков пакетов. Чужой сломанный репозиторий (например, speedtest-cli без файла Release для этой версии системы)
|
||||||
|
# не должен ронять установку: предупреждаем и идём дальше, а если нужных пакетов не окажется – об этом скажет следующий шаг.
|
||||||
|
apt_refresh() {
|
||||||
|
local out
|
||||||
|
out=$(apt-get update -qq 2>&1) && return 0
|
||||||
|
warn "apt update завершился с ошибкой (чаще всего виноват посторонний репозиторий на сервере, не наш). Продолжаю."
|
||||||
|
{ grep -E '^(E|W):' <<<"$out" | head -5 | sed 's/^/ /'; } >&2 || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
wait_apt_idle() {
|
wait_apt_idle() {
|
||||||
local i
|
local i
|
||||||
pgrep -f '/usr/bin/unattended-upgrade$|apt\.systemd\.daily' >/dev/null || return 0
|
pgrep -f '/usr/bin/unattended-upgrade$|apt\.systemd\.daily' >/dev/null || return 0
|
||||||
@@ -30,7 +39,7 @@ wait_apt_idle() {
|
|||||||
XUI_VERSION="v3.9.0"
|
XUI_VERSION="v3.9.0"
|
||||||
# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30).
|
# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30).
|
||||||
XUI_INSTALL_SHA256="18616fe26c8f6c92db6daa2dcd7cd53c5143ee69ecfd26d8ea6dc9b2c78607a6"
|
XUI_INSTALL_SHA256="18616fe26c8f6c92db6daa2dcd7cd53c5143ee69ecfd26d8ea6dc9b2c78607a6"
|
||||||
KIT_VERSION="1.2.26"
|
KIT_VERSION="1.2.1.9"
|
||||||
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
|
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
|
||||||
KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"
|
KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"
|
||||||
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
|
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
|
||||||
@@ -54,8 +63,7 @@ XUI_ENV=/etc/x-ui/install-result.env
|
|||||||
SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com www.microsoft.com www.amd.com www.intel.com www.nvidia.com)
|
SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com www.microsoft.com www.amd.com www.intel.com www.nvidia.com)
|
||||||
|
|
||||||
ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
|
ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
|
||||||
# Обычный WireGuard легко распознаётся сетевым оборудованием и работает нестабильно
|
# Стандартный набор; остальные протоколы (например, wg) отмечаются в «На выбор» или через --protocols.
|
||||||
# (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим.
|
|
||||||
DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto)
|
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)
|
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=(); NB_ALL=()
|
PROTOS=(); CREATED=(); OPEN=(); NB_ALL=()
|
||||||
@@ -79,9 +87,61 @@ WARNINGS=(); STEP=0; STEPS=6
|
|||||||
say() { printf ' %s\n' "$*"; } # подробность под шагом
|
say() { printf ' %s\n' "$*"; } # подробность под шагом
|
||||||
ok() { printf ' %s\n' "${G}✓${N} $*"; }
|
ok() { printf ' %s\n' "${G}✓${N} $*"; }
|
||||||
step() { STEP=$((STEP + 1)); printf '\n%s\n' "${B}[$STEP/$STEPS]${N} $1"; }
|
step() { STEP=$((STEP + 1)); printf '\n%s\n' "${B}[$STEP/$STEPS]${N} $1"; }
|
||||||
warn() { printf '%s\n' "${Y}!${N} $*" >&2; } # сразу (диалоги, ошибки перед остановкой)
|
warn() { spin_stop; printf '%s\n' "${Y}!${N} $*" >&2; } # сразу (диалоги, ошибки перед остановкой)
|
||||||
|
|
||||||
|
# A-записи домена. Сначала публичный DNS (DoH): локальный резолвер сервера может помнить «записи нет» с тех пор, как её не было,
|
||||||
|
# и kit тогда спорит с тем, что вы видите у регистратора. Нет ответа от публичного DNS – берём системный.
|
||||||
|
dns_a() { # домен → IPv4 по одному в строке
|
||||||
|
local r
|
||||||
|
r=$(curl -fsS -m 6 -H 'accept: application/dns-json' "https://1.1.1.1/dns-query?name=$1&type=A" 2>/dev/null \
|
||||||
|
| grep -oE '"data":"[0-9]{1,3}(\.[0-9]{1,3}){3}"' | cut -d'"' -f4 | sort -u || true)
|
||||||
|
[[ -n $r ]] || r=$(getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u || true)
|
||||||
|
printf '%s' "$r"
|
||||||
|
}
|
||||||
later() { WARNINGS+=("$*"); } # в блок «Внимание» в конце
|
later() { WARNINGS+=("$*"); } # в блок «Внимание» в конце
|
||||||
die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
|
die() { spin_stop; printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Оформление вывода: ожидание и советы ---------------------------------------------------------
|
||||||
|
# Анимация ожидания: пока скрипт занят долгой работой без вывода (опрос соседей по подсети, установка пакетов),
|
||||||
|
# крутится индикатор и идут секунды, чтобы не казалось, что всё зависло. Без терминала (лог, pipe) – одна строка.
|
||||||
|
SPIN_PID=""
|
||||||
|
spin_start() { # текст
|
||||||
|
spin_stop
|
||||||
|
if [[ ! -t 2 ]]; then printf '%s…\n' "$1" >&2; return 0; fi
|
||||||
|
local msg=$1 start=$SECONDS parent=$$
|
||||||
|
(
|
||||||
|
local -a fr=('|' '/' '-' '\\') i=0
|
||||||
|
case ${LC_ALL:-${LC_CTYPE:-${LANG:-}}} in *[Uu][Tt][Ff]-8* | *[Uu][Tt][Ff]8*) fr=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) ;; esac
|
||||||
|
while kill -0 "$parent" 2>/dev/null; do
|
||||||
|
printf '\r %s %s %s с ' "${fr[i % ${#fr[@]}]}" "$msg" "$((SECONDS - start))" >&2
|
||||||
|
i=$((i + 1)); sleep 0.1
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
SPIN_PID=$!
|
||||||
|
}
|
||||||
|
spin_stop() {
|
||||||
|
[[ -n $SPIN_PID ]] || return 0
|
||||||
|
kill "$SPIN_PID" 2>/dev/null || true
|
||||||
|
wait "$SPIN_PID" 2>/dev/null || true
|
||||||
|
SPIN_PID=""
|
||||||
|
[[ -t 2 ]] && printf '\r\033[K' >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
# Блок «Что дальше»: советы и рекомендации отделены от хода работы рамкой и нумерацией. Пункты – обычные фразы,
|
||||||
|
# без команд: пользователя ведём в меню kit.
|
||||||
|
tips() { # заголовок, затем пункты
|
||||||
|
local title=$1 n=0 item row first
|
||||||
|
shift
|
||||||
|
printf '\n%s┌─%s %s%s%s\n' "$D" "$N" "$B" "$title" "$N"
|
||||||
|
for item in "$@"; do
|
||||||
|
n=$((n + 1)); first=yes
|
||||||
|
while IFS= read -r row; do
|
||||||
|
if [[ $first == yes ]]; then printf '%s│%s %s%d.%s %s\n' "$D" "$N" "$B" "$n" "$N" "$row"; first=no
|
||||||
|
else printf '%s│%s %s\n' "$D" "$N" "$row"; fi
|
||||||
|
done < <(if command -v python3 >/dev/null 2>&1; then python3 -c 'import sys,textwrap; t=sys.argv[1].replace(" → ","\u00a0→\u00a0"); print("\n".join(textwrap.wrap(t, 74, break_on_hyphens=False, break_long_words=False)).replace("\u00a0"," "))' "$item"; else printf '%s\n' "$item"; fi)
|
||||||
|
done
|
||||||
|
printf '%s└─%s\n' "$D" "$N"
|
||||||
|
}
|
||||||
trap 'die "Ошибка в строке $LINENO. Исправьте причину и запустите скрипт ещё раз."' ERR
|
trap 'die "Ошибка в строке $LINENO. Исправьте причину и запустите скрипт ещё раз."' ERR
|
||||||
|
|
||||||
rand_str() { openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$1"; }
|
rand_str() { openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$1"; }
|
||||||
@@ -201,7 +261,7 @@ nearby_sites() {
|
|||||||
domain_points_here() { # домен
|
domain_points_here() { # домен
|
||||||
local me=$HOST ips
|
local me=$HOST ips
|
||||||
[[ $me =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || me=$(public_ip)
|
[[ $me =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || me=$(public_ip)
|
||||||
ips=$(getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u)
|
ips=$(dns_a "$1")
|
||||||
if [[ -z $ips ]]; then
|
if [[ -z $ips ]]; then
|
||||||
warn "У домена $1 нет A-записи. Добавьте её у регистратора: тип A, значение $me."
|
warn "У домена $1 нет A-записи. Добавьте её у регистратора: тип A, значение $me."
|
||||||
elif ! grep -qx "$me" <<<"$ips"; then
|
elif ! grep -qx "$me" <<<"$ips"; then
|
||||||
@@ -224,22 +284,75 @@ panel_on_choice() {
|
|||||||
echo "${B}Где открывать панель и подписку?${N}"
|
echo "${B}Где открывать панель и подписку?${N}"
|
||||||
echo " ${B}1${N} По IP https://$HOST/… как раньше ${G}[по умолчанию]${N}"
|
echo " ${B}1${N} По IP https://$HOST/… как раньше ${G}[по умолчанию]${N}"
|
||||||
echo " ${B}2${N} По домену https://$DOMAIN/… сертификат домена, ссылка не зависит от IP"
|
echo " ${B}2${N} По домену https://$DOMAIN/… сертификат домена, ссылка не зависит от IP"
|
||||||
echo " ${D}Менять можно потом: kit net panel domain | ip${N}"
|
echo " ${D}Менять можно потом: kit → Протоколы, порты и маскировка → Панель и подписка: по IP или по домену${N}"
|
||||||
ask_tty "Выбор [1]: " || return 0
|
ask_tty "Выбор [1]: " || return 0
|
||||||
REPLY=${REPLY//[[:space:]]/}
|
REPLY=${REPLY//[[:space:]]/}
|
||||||
[[ ${REPLY%.} == 2 ]] && PANEL_ON=domain
|
[[ ${REPLY%.} == 2 ]] && PANEL_ON=domain
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Вопрос пользователю: свой домен, сосед по подсети (по умолчанию) или свой сайт.
|
# Вопрос пользователю: все протоколы (стандарт) или только нужные. Меньше протоколов – меньше памяти и портов.
|
||||||
|
choose_protocols() {
|
||||||
|
local -a names=(
|
||||||
|
"reality|REALITY (TCP 443)|основной, самый устойчивый"
|
||||||
|
"reality2|REALITY №2|запасной, со своим сайтом маскировки"
|
||||||
|
"hy2|Hysteria2 (UDP)|быстрый на плохих каналах"
|
||||||
|
"xhttp|XHTTP + REALITY|если обычный REALITY у вас не подключается"
|
||||||
|
"ws|VLESS WebSocket|через CDN"
|
||||||
|
"trojan|Trojan|"
|
||||||
|
"vmess|VMess|"
|
||||||
|
"ss|Shadowsocks|"
|
||||||
|
"tuic|TUIC (UDP)|"
|
||||||
|
"awg|AmneziaWG|"
|
||||||
|
"awg3|AmneziaWG 3.1|"
|
||||||
|
"mtproto|MTProto для Telegram|"
|
||||||
|
"wg|WireGuard|"
|
||||||
|
)
|
||||||
|
echo
|
||||||
|
echo "${B}Какие протоколы ставить?${N}"
|
||||||
|
echo
|
||||||
|
echo " ${B}1${N} Стандарт основной набор ${G}[по умолчанию]${N}"
|
||||||
|
echo " ${B}2${N} На выбор только нужные: сервер слабее нагружен, портов меньше"
|
||||||
|
echo " ${D}Потом можно включить или выключить: kit → Протоколы, порты и маскировка → Выключить или включить протокол${N}"
|
||||||
|
ask_tty "Выбор [1]: " || return 0
|
||||||
|
REPLY=${REPLY//[[:space:]]/}
|
||||||
|
[[ ${REPLY%.} == 2 ]] || return 0
|
||||||
|
echo
|
||||||
|
local i=0 e
|
||||||
|
for e in "${names[@]}"; do
|
||||||
|
i=$((i + 1)); IFS='|' read -r _ title note <<<"$e"
|
||||||
|
printf ' %s%2d%s %s%s\n' "$B" "$i" "$N" "$title" "${note:+ $D– $note$N}"
|
||||||
|
done
|
||||||
|
while :; do
|
||||||
|
ask_tty "Номера через запятую, например 1,3,12 (Enter – стандарт): " || return 0
|
||||||
|
REPLY=${REPLY//[[:space:]]/}
|
||||||
|
[[ -n $REPLY ]] || return 0
|
||||||
|
local -a pick=() chosen=(); local bad="" n key
|
||||||
|
IFS=, read -ra pick <<<"$REPLY"
|
||||||
|
for n in "${pick[@]}"; do
|
||||||
|
if [[ $n =~ ^[0-9]+$ ]] && (( n >= 1 && n <= ${#names[@]} )); then
|
||||||
|
key=${names[n-1]%%|*}
|
||||||
|
[[ " ${chosen[*]:-} " == *" $key "* ]] || chosen+=("$key")
|
||||||
|
else bad=$n; break; fi
|
||||||
|
done
|
||||||
|
[[ -z $bad ]] || { warn "«$bad» – нет такого номера. Нужны числа от 1 до ${#names[@]}."; continue; }
|
||||||
|
((${#chosen[@]})) || { warn "Выберите хотя бы один протокол: например 1,3."; continue; }
|
||||||
|
PROTOS=("${chosen[@]}")
|
||||||
|
echo " Ставлю: ${PROTOS[*]}"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Вопрос пользователю: свой домен, сосед по подсети (по умолчанию) или другой сайт.
|
||||||
choose_masking() {
|
choose_masking() {
|
||||||
echo
|
echo
|
||||||
echo "${B}Под какой сайт маскировать сервер?${N}"
|
echo "${B}Под какой сайт маскировать сервер?${N}"
|
||||||
echo "Со стороны он будет выглядеть как обычный сайт."
|
echo "Со стороны он будет выглядеть как обычный сайт."
|
||||||
echo
|
echo
|
||||||
echo " ${B}1${N} Свой домен самый надёжный: нужен домен и свободный порт 80 ${G}[рекомендуем]${N}"
|
echo " ${B}1${N} Свой домен ваш домен на этот сервер: самый надёжный ${G}[рекомендуем]${N}"
|
||||||
|
echo " ${D}(нужна A-запись домена и свободный порт 80)${N}"
|
||||||
echo " ${B}2${N} Сосед по подсети подберу сам, домен не нужен ${G}[рекомендуем]${N}"
|
echo " ${B}2${N} Сосед по подсети подберу сам, домен не нужен ${G}[рекомендуем]${N}"
|
||||||
echo " ${B}3${N} Свой сайт вы вводите адрес сами"
|
echo " ${B}3${N} Другой сайт вы сами вводите адрес любого сайта (подходит хуже соседа)"
|
||||||
echo " ${D}Сертификат своего домена виден в публичных журналах сертификатов: связь «домен – сервер» не скрыта.${N}"
|
echo " ${D}Сертификат своего домена виден в публичных журналах сертификатов: связь «домен – сервер» не скрыта.${N}"
|
||||||
echo
|
echo
|
||||||
local d="" prev=""
|
local d="" prev=""
|
||||||
@@ -278,6 +391,7 @@ choose_masking() {
|
|||||||
# Продлевает его тот же cron, а после продления nginx перечитывает сертификат.
|
# Продлевает его тот же cron, а после продления nginx перечитывает сертификат.
|
||||||
issue_domain_cert() {
|
issue_domain_cert() {
|
||||||
local acme=/root/.acme.sh/acme.sh rc=0 log=/var/log/kit-domain-cert.log
|
local acme=/root/.acme.sh/acme.sh rc=0 log=/var/log/kit-domain-cert.log
|
||||||
|
ufw_open_80
|
||||||
if [[ -s $DOMAIN_CERT_DIR/fullchain.pem && -s $DOMAIN_CERT_DIR/privkey.pem ]] \
|
if [[ -s $DOMAIN_CERT_DIR/fullchain.pem && -s $DOMAIN_CERT_DIR/privkey.pem ]] \
|
||||||
&& openssl x509 -in "$DOMAIN_CERT_DIR/fullchain.pem" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q 'does match' \
|
&& openssl x509 -in "$DOMAIN_CERT_DIR/fullchain.pem" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q 'does match' \
|
||||||
&& openssl x509 -in "$DOMAIN_CERT_DIR/fullchain.pem" -noout -issuer 2>/dev/null | grep -q "Let's Encrypt" \
|
&& openssl x509 -in "$DOMAIN_CERT_DIR/fullchain.pem" -noout -issuer 2>/dev/null | grep -q "Let's Encrypt" \
|
||||||
@@ -299,18 +413,16 @@ issue_domain_cert() {
|
|||||||
say "Получаю сертификат Let's Encrypt для ${B}$DOMAIN${N}"
|
say "Получаю сертификат Let's Encrypt для ${B}$DOMAIN${N}"
|
||||||
install -m 600 /dev/null "$log"
|
install -m 600 /dev/null "$log"
|
||||||
# Сбои бывают временными (например, ошибка TLS при скачивании готового сертификата): до трёх попыток.
|
# Сбои бывают временными (например, ошибка TLS при скачивании готового сертификата): до трёх попыток.
|
||||||
if [[ $need_issue == yes ]]; then
|
local try
|
||||||
local try
|
spin_start "Жду ответа Let's Encrypt"
|
||||||
for try in 1 2 3; do
|
for try in 1 2 3; do
|
||||||
rc=0
|
rc=0
|
||||||
"$acme" --issue -d "$DOMAIN" --standalone --httpport 80 --server letsencrypt --keylength ec-256 ${rc_force:-} >>"$log" 2>&1 || rc=$?
|
"$acme" --issue -d "$DOMAIN" --standalone --httpport 80 --server letsencrypt --keylength ec-256 ${rc_force:-} >>"$log" 2>&1 || rc=$?
|
||||||
[[ $rc == 0 || $rc == 2 ]] && break
|
[[ $rc == 0 || $rc == 2 ]] && break
|
||||||
local rc_force="--force"
|
local rc_force="--force"
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
else
|
spin_stop
|
||||||
rc=2 # сертификат взяли готовый: для кода ниже это то же, что «уже свежий»
|
|
||||||
fi
|
|
||||||
# 2 – сертификат уже свежий, выпускать заново не нужно.
|
# 2 – сертификат уже свежий, выпускать заново не нужно.
|
||||||
[[ $rc == 0 || $rc == 2 ]] || {
|
[[ $rc == 0 || $rc == 2 ]] || {
|
||||||
# Наш шов: лимит Let's Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.
|
# Наш шов: лимит Let's Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.
|
||||||
@@ -339,11 +451,21 @@ issue_domain_cert() {
|
|||||||
ip_cert_self_signed_fallback() {
|
ip_cert_self_signed_fallback() {
|
||||||
local san="IP:$HOST"
|
local san="IP:$HOST"
|
||||||
[[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"
|
[[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"
|
||||||
|
# Наш шов: если причина — лимит Let's Encrypt, говорим это прямо и с датой повтора, как для домена.
|
||||||
|
# Иначе человек читает «частые причины» и ищет проблему у хостера, которой нет.
|
||||||
|
local till
|
||||||
|
till=$(grep -rhoE 'retry after [0-9-]+ [0-9:]+ UTC' /var/log/3x-ui-install.log "$HOME"/.acme.sh/*/ca 2>/dev/null | tail -1 | sed 's/^retry after //')
|
||||||
|
if grep -rqsE 'rateLimited|too many certificates' /var/log/3x-ui-install.log 2>/dev/null; then
|
||||||
|
warn "Причина: лимит Let's Encrypt — на этот IP за 7 суток уже выпущено 5 сертификатов${till:+; повтор возможен после $till}. Это ограничение Let's Encrypt, а не вашего сервера: порт 80 и файрвол тут не при чём."
|
||||||
|
else
|
||||||
warn "Let's Encrypt не выдал сертификат на IP $HOST. Частые причины:"
|
warn "Let's Encrypt не выдал сертификат на IP $HOST. Частые причины:"
|
||||||
{
|
{
|
||||||
echo " – лимит Let's Encrypt: 5 сертификатов в неделю на один IP;"
|
echo " – лимит Let's Encrypt: 5 сертификатов в неделю на один IP;"
|
||||||
echo " – входящий порт 80 закрыт (у хостера или в файрволе);"
|
echo " – входящий порт 80 закрыт (у хостера или в файрволе);"
|
||||||
echo " – сбой у самого Let's Encrypt (лог: /var/log/3x-ui-install.log)."
|
echo " – сбой у самого Let's Encrypt (лог: /var/log/3x-ui-install.log)."
|
||||||
|
} >&2
|
||||||
|
fi
|
||||||
|
{
|
||||||
echo " Можно продолжить с самоподписанным сертификатом: ссылки на подключения работают, браузер покажет"
|
echo " Можно продолжить с самоподписанным сертификатом: ссылки на подключения работают, браузер покажет"
|
||||||
echo " предупреждение, а подписка в приложениях может не открыться."
|
echo " предупреждение, а подписка в приложениях может не открыться."
|
||||||
} >&2
|
} >&2
|
||||||
@@ -368,6 +490,16 @@ ip_cert_self_signed_fallback() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Сайт default в sites-enabled – нетронутый файл из пакета nginx (md5 сверяем с тем, что записал dpkg)?
|
||||||
|
# Нет файла – тоже да: прошлый запуск kit его уже убрал. Правленый default – чужой сайт.
|
||||||
|
nginx_default_stock() {
|
||||||
|
local f=/etc/nginx/sites-enabled/default want got
|
||||||
|
[[ -e $f ]] || return 0
|
||||||
|
want=$(dpkg-query -W -f='${Conffiles}\n' nginx-common 2>/dev/null | awk '$1 == "/etc/nginx/sites-available/default" {print $2}') || true
|
||||||
|
got=$(md5sum <"$f" 2>/dev/null | awk '{print $1}') || true
|
||||||
|
[[ -n $want && $got == "$want" ]]
|
||||||
|
}
|
||||||
|
|
||||||
# Куда REALITY отправляет чужих гостей: для своего домена – на nginx этого сервера.
|
# Куда REALITY отправляет чужих гостей: для своего домена – на nginx этого сервера.
|
||||||
reality_target() { # сайт
|
reality_target() { # сайт
|
||||||
if [[ -n $DOMAIN && $1 == "$DOMAIN" ]]; then echo "127.0.0.1:${INNER[selfweb]}"; else echo "$1:443"; fi
|
if [[ -n $DOMAIN && $1 == "$DOMAIN" ]]; then echo "127.0.0.1:${INNER[selfweb]}"; else echo "$1:443"; fi
|
||||||
@@ -378,9 +510,9 @@ reality_target() { # сайт
|
|||||||
api() { # METHOD path [json]
|
api() { # METHOD path [json]
|
||||||
local url="$API/$2" out
|
local url="$API/$2" out
|
||||||
if [[ $1 == GET ]]; then
|
if [[ $1 == GET ]]; then
|
||||||
out=$(curl -fsSk -m 20 -H "Authorization: Bearer $TOKEN" "$url")
|
out=$(curl -fsSk -m 20 -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") "$url")
|
||||||
else
|
else
|
||||||
out=$(curl -fsSk -m 20 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X "$1" -d "$3" "$url")
|
out=$(curl -fsSk -m 20 -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") -H 'Content-Type: application/json' -X "$1" --data-binary @<(printf '%s' "$3") "$url")
|
||||||
fi
|
fi
|
||||||
[[ $(jq -r '.success' <<<"$out") == true ]] || die "Панель ответила ошибкой на $2: $(jq -r '.msg // .' <<<"$out" | head -c 300)"
|
[[ $(jq -r '.success' <<<"$out") == true ]] || die "Панель ответила ошибкой на $2: $(jq -r '.msg // .' <<<"$out" | head -c 300)"
|
||||||
jq -c '.obj' <<<"$out"
|
jq -c '.obj' <<<"$out"
|
||||||
@@ -389,7 +521,7 @@ api() { # METHOD path [json]
|
|||||||
wait_panel() {
|
wait_panel() {
|
||||||
local i
|
local i
|
||||||
for i in $(seq 1 60); do
|
for i in $(seq 1 60); do
|
||||||
curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $TOKEN" "$API/server/getNewUUID" 2>/dev/null && return 0
|
curl -fsk -m 5 -o /dev/null -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") "$API/server/getNewUUID" 2>/dev/null && return 0
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
die "Панель не отвечает. Лог: journalctl -u x-ui -n 50"
|
die "Панель не отвечает. Лог: journalctl -u x-ui -n 50"
|
||||||
@@ -449,7 +581,7 @@ main() {
|
|||||||
die "3X-UI уже установлена другим способом – не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
|
die "3X-UI уже установлена другим способом – не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey="" multi=no restore=""
|
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all protos_given=no ucert="" ukey="" multi=no restore=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--port | --sni | --panel-ssl | --host | --user | --protocols | --domain | --cert | --key | --restore | --panel-on)
|
--port | --sni | --panel-ssl | --host | --user | --protocols | --domain | --cert | --key | --restore | --panel-on)
|
||||||
@@ -461,7 +593,7 @@ main() {
|
|||||||
--panel-ssl) PANEL_SSL=$2; shift 2 ;;
|
--panel-ssl) PANEL_SSL=$2; shift 2 ;;
|
||||||
--host) HOST=$2; shift 2 ;;
|
--host) HOST=$2; shift 2 ;;
|
||||||
--user) NAME=$2; shift 2 ;;
|
--user) NAME=$2; shift 2 ;;
|
||||||
--protocols) protos=$2; shift 2 ;;
|
--protocols) protos=$2; protos_given=yes; shift 2 ;;
|
||||||
--domain) DOMAIN=$2; shift 2 ;;
|
--domain) DOMAIN=$2; shift 2 ;;
|
||||||
--cert) ucert=$2; shift 2 ;;
|
--cert) ucert=$2; shift 2 ;;
|
||||||
--multi-port) multi=yes; shift ;;
|
--multi-port) multi=yes; shift ;;
|
||||||
@@ -499,7 +631,7 @@ main() {
|
|||||||
[[ $PANEL_ON == ip || $PANEL_ON == domain ]] || die "--panel-on: ip или domain"
|
[[ $PANEL_ON == ip || $PANEL_ON == domain ]] || die "--panel-on: ip или domain"
|
||||||
[[ $PANEL_ON == ip || -n $DOMAIN || $yes == no ]] || die "--panel-on domain работает только со своим доменом (--domain vpn.example.com)."
|
[[ $PANEL_ON == ip || -n $DOMAIN || $yes == no ]] || die "--panel-on domain работает только со своим доменом (--domain vpn.example.com)."
|
||||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$ ]] || die "--host: нужен IP (например 1.2.3.4) или домен"
|
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$ ]] || die "--host: нужен IP (например 1.2.3.4) или домен"
|
||||||
[[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)."
|
[[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ && $NAME =~ [^.] ]] || die "Имя: латиница, цифры, _ . - (до 32 символов, не только точки)."
|
||||||
[[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none"
|
[[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none"
|
||||||
if [[ -n $ucert || -n $ukey ]]; then
|
if [[ -n $ucert || -n $ukey ]]; then
|
||||||
[[ -s $ucert && -s $ukey ]] || die "Нужны оба файла: --cert fullchain.pem --key privkey.pem"
|
[[ -s $ucert && -s $ukey ]] || die "Нужны оба файла: --cert fullchain.pem --key privkey.pem"
|
||||||
@@ -520,6 +652,24 @@ main() {
|
|||||||
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт – укажите другой: --port 8443"
|
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт – укажите другой: --port 8443"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Прошлый запуск мог упасть после установки nginx: его сайт по умолчанию держит порт 80 и мешает выпустить сертификат.
|
||||||
|
# $XUI_ENV без $RESULT (иначе выше остановились бы) или наш kit.conf – след незавершённого запуска. kit.conf порт 80
|
||||||
|
# не слушает и остаётся после сбоя на nginx -t, поэтому чужими считаем всё, кроме него и нетронутого default из пакета;
|
||||||
|
# свои server прямо в nginx.conf – тоже чужой сайт.
|
||||||
|
if [[ -f $XUI_ENV || -f /etc/nginx/conf.d/kit.conf ]] && ss -H -ltnp "sport = :80" 2>/dev/null | grep -q nginx \
|
||||||
|
&& [[ -z $(find /etc/nginx/conf.d -mindepth 1 -name '*.conf' ! -name kit.conf 2>/dev/null) ]] \
|
||||||
|
&& [[ -z $(find /etc/nginx/sites-enabled -mindepth 1 ! -name default 2>/dev/null) ]] && nginx_default_stock \
|
||||||
|
&& ! grep -qE '^[[:space:]]*server[[:space:]]*\{' /etc/nginx/nginx.conf 2>/dev/null; then
|
||||||
|
say "Остановил стандартный сайт nginx от прошлого запуска: он занимал порт 80"
|
||||||
|
systemctl stop nginx >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
# Повтор после сбоя без --cert: ваш сертификат из прошлого запуска лежит в /root/cert/custom – берём его,
|
||||||
|
# а не просим Let's Encrypt (повторный запуск его не выпускает: это делает установщик 3X-UI один раз).
|
||||||
|
if [[ $PANEL_SSL == auto && -f $XUI_ENV && -s /root/cert/custom/fullchain.pem && -s /root/cert/custom/privkey.pem ]] \
|
||||||
|
&& openssl x509 -in /root/cert/custom/fullchain.pem -noout -checkend 0 >/dev/null 2>&1; then
|
||||||
|
ucert=/root/cert/custom/fullchain.pem; ukey=/root/cert/custom/privkey.pem; PANEL_SSL=custom
|
||||||
|
say "Беру ваш сертификат из прошлого запуска: /root/cert/custom"
|
||||||
|
fi
|
||||||
if [[ $PANEL_SSL == auto ]]; then
|
if [[ $PANEL_SSL == auto ]]; then
|
||||||
if port_busy 80 tcp; then
|
if port_busy 80 tcp; then
|
||||||
PANEL_SSL=none
|
PANEL_SSL=none
|
||||||
@@ -534,8 +684,9 @@ main() {
|
|||||||
[[ $PANEL_SSL == ip || $PANEL_SSL == custom ]] && TRUSTED=yes
|
[[ $PANEL_SSL == ip || $PANEL_SSL == custom ]] && TRUSTED=yes
|
||||||
if [[ $PANEL_SSL == custom ]]; then
|
if [[ $PANEL_SSL == custom ]]; then
|
||||||
mkdir -p /root/cert/custom
|
mkdir -p /root/cert/custom
|
||||||
install -m 644 "$ucert" /root/cert/custom/fullchain.pem
|
# Те же файлы (повторный запуск) install не копирует сам в себя – только права.
|
||||||
install -m 600 "$ukey" /root/cert/custom/privkey.pem
|
if [[ $ucert -ef /root/cert/custom/fullchain.pem ]]; then chmod 644 "$ucert"; else install -m 644 "$ucert" /root/cert/custom/fullchain.pem; fi
|
||||||
|
if [[ $ukey -ef /root/cert/custom/privkey.pem ]]; then chmod 600 "$ukey"; else install -m 600 "$ukey" /root/cert/custom/privkey.pem; fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kit_banner
|
kit_banner
|
||||||
@@ -543,7 +694,7 @@ main() {
|
|||||||
echo "Поставлю панель 3X-UI и протоколы на этот сервер. Займёт 3–5 минут."
|
echo "Поставлю панель 3X-UI и протоколы на этот сервер. Займёт 3–5 минут."
|
||||||
if ! command -v curl >/dev/null || ! command -v openssl >/dev/null; then
|
if ! command -v curl >/dev/null || ! command -v openssl >/dev/null; then
|
||||||
export DEBIAN_FRONTEND=noninteractive; wait_apt_idle
|
export DEBIAN_FRONTEND=noninteractive; wait_apt_idle
|
||||||
apt-get update -qq; apt-get install -y -qq curl openssl ca-certificates iproute2 >/dev/null
|
apt_refresh; apt-get install -y -qq curl openssl ca-certificates iproute2 >/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local host_given=${HOST:+yes}
|
local host_given=${HOST:+yes}
|
||||||
@@ -554,6 +705,7 @@ main() {
|
|||||||
mem_gb=$(awk '/MemTotal/ {printf "%.1f", $2 / 1048576}' /proc/meminfo 2>/dev/null || echo "?")
|
mem_gb=$(awk '/MemTotal/ {printf "%.1f", $2 / 1048576}' /proc/meminfo 2>/dev/null || echo "?")
|
||||||
echo
|
echo
|
||||||
echo "Сервер: ${B}$HOST${N} · $os_name · ${mem_gb} ГБ памяти"
|
echo "Сервер: ${B}$HOST${N} · $os_name · ${mem_gb} ГБ памяти"
|
||||||
|
[[ -f $XUI_ENV && ! -f $RESULT ]] && echo "${D}Нашёл следы прерванной установки: продолжу с ними. Ответы на вопросы ниже нужны заново, прошлые выборы не сохраняются.${N}"
|
||||||
# IP определился снаружи, а ссылки получат именно его: у части хостеров это не адрес самого сервера.
|
# IP определился снаружи, а ссылки получат именно его: у части хостеров это не адрес самого сервера.
|
||||||
if [[ -z $host_given && $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
if [[ -z $host_given && $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
||||||
ask_tty "Адрес в ссылках: $HOST. Верно? Enter – да, или введите другой IPv4: " || true
|
ask_tty "Адрес в ссылках: $HOST. Верно? Enter – да, или введите другой IPv4: " || true
|
||||||
@@ -604,6 +756,8 @@ kl_sni_menu() {
|
|||||||
if ((i == known_idx)); then
|
if ((i == known_idx)); then
|
||||||
printf '\nОбщедоступные сайты:\n' >&2
|
printf '\nОбщедоступные сайты:\n' >&2
|
||||||
for k in "${!known[@]}"; do printf ' %d - %s\n' "$((k + 1))" "${known[k]}" >&2; done
|
for k in "${!known[@]}"; do printf ' %d - %s\n' "$((k + 1))" "${known[k]}" >&2; done
|
||||||
|
# Наш шов: последним пунктом — автовыбор из этого списка (раньше выбор был только по номеру).
|
||||||
|
printf ' %d - автовыбор из списка\n' "$((${#known[@]} + 1))" >&2
|
||||||
printf ' 0 - назад\nВаш выбор: ' >&2
|
printf ' 0 - назад\nВаш выбор: ' >&2
|
||||||
ans=""
|
ans=""
|
||||||
read -r -t 300 ans <"$src" 2>/dev/null || true
|
read -r -t 300 ans <"$src" 2>/dev/null || true
|
||||||
@@ -611,6 +765,11 @@ kl_sni_menu() {
|
|||||||
if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then
|
if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then
|
||||||
printf 'sni:%s' "${known[10#$ans - 1]}"; return 0
|
printf 'sni:%s' "${known[10#$ans - 1]}"; return 0
|
||||||
fi
|
fi
|
||||||
|
if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans == ${#known[@]} + 1)); then
|
||||||
|
local autopick="${known[RANDOM % ${#known[@]}]}"
|
||||||
|
printf ' автовыбор: %s\n' "$autopick" >&2
|
||||||
|
printf 'sni:%s' "$autopick"; return 0
|
||||||
|
fi
|
||||||
printf ' ничего не выбрано — оставляю автовыбор\n' >&2
|
printf ' ничего не выбрано — оставляю автовыбор\n' >&2
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -628,13 +787,18 @@ kl_sni_menu() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Протоколы: из флага --protocols, по вопросу пользователю или стандартный набор.
|
||||||
|
if [[ $protos_given == no && $yes == no && -z $restore && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
||||||
|
choose_protocols
|
||||||
|
fi
|
||||||
|
|
||||||
# Маскировка: из флагов, по вопросу пользователю или автоматически (сосед по подсети).
|
# Маскировка: из флагов, по вопросу пользователю или автоматически (сосед по подсети).
|
||||||
if [[ -n $DOMAIN ]]; then
|
if [[ -n $DOMAIN ]]; then
|
||||||
[[ $PANEL_SSL == ip ]] || die "Свой домен работает с сертификатом панели Let's Encrypt на IP: освободите порт 80 и не указывайте --cert, --key и --panel-ssl none (сертификат для домена установщик получит сам)."
|
[[ $PANEL_SSL == ip ]] || die "Свой домен работает с сертификатом панели Let's Encrypt на IP: освободите порт 80 и не указывайте --cert, --key и --panel-ssl none (сертификат для домена установщик получит сам)."
|
||||||
domain_points_here "$DOMAIN" || die "Исправьте A-запись домена и запустите скрипт снова (DNS обновляется от нескольких минут до нескольких часов)."
|
domain_points_here "$DOMAIN" || die "Исправьте A-запись домена и запустите скрипт снова (DNS обновляется от нескольких минут до нескольких часов)."
|
||||||
# Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе
|
# Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе
|
||||||
# человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.
|
# человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.
|
||||||
elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && ! -f $XUI_ENV && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
||||||
choose_masking
|
choose_masking
|
||||||
fi
|
fi
|
||||||
if [[ -n $DOMAIN ]]; then
|
if [[ -n $DOMAIN ]]; then
|
||||||
@@ -644,7 +808,9 @@ kl_sni_menu() {
|
|||||||
echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."
|
echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."
|
||||||
echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."
|
echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."
|
||||||
local -a nb=()
|
local -a nb=()
|
||||||
|
spin_start "Опрашиваю соседние адреса"
|
||||||
mapfile -t nb < <(nearby_sites | shuf)
|
mapfile -t nb < <(nearby_sites | shuf)
|
||||||
|
spin_stop
|
||||||
NB_ALL=("${nb[@]}")
|
NB_ALL=("${nb[@]}")
|
||||||
# Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.
|
# Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.
|
||||||
if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : </dev/tty; } 2>/dev/null; then
|
if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : </dev/tty; } 2>/dev/null; then
|
||||||
@@ -654,16 +820,32 @@ kl_sni_menu() {
|
|||||||
own:*)
|
own:*)
|
||||||
own_sni=yes
|
own_sni=yes
|
||||||
DOMAIN=${pick#own:}
|
DOMAIN=${pick#own:}
|
||||||
if domain_points_here "$DOMAIN" 2>/dev/null; then
|
# Наш шов: домен вводится заново, пока он не указывает на этот сервер. Раньше при промахе
|
||||||
SNI=$DOMAIN
|
# установка молча маскировалась под чужой сайт, а при домене из флага — обрывалась.
|
||||||
ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let's Encrypt выпустит установщик)"
|
# A-запись у регистратора расходится не мгновенно, поэтому первым пунктом — «проверить снова».
|
||||||
# Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.
|
while :; do
|
||||||
# У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой
|
if domain_points_here "$DOMAIN"; then
|
||||||
# домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.
|
SNI=$DOMAIN
|
||||||
panel_on_choice
|
ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let's Encrypt выпустит установщик)"
|
||||||
else
|
# Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.
|
||||||
SNI=$DOMAIN; later "домен $DOMAIN не указывает на этот сервер — маскируюсь под него как под чужой сайт, панель и подписка остаются по IP"
|
# У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой
|
||||||
fi
|
# домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.
|
||||||
|
panel_on_choice
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo " Домен ${B}$DOMAIN${N} пока не указывает на этот сервер." >&2
|
||||||
|
echo " ${B}1${N} проверить снова A-запись расходится по DNS от нескольких минут до нескольких часов" >&2
|
||||||
|
echo " ${B}2${N} ввести другой домен" >&2
|
||||||
|
echo " ${B}3${N} без своего домена: маскировка соседом по подсети" >&2
|
||||||
|
ask_tty " Выбор [1]: " || break
|
||||||
|
case ${REPLY//[[:space:]]/} in
|
||||||
|
2) ask_tty " адрес сайта (например vpn.example.com): " || break
|
||||||
|
local d=${REPLY//[[:space:]]/}
|
||||||
|
[[ -n $d ]] && DOMAIN=$d ;;
|
||||||
|
3) own_sni=no; DOMAIN=""; break ;;
|
||||||
|
*) sleep 5 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
;;
|
;;
|
||||||
sni:*) SNI=${pick#sni:} ;;
|
sni:*) SNI=${pick#sni:} ;;
|
||||||
esac
|
esac
|
||||||
@@ -696,7 +878,7 @@ kl_sni_menu() {
|
|||||||
ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true
|
ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true
|
||||||
[[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент."
|
[[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент."
|
||||||
else
|
else
|
||||||
later "Сайт для маскировки взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site."
|
later "Сайт для маскировки взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit → Протоколы, порты и маскировка → Сменить сайт маскировки."
|
||||||
fi
|
fi
|
||||||
local -a pool=()
|
local -a pool=()
|
||||||
mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
|
mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
|
||||||
@@ -722,7 +904,9 @@ kl_sni_menu() {
|
|||||||
nb2=("${NB_ALL[@]}")
|
nb2=("${NB_ALL[@]}")
|
||||||
else
|
else
|
||||||
echo "Подбираю сайты для XHTTP и MTProto… (около 250 коротких обращений к порту 443 соседних адресов вашей подсети)"
|
echo "Подбираю сайты для XHTTP и MTProto… (около 250 коротких обращений к порту 443 соседних адресов вашей подсети)"
|
||||||
|
spin_start "Опрашиваю соседние адреса"
|
||||||
mapfile -t nb2 < <(nearby_sites | shuf)
|
mapfile -t nb2 < <(nearby_sites | shuf)
|
||||||
|
spin_stop
|
||||||
fi
|
fi
|
||||||
for s in "${nb2[@]}"; do
|
for s in "${nb2[@]}"; do
|
||||||
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
||||||
@@ -738,17 +922,14 @@ kl_sni_menu() {
|
|||||||
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
|
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
|
||||||
done
|
done
|
||||||
SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com}
|
SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com}
|
||||||
if [[ $TRUSTED == yes && $multi == no && ( $SNI2 =~ $SNI_BRAND_RE || $SNI3 =~ $SNI_BRAND_RE ) ]]; then
|
|
||||||
# Наш шов: про XHTTP и MTProto говорим только если они выбраны — иначе это шум.
|
|
||||||
[[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]] && \
|
|
||||||
later "XHTTP и MTProto используют известные сайты из общего списка (достаточно соседей по подсети не нашлось). Заменить: kit net site"
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "Пакеты"
|
step "Пакеты"
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
wait_apt_idle
|
wait_apt_idle
|
||||||
apt-get update -qq
|
apt_refresh
|
||||||
|
spin_start "Ставлю пакеты"
|
||||||
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron unzip >/dev/null
|
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron unzip >/dev/null
|
||||||
|
spin_stop
|
||||||
ok "curl, jq, openssl, qrencode, ufw и другое"
|
ok "curl, jq, openssl, qrencode, ufw и другое"
|
||||||
|
|
||||||
# --- официальный установщик 3X-UI с закреплённой версией ---
|
# --- официальный установщик 3X-UI с закреплённой версией ---
|
||||||
@@ -758,7 +939,9 @@ kl_sni_menu() {
|
|||||||
panel_path=$(rand_str 18)
|
panel_path=$(rand_str 18)
|
||||||
panel_user=$(rand_str 10)
|
panel_user=$(rand_str 10)
|
||||||
panel_pass=$(rand_str 20)
|
panel_pass=$(rand_str 20)
|
||||||
|
local rerun=no
|
||||||
if [[ -f $XUI_ENV ]]; then
|
if [[ -f $XUI_ENV ]]; then
|
||||||
|
rerun=yes
|
||||||
say "3X-UI уже стоит после прошлого запуска – продолжаю с создания подключений"
|
say "3X-UI уже стоит после прошлого запуска – продолжаю с создания подключений"
|
||||||
else
|
else
|
||||||
install_xui "$panel_port" "$panel_path" "$panel_user" "$panel_pass"
|
install_xui "$panel_port" "$panel_path" "$panel_user" "$panel_pass"
|
||||||
@@ -784,7 +967,12 @@ kl_sni_menu() {
|
|||||||
if [[ -n $DOMAIN ]]; then
|
if [[ -n $DOMAIN ]]; then
|
||||||
ip_cert_self_signed_fallback || die "Остановил установку по вашему выбору. Запустите скрипт снова без --domain (панель будет доступна через SSH-туннель) или позже, когда сертификат на IP снова можно будет получить."
|
ip_cert_self_signed_fallback || die "Остановил установку по вашему выбору. Запустите скрипт снова без --domain (панель будет доступна через SSH-туннель) или позже, когда сертификат на IP снова можно будет получить."
|
||||||
else
|
else
|
||||||
later "Let's Encrypt не выдал сертификат на IP $HOST (порт 80 закрыт у хостера, лимит выпусков или сбой). Ставлю без него: панель будет доступна только через SSH-туннель."
|
if [[ $rerun == yes ]]; then
|
||||||
|
# Сертификат на IP выпускает установщик 3X-UI, а при повторном запуске он не работает.
|
||||||
|
later "Сертификата на IP нет: в прошлый запуск его не получили, а повторный запуск его не выпускает. Ставлю без него: панель будет доступна только через SSH-туннель."
|
||||||
|
else
|
||||||
|
later "Let's Encrypt не выдал сертификат на IP $HOST (порт 80 закрыт у хостера, лимит выпусков или сбой). Ставлю без него: панель будет доступна только через SSH-туннель."
|
||||||
|
fi
|
||||||
PANEL_SSL=none
|
PANEL_SSL=none
|
||||||
TRUSTED=no
|
TRUSTED=no
|
||||||
fi
|
fi
|
||||||
@@ -807,6 +995,9 @@ kl_sni_menu() {
|
|||||||
# --- сертификат для протоколов с TLS ---
|
# --- сертификат для протоколов с TLS ---
|
||||||
setup_tls_cert
|
setup_tls_cert
|
||||||
ok "ядро закреплено, сертификат для протоколов с TLS готов"
|
ok "ядро закреплено, сертификат для протоколов с TLS готов"
|
||||||
|
say "${D}Ядро Xray ${XRAY_CORE#v} – не самое новое, и это намеренно.${N}"
|
||||||
|
say "${D}На новых версиях клиенты на sing-box (Karing, Hiddify) перестают подключаться${N}"
|
||||||
|
say "${D}по REALITY и ещё нескольким протоколам. Перейдём на новое, когда это исправят.${N}"
|
||||||
|
|
||||||
# --- подключения: все выбранные протоколы, один subId на пользователя ---
|
# --- подключения: все выбранные протоколы, один subId на пользователя ---
|
||||||
step "Подключения"
|
step "Подключения"
|
||||||
@@ -821,9 +1012,45 @@ kl_sni_menu() {
|
|||||||
SINGLE=yes
|
SINGLE=yes
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
# Повторный запуск после сбоя: REALITY уже за nginx (слушает localhost). С доверенным сертификатом режим «всё на 443»
|
||||||
|
# включился выше, и nginx с картой имён соберётся заново. Без сертификата (или с --multi-port) так нельзя: на 443 никого,
|
||||||
|
# подписку не выдать, а ссылки через nginx клиенты не примут. Честно останавливаемся с подсказкой.
|
||||||
|
if [[ $SINGLE != yes ]] && jq -e 'any(.[]; .remark == "REALITY" and .listen == "127.0.0.1")' <<<"$EXISTING" >/dev/null; then
|
||||||
|
local how="запустите установщик со своим сертификатом: --cert fullchain.pem --key privkey.pem"
|
||||||
|
[[ -s /root/cert/custom/fullchain.pem ]] && how="запустите установщик ещё раз без --panel-ssl none: ваш сертификат из прошлого запуска (/root/cert/custom) возьму сам"
|
||||||
|
[[ -s /root/cert/ip/fullchain.pem ]] && how="освободите порт 80/tcp и запустите установщик ещё раз (без --panel-ssl none): сертификат Let's Encrypt от прошлого запуска на месте"
|
||||||
|
[[ $multi == yes && $TRUSTED == yes ]] && how="запустите установщик ещё раз без --multi-port"
|
||||||
|
die "Прошлый запуск собрал подключения для режима «всё на 443» (за nginx), а ему нужен доверенный сертификат: Let's Encrypt на IP или ваш. Дальше не ставлю – иначе на 443 никого, а подписка не откроется. Что сделать: $how. Или начать с чистого листа: x-ui uninstall, затем установщик заново."
|
||||||
|
fi
|
||||||
if [[ -n $DOMAIN ]]; then
|
if [[ -n $DOMAIN ]]; then
|
||||||
[[ $SINGLE == yes ]] || die "Свой домен работает только в режиме «всё на 443», а эта установка уже работает с отдельными портами."
|
[[ $SINGLE == yes ]] || die "Свой домен работает только в режиме «всё на 443», а эта установка уже работает с отдельными портами."
|
||||||
issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя. Исправьте причину и запустите скрипт снова."
|
# Наш шов: провал сертификата не обрывает установку. Человек может повторить попытку (DNS доехал,
|
||||||
|
# лимит Let's Encrypt кончился) или ввести другое имя — установка продолжится с него же.
|
||||||
|
while ! issue_domain_cert; do
|
||||||
|
echo
|
||||||
|
echo " ${B}Что делать дальше?${N}"
|
||||||
|
echo " ${B}1${N} повторить попытку если ждали обновления A-записи или лимита Let's Encrypt"
|
||||||
|
echo " ${B}2${N} ввести другой домен проверим, что он указывает на этот сервер, и повторим"
|
||||||
|
echo " ${B}3${N} остановиться поставить без своего домена: перезапустите и выберите соседа по подсети"
|
||||||
|
ask_tty " Выбор [1]: " || die "Сертификат для $DOMAIN не выпущен. Подробности: /var/log/kit-domain-cert.log"
|
||||||
|
case ${REPLY//[[:space:]]/} in
|
||||||
|
2) ask_tty " адрес сайта (например vpn.example.com): " || continue
|
||||||
|
local d=${REPLY//[[:space:]]/}
|
||||||
|
[[ -n $d ]] || continue
|
||||||
|
if domain_points_here "$d"; then DOMAIN=$d; SNI=$DOMAIN
|
||||||
|
else warn "оставляю прежний домен $DOMAIN — новый не указывает на этот сервер"; fi ;;
|
||||||
|
3) # Наш шов: продолжаем без своего домена прямо здесь — маскировка соседом по подсети
|
||||||
|
# (или известным сайтом), панель и подписка по IP. Перезапускать установку не нужно.
|
||||||
|
local nb2
|
||||||
|
nb2=$(nearby_sites 2>/dev/null | shuf | head -1)
|
||||||
|
[[ -n $nb2 ]] || nb2=dl.google.com
|
||||||
|
DOMAIN=""; PANEL_ON=ip; LINK_HOST=$HOST
|
||||||
|
SNI=$nb2; SNI2=${SNI2:-$nb2}
|
||||||
|
ok "продолжаю без своего домена: маскировка ${B}$SNI${N}, панель и подписка — по IP"
|
||||||
|
break ;;
|
||||||
|
*) sleep 5 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
OPEN+=("80/tcp")
|
OPEN+=("80/tcp")
|
||||||
fi
|
fi
|
||||||
# Одна заглушка на сервер: её показывают nginx на 443 и Hysteria2 (masquerade), её же берёт kit net masq.
|
# Одна заглушка на сервер: её показывают nginx на 443 и Hysteria2 (masquerade), её же берёт kit net masq.
|
||||||
@@ -841,12 +1068,24 @@ kl_sni_menu() {
|
|||||||
setup_subscription
|
setup_subscription
|
||||||
[[ $SINGLE == yes ]] && setup_nginx
|
[[ $SINGLE == yes ]] && setup_nginx
|
||||||
ok "${#CREATED[@]} подключений, пользователь $NAME"
|
ok "${#CREATED[@]} подключений, пользователь $NAME"
|
||||||
|
# Сайты XHTTP и MTProto – из самих подключений: при повторном запуске они остаются от прошлого раза, а не выбранные сейчас.
|
||||||
|
if [[ $SINGLE == yes ]]; then
|
||||||
|
local sites
|
||||||
|
sites=$(api GET inbounds/list | jq -r '.[] | select(.listen == "127.0.0.1")
|
||||||
|
| if .remark == "XHTTP" then (.streamSettings | if type == "string" then fromjson else . end).realitySettings.serverNames[0]
|
||||||
|
elif .protocol == "mtproto" then (.settings | if type == "string" then fromjson else . end).fakeTlsDomain else empty end // empty')
|
||||||
|
if grep -qE "$SNI_BRAND_RE" <<<"$sites"; then
|
||||||
|
later "XHTTP или MTProto используют известный сайт из общего списка (достаточно соседей по подсети не нашлось). Заменить: kit → Протоколы, порты и маскировка → Сменить сайт маскировки"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
step "Защита и обновления"
|
step "Защита и обновления"
|
||||||
install_kit_cli
|
install_kit_cli
|
||||||
if [[ $TRUSTED == yes ]]; then
|
if [[ $TRUSTED == yes ]]; then
|
||||||
/usr/local/bin/kit __subextra >/dev/null 2>&1 || later "Адреса Clash и JSON на странице подписки не подключились: kit update --force"
|
/usr/local/bin/kit __subextra >/dev/null 2>&1 || later "Адреса Clash и JSON на странице подписки не подключились: kit update --force"
|
||||||
fi
|
fi
|
||||||
|
# Ссылка MTProto в панели: адрес сервера и порт 443 (панель берёт их из таблицы hosts).
|
||||||
|
[[ $SINGLE == yes ]] && { /usr/local/bin/kit __mtproto >/dev/null 2>&1 || later "Ссылка MTProto в панели осталась с внутренним портом: берите её из kit user link имя --telegram"; }
|
||||||
if [[ $PANEL_ON == domain ]]; then
|
if [[ $PANEL_ON == domain ]]; then
|
||||||
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || { PANEL_ON=ip; later "Панель и подписку по домену включить не удалось: оставил по IP. Попробуйте позже: kit net panel domain"; }
|
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || { PANEL_ON=ip; later "Панель и подписку по домену включить не удалось: оставил по IP. Попробуйте позже: kit net panel domain"; }
|
||||||
fi
|
fi
|
||||||
@@ -875,7 +1114,7 @@ kl_sni_menu() {
|
|||||||
else
|
else
|
||||||
install -m 600 /dev/null /var/log/kit-install-check.log
|
install -m 600 /dev/null /var/log/kit-install-check.log
|
||||||
printf '%s\n' "$chk" >/var/log/kit-install-check.log
|
printf '%s\n' "$chk" >/var/log/kit-install-check.log
|
||||||
later "kit check нашёл: $(sed 's/\x1b\[[0-9;]*m//g' <<<"$chk" | grep -a -m1 '❌' | cut -c1-110). Подробности: kit check --deep"
|
later "Проверка сервера нашла: $(sed 's/\x1b\[[0-9;]*m//g' <<<"$chk" | grep -a -m1 '❌' | cut -c1-110). Подробности: kit → Проверка сервера → Глубокая"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- итог ---
|
# --- итог ---
|
||||||
@@ -913,38 +1152,41 @@ kl_sni_menu() {
|
|||||||
[[ $TRUSTED == yes ]] && { echo "Подписка ($NAME) – все протоколы одной ссылкой:"; echo "$SUB_URL"; echo; }
|
[[ $TRUSTED == yes ]] && { echo "Подписка ($NAME) – все протоколы одной ссылкой:"; echo "$SUB_URL"; echo; }
|
||||||
echo "Отдельные подключения ($NAME):"
|
echo "Отдельные подключения ($NAME):"
|
||||||
echo "$links"
|
echo "$links"
|
||||||
|
if [[ $SINGLE == yes ]] && grep -q '^tg://' <<<"$links"; then
|
||||||
|
echo
|
||||||
|
echo "Telegram (tg://): порт 443 – верный, ${INNER[mtproto]} – внутренний порт сервера, снаружи закрыт."
|
||||||
|
fi
|
||||||
} >"$RESULT"
|
} >"$RESULT"
|
||||||
|
|
||||||
|
echo
|
||||||
echo
|
echo
|
||||||
echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."
|
echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."
|
||||||
if ((${#WARNINGS[@]})); then
|
if ((${#WARNINGS[@]})); then
|
||||||
echo
|
local -a wn=("${WARNINGS[@]:0:3}")
|
||||||
echo "${B}ВНИМАНИЕ${N}"
|
((${#WARNINGS[@]} > 3)) && wn+=("И ещё $((${#WARNINGS[@]} - 3)): kit → Проверка сервера → Быстрая проверка.")
|
||||||
local w n=0
|
tips "На что обратить внимание" "${wn[@]}"
|
||||||
for w in "${WARNINGS[@]}"; do
|
|
||||||
n=$((n + 1)); ((n > 3)) && break
|
|
||||||
echo " ${Y}!${N} $w"
|
|
||||||
done
|
|
||||||
((${#WARNINGS[@]} > 3)) && echo " … и ещё $((${#WARNINGS[@]} - 3)): kit check"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Официальный установщик 3X-UI закреплённой версии, сверенный по SHA256.
|
# Официальный установщик 3X-UI закреплённой версии, сверенный по SHA256.
|
||||||
install_xui() { # порт путь логин пароль
|
install_xui() { # порт путь логин пароль
|
||||||
local tmp
|
local tmp
|
||||||
|
[[ ${PANEL_SSL:-} == ip ]] && ufw_open_80
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)"
|
say "Ставлю 3X-UI $XUI_VERSION официальным установщиком (пара минут)"
|
||||||
curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh"
|
curl -fsSL --retry 3 -o "$tmp" "https://raw.githubusercontent.com/$XUI_REPO/$XUI_VERSION/install.sh"
|
||||||
[[ $(sha256sum "$tmp" | awk '{print $1}') == "$XUI_INSTALL_SHA256" ]] \
|
[[ $(sha256sum "$tmp" | awk '{print $1}') == "$XUI_INSTALL_SHA256" ]] \
|
||||||
|| die "Установщик 3X-UI $XUI_VERSION не совпал с проверенным (SHA256) – не запускаю. Сообщите нам: github.com/itsnotkubrick/3X-UI_KIT/issues"
|
|| die "Установщик 3X-UI $XUI_VERSION не совпал с проверенным (SHA256) – не запускаю. Сообщите нам: github.com/itsnotkubrick/3X-UI_KIT/issues"
|
||||||
|
spin_start "Устанавливаю панель 3X-UI"
|
||||||
if ! XUI_NONINTERACTIVE=1 XUI_SSL_MODE="${PANEL_SSL/custom/none}" XUI_SERVER_IP="$HOST" \
|
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_PANEL_PORT="$1" XUI_WEB_BASE_PATH="$2" \
|
||||||
XUI_USERNAME="$3" XUI_PASSWORD="$4" \
|
XUI_USERNAME="$3" XUI_PASSWORD="$4" \
|
||||||
bash "$tmp" "$XUI_VERSION" </dev/null >/var/log/3x-ui-install.log 2>&1; then
|
bash "$tmp" "$XUI_VERSION" </dev/null >/var/log/3x-ui-install.log 2>&1; then
|
||||||
|
spin_stop
|
||||||
tail -20 /var/log/3x-ui-install.log >&2
|
tail -20 /var/log/3x-ui-install.log >&2
|
||||||
die "Установщик 3X-UI завершился с ошибкой. Полный лог: /var/log/3x-ui-install.log"
|
die "Установщик 3X-UI завершился с ошибкой. Полный лог: /var/log/3x-ui-install.log"
|
||||||
fi
|
fi
|
||||||
|
spin_stop
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -959,7 +1201,7 @@ connect_panel() {
|
|||||||
for i in $(seq 1 60); do
|
for i in $(seq 1 60); do
|
||||||
for scheme in https http; do
|
for scheme in https http; do
|
||||||
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
|
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
|
||||||
curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $XUI_API_TOKEN" "$API/server/getNewUUID" 2>/dev/null && return 0
|
curl -fsk -m 5 -o /dev/null -H @<(printf 'Authorization: Bearer %s\n' "$XUI_API_TOKEN") "$API/server/getNewUUID" 2>/dev/null && return 0
|
||||||
done
|
done
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
@@ -1010,12 +1252,14 @@ set_xray_core() {
|
|||||||
say "Ставлю ядро Xray $XRAY_CORE (совместимо с Hiddify, Mihomo и другими клиентами)"
|
say "Ставлю ядро Xray $XRAY_CORE (совместимо с Hiddify, Mihomo и другими клиентами)"
|
||||||
# 3X-UI 3.9 бывает отвечает ошибкой «GitHub API error», хотя ядро уже заменено: ответ не считаем
|
# 3X-UI 3.9 бывает отвечает ошибкой «GitHub API error», хотя ядро уже заменено: ответ не считаем
|
||||||
# проверкой, смотрим на версию ядра ниже.
|
# проверкой, смотрим на версию ядра ниже.
|
||||||
|
spin_start "Ставлю ядро Xray"
|
||||||
(api POST "server/installXray/$XRAY_CORE" '{}' >/dev/null) 2>/dev/null || warn "Панель не подтвердила смену ядра – проверяю версию сам."
|
(api POST "server/installXray/$XRAY_CORE" '{}' >/dev/null) 2>/dev/null || warn "Панель не подтвердила смену ядра – проверяю версию сам."
|
||||||
for _ in $(seq 1 30); do
|
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=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
|
||||||
[[ $cur_core == "$XRAY_CORE" ]] && break
|
[[ $cur_core == "$XRAY_CORE" ]] && break
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
spin_stop
|
||||||
if [[ $cur_core != "$XRAY_CORE" ]]; then
|
if [[ $cur_core != "$XRAY_CORE" ]]; then
|
||||||
warn "Панель не смогла скачать ядро с GitHub – пробую запасной путь: зеркало с проверкой SHA256."
|
warn "Панель не смогла скачать ядро с GitHub – пробую запасной путь: зеркало с проверкой SHA256."
|
||||||
xray_install_fallback
|
xray_install_fallback
|
||||||
@@ -1034,6 +1278,19 @@ ssh_ports() {
|
|||||||
} | grep -E '^[0-9]{1,5}$' | sort -un || true
|
} | grep -E '^[0-9]{1,5}$' | sort -un || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Let's Encrypt проверяет сервер по порту 80. Если ufw уже включён (остался после прошлой установки или включён на образе
|
||||||
|
# хостера), он этот порт закрыл бы, и сертификат не выдастся («Timeout during connect»). Открываем порт заранее;
|
||||||
|
# нужен ли он потом, решает setup_ufw.
|
||||||
|
UFW_OPENED_80=no
|
||||||
|
ufw_open_80() {
|
||||||
|
[[ ${UFW:-yes} == yes ]] || return 0
|
||||||
|
command -v ufw >/dev/null 2>&1 || return 0
|
||||||
|
ufw status 2>/dev/null | grep -q '^Status: active' || return 0
|
||||||
|
ufw status 2>/dev/null | grep -qE '^80(/tcp)? +ALLOW' && return 0
|
||||||
|
ufw allow 80/tcp >/dev/null 2>&1 && UFW_OPENED_80=yes
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
setup_ufw() {
|
setup_ufw() {
|
||||||
local ssh_port o
|
local ssh_port o
|
||||||
ssh_port=$(ssh_ports)
|
ssh_port=$(ssh_ports)
|
||||||
@@ -1041,6 +1298,7 @@ setup_ufw() {
|
|||||||
ok "ufw: открыто ${#OPEN[@]} портов (${OPEN[*]}), панель не торчит наружу"
|
ok "ufw: открыто ${#OPEN[@]} портов (${OPEN[*]}), панель не торчит наружу"
|
||||||
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
|
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
|
||||||
ufw --force enable >/dev/null || later "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
|
ufw --force enable >/dev/null || later "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
|
||||||
|
if [[ $UFW_OPENED_80 == yes && " ${OPEN[*]} " != *" 80/tcp "* ]]; then ufw delete allow 80/tcp >/dev/null 2>&1 || true; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- сертификат ----------
|
# ---------- сертификат ----------
|
||||||
@@ -1112,11 +1370,11 @@ add_inbound() {
|
|||||||
sniffing: "{\"enabled\":true,\"destOverride\":[\"http\",\"tls\",\"quic\"],\"metadataOnly\":false,\"routeOnly\":false}",
|
sniffing: "{\"enabled\":true,\"destOverride\":[\"http\",\"tls\",\"quic\"],\"metadataOnly\":false,\"routeOnly\":false}",
|
||||||
expiryTime: 0, total: 0}')
|
expiryTime: 0, total: 0}')
|
||||||
local out
|
local out
|
||||||
out=$(curl -sSk -m 20 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X POST -d "$body" "$API/inbounds/add")
|
out=$(curl -sSk -m 20 -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") -H 'Content-Type: application/json' -X POST --data-binary @<(printf '%s' "$body") "$API/inbounds/add")
|
||||||
if [[ $(jq -r '.success' <<<"$out") != true ]] && grep -q 'Duplicate email' <<<"$out"; then
|
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")
|
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")
|
out=$(curl -sSk -m 20 -H @<(printf 'Authorization: Bearer %s\n' "$TOKEN") -H 'Content-Type: application/json' -X POST --data-binary @<(printf '%s' "$body") "$API/inbounds/add")
|
||||||
fi
|
fi
|
||||||
[[ $(jq -r '.success' <<<"$out") == true ]] || die "Панель не создала $remark: $(jq -r '.msg // .' <<<"$out" | head -c 300)"
|
[[ $(jq -r '.success' <<<"$out") == true ]] || die "Панель не создала $remark: $(jq -r '.msg // .' <<<"$out" | head -c 300)"
|
||||||
CREATED+=("$remark"); open_port "$port" "$net"
|
CREATED+=("$remark"); open_port "$port" "$net"
|
||||||
@@ -1521,9 +1779,19 @@ stub_site() {
|
|||||||
|
|
||||||
setup_nginx() {
|
setup_nginx() {
|
||||||
say "Настраиваю nginx: всё TCP через порт 443"
|
say "Настраиваю nginx: всё TCP через порт 443"
|
||||||
|
spin_start "Ставлю nginx"
|
||||||
apt-get install -y -qq nginx libnginx-mod-stream >/dev/null
|
apt-get install -y -qq nginx libnginx-mod-stream >/dev/null
|
||||||
|
spin_stop
|
||||||
# Порт 80 нужен Let's Encrypt для продления сертификата – сайт nginx по умолчанию убираем.
|
# Порт 80 нужен Let's Encrypt для продления сертификата – сайт nginx по умолчанию убираем.
|
||||||
rm -f /etc/nginx/sites-enabled/default
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
# apt уже запустил nginx со стандартным сайтом на :80. Если установка упадёт ниже, он не должен остаться и занимать порт.
|
||||||
|
[[ -f /etc/nginx/kit-stream.conf ]] || systemctl stop nginx >/dev/null 2>&1 || true
|
||||||
|
# С nginx 1.25.1 «listen … http2» устарело: пишем «http2 on;».
|
||||||
|
local ngx_ver h2="http2" h2on=""
|
||||||
|
ngx_ver=$(nginx -v 2>&1 | sed -n 's|.*nginx/\([0-9][0-9.]*\).*|\1|p')
|
||||||
|
if [[ -n $ngx_ver && $(printf '%s\n1.25.1\n' "$ngx_ver" | sort -V | head -1) == 1.25.1 ]]; then
|
||||||
|
h2=""; h2on=$'\n http2 on;'
|
||||||
|
fi
|
||||||
|
|
||||||
# Панель – только через nginx.
|
# Панель – только через nginx.
|
||||||
local all
|
local all
|
||||||
@@ -1594,6 +1862,8 @@ setup_nginx() {
|
|||||||
{
|
{
|
||||||
echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."
|
echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."
|
||||||
echo "stream {"
|
echo "stream {"
|
||||||
|
echo " # Длинные имена сайтов (до 253 знаков) не влезают в хеш-таблицу nginx по умолчанию."
|
||||||
|
echo " map_hash_bucket_size 512;"
|
||||||
echo " map \$ssl_preread_server_name \$kit_upstream {"
|
echo " map \$ssl_preread_server_name \$kit_upstream {"
|
||||||
[[ -n $reality_sni ]] && echo " $reality_sni 127.0.0.1:${INNER[reality]};"
|
[[ -n $reality_sni ]] && echo " $reality_sni 127.0.0.1:${INNER[reality]};"
|
||||||
[[ -n $xhttp_sni && $xhttp_sni != "$reality_sni" ]] && echo " $xhttp_sni 127.0.0.1:${INNER[xhttp]};"
|
[[ -n $xhttp_sni && $xhttp_sni != "$reality_sni" ]] && echo " $xhttp_sni 127.0.0.1:${INNER[xhttp]};"
|
||||||
@@ -1614,8 +1884,8 @@ setup_nginx() {
|
|||||||
cat >/etc/nginx/conf.d/kit.conf <<NGX
|
cat >/etc/nginx/conf.d/kit.conf <<NGX
|
||||||
# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.
|
# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.
|
||||||
server {
|
server {
|
||||||
listen 127.0.0.1:${INNER[web]} ssl http2 proxy_protocol;
|
listen 127.0.0.1:${INNER[web]} ssl${h2:+ $h2} proxy_protocol;
|
||||||
server_name _;
|
server_name _;${h2on}
|
||||||
ssl_certificate $CERT;
|
ssl_certificate $CERT;
|
||||||
ssl_certificate_key $KEY;
|
ssl_certificate_key $KEY;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
@@ -1653,8 +1923,8 @@ NGX
|
|||||||
# Свой домен (self-steal): сюда REALITY отправляет всех, кто не подключается как клиент.
|
# Свой домен (self-steal): сюда REALITY отправляет всех, кто не подключается как клиент.
|
||||||
# Только заглушка, без панели и подписки; порт слушает localhost.
|
# Только заглушка, без панели и подписки; порт слушает localhost.
|
||||||
server {
|
server {
|
||||||
listen 127.0.0.1:${INNER[selfweb]} ssl http2;
|
listen 127.0.0.1:${INNER[selfweb]} ssl${h2:+ $h2};
|
||||||
server_name $steal_domain;
|
server_name $steal_domain;${h2on}
|
||||||
ssl_certificate $DOMAIN_CERT_DIR/fullchain.pem;
|
ssl_certificate $DOMAIN_CERT_DIR/fullchain.pem;
|
||||||
ssl_certificate_key $DOMAIN_CERT_DIR/privkey.pem;
|
ssl_certificate_key $DOMAIN_CERT_DIR/privkey.pem;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
@@ -1776,7 +2046,9 @@ install_kit_sub_file() {
|
|||||||
|
|
||||||
install_kit_sub() {
|
install_kit_sub() {
|
||||||
say "Ставлю подписку с учётом приложения (kit-sub)"
|
say "Ставлю подписку с учётом приложения (kit-sub)"
|
||||||
|
spin_start "Ставлю python3 для подписки"
|
||||||
apt-get install -y -qq python3 python3-yaml >/dev/null
|
apt-get install -y -qq python3 python3-yaml >/dev/null
|
||||||
|
spin_stop
|
||||||
install_kit_sub_file
|
install_kit_sub_file
|
||||||
if [[ $SINGLE == yes ]]; then
|
if [[ $SINGLE == yes ]]; then
|
||||||
# За nginx: слушаем только localhost, TLS снимает nginx на 443.
|
# За nginx: слушаем только localhost, TLS снимает nginx на 443.
|
||||||
@@ -1827,12 +2099,14 @@ sub_links() {
|
|||||||
# Сначала – только печатные ASCII и перевод строки: файл с нулевым байтом GNU grep считает двоичным и молчит
|
# Сначала – только печатные ASCII и перевод строки: файл с нулевым байтом GNU grep считает двоичным и молчит
|
||||||
# (проверка «прошла» бы), а bash при «.» нулевые байты выкидывает и выполняет остальное. Поэтому и grep -a.
|
# (проверка «прошла» бы), а bash при «.» нулевые байты выкидывает и выполняет остальное. Поэтому и grep -a.
|
||||||
safe_env() { # файл имя...
|
safe_env() { # файл имя...
|
||||||
local f=$1 n re; shift
|
local f=$1 n re rc=0; shift
|
||||||
[[ -r $f ]] || die "В копии нет файла ${f##*/}."
|
[[ -f $f && -r $f ]] || die "В копии нет файла ${f##*/}."
|
||||||
[[ $(LC_ALL=C tr -d '\n -~' <"$f" | wc -c) -eq 0 ]] \
|
[[ $(LC_ALL=C tr -d '\n -~' <"$f" | wc -c) -eq 0 ]] \
|
||||||
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
|
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
|
||||||
LC_ALL=C grep -aqvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|''))?$" "$f" \
|
# grep: 0 – есть недопустимая строка, 1 – все строки хорошие, 2 – не смог прочитать: и 0, и 2 считаем отказом.
|
||||||
&& die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
|
LC_ALL=C grep -aqvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|''))?$" "$f" || rc=$?
|
||||||
|
[[ $rc -eq 1 ]] \
|
||||||
|
|| die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."
|
||||||
while IFS= read -r n; do
|
while IFS= read -r n; do
|
||||||
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
|
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
|
||||||
case $n in
|
case $n in
|
||||||
@@ -1945,7 +2219,7 @@ restore_self_cert() { # путь-к-fullchain.pem
|
|||||||
if [[ $dir == /root/cert/ip ]]; then
|
if [[ $dir == /root/cert/ip ]]; then
|
||||||
printf '%s\n' "${R}!${N} Let's Encrypt не выдал сертификат на IP $HOST (лимит: 5 сертификатов в неделю на один IP, закрытый порт 80 или сбой; лог: /var/log/3x-ui-install.log)." >&2
|
printf '%s\n' "${R}!${N} Let's Encrypt не выдал сертификат на IP $HOST (лимит: 5 сертификатов в неделю на один IP, закрытый порт 80 или сбой; лог: /var/log/3x-ui-install.log)." >&2
|
||||||
echo " Ставлю самоподписанный: ссылки на подключения работают (отпечаток в них), браузер покажет предупреждение, а подписка в приложениях может не открыться." >&2
|
echo " Ставлю самоподписанный: ссылки на подключения работают (отпечаток в них), браузер покажет предупреждение, а подписка в приложениях может не открыться." >&2
|
||||||
later "Сертификат на IP самоподписанный (Let's Encrypt не выдал): пользуйтесь ссылками на отдельные подключения (kit user link имя), подписка в приложениях может не открыться."
|
later "Сертификат на IP самоподписанный (Let's Encrypt не выдал): пользуйтесь ссылками на отдельные подключения (kit → Пользователи → Показать ссылки), подписка в приложениях может не открыться."
|
||||||
SELF_IP_CERT=yes
|
SELF_IP_CERT=yes
|
||||||
fi
|
fi
|
||||||
install -d -m 700 "$dir"
|
install -d -m 700 "$dir"
|
||||||
@@ -1978,7 +2252,7 @@ restore_main() { # файл
|
|||||||
say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw, python3"
|
say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw, python3"
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
wait_apt_idle
|
wait_apt_idle
|
||||||
apt-get update -qq
|
apt_refresh
|
||||||
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron python3 python3-yaml >/dev/null
|
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron python3 python3-yaml >/dev/null
|
||||||
|
|
||||||
# Сначала проверяем архив целиком: только наши пути, только файлы и папки, без ссылок
|
# Сначала проверяем архив целиком: только наши пути, только файлы и папки, без ссылок
|
||||||
@@ -1987,7 +2261,7 @@ restore_main() { # файл
|
|||||||
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
# shellcheck disable=SC2064 # путь подставляем сразу: при выходе локальной переменной уже нет
|
||||||
trap "rm -rf -- '$tmp'" EXIT
|
trap "rm -rf -- '$tmp'" EXIT
|
||||||
python3 - "$file" "$tmp" <<'PY' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."
|
python3 - "$file" "$tmp" <<'PY' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."
|
||||||
import os, sys, tarfile
|
import os, sys, tarfile, unicodedata
|
||||||
allowed = ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "etc/kit/kit.env", "etc/kit-sub/config.json", "etc/kit-sub/rules.yaml",
|
allowed = ("etc/x-ui/x-ui.db", "etc/x-ui/install-result.env", "etc/kit/kit.env", "etc/kit-sub/config.json", "etc/kit-sub/rules.yaml",
|
||||||
"etc/nginx/kit-stream.conf", "etc/nginx/conf.d/kit.conf", "var/www/kit", "root/cert/self",
|
"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")
|
"root/cert/custom", "root/3x-ui.txt", "kit-backup.env")
|
||||||
@@ -2015,7 +2289,7 @@ with tarfile.open(sys.argv[1], "r:gz") as t:
|
|||||||
sys.exit("нет " + need)
|
sys.exit("нет " + need)
|
||||||
# Файлы настроек потом подключаются от root: в них только печатные ASCII и перевод строки. Нулевой байт,
|
# Файлы настроек потом подключаются от root: в них только печатные ASCII и перевод строки. Нулевой байт,
|
||||||
# \r или не-ASCII ломают построчную проверку (GNU grep считает такой файл двоичным). Данные для входа
|
# \r или не-ASCII ломают построчную проверку (GNU grep считает такой файл двоичным). Данные для входа
|
||||||
# (3x-ui.txt) показываются в терминале: только UTF-8 без управляющих символов, кроме \n и \t.
|
# (3x-ui.txt) показываются в терминале: только UTF-8 без управляющих и невидимых символов (Cc, Cf: U+202E, U+200B, U+FEFF), кроме \n и \t.
|
||||||
for m in ms:
|
for m in ms:
|
||||||
if not m.isfile():
|
if not m.isfile():
|
||||||
continue
|
continue
|
||||||
@@ -2029,7 +2303,7 @@ with tarfile.open(sys.argv[1], "r:gz") as t:
|
|||||||
text = data.decode("utf-8")
|
text = data.decode("utf-8")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
sys.exit("байты " + m.name)
|
sys.exit("байты " + m.name)
|
||||||
if len(data) > 1048576 or any((c < " " and c not in "\n\t") or "\x7f" <= c <= "\x9f" for c in text):
|
if len(data) > 1048576 or any(c not in "\n\t" and unicodedata.category(c) in ("Cc", "Cf") for c in text):
|
||||||
sys.exit("байты " + m.name)
|
sys.exit("байты " + m.name)
|
||||||
# Каталог сертификата есть – в нём оба файла, иначе установка споткнётся на полпути.
|
# Каталог сертификата есть – в нём оба файла, иначе установка споткнётся на полпути.
|
||||||
files = {m.name for m in ms if m.isfile() and m.size > 0}
|
files = {m.name for m in ms if m.isfile() and m.size > 0}
|
||||||
@@ -2053,6 +2327,21 @@ PY
|
|||||||
fi
|
fi
|
||||||
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')" \
|
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 "База панели в копии повреждена. Ничего не менял."
|
"$tmp/etc/x-ui/x-ui.db" 2>/dev/null || die "База панели в копии повреждена. Ничего не менял."
|
||||||
|
# Прежние имена сайта (льготный срок после kit net site) из копии не берём: у REALITY и XHTTP остаётся только нынешнее имя.
|
||||||
|
python3 - "$tmp/etc/x-ui/x-ui.db" <<'PY' || die "Не удалось подготовить базу из копии. Ничего не менял."
|
||||||
|
import json, sqlite3, sys
|
||||||
|
db = sqlite3.connect(sys.argv[1])
|
||||||
|
for i, s in db.execute("SELECT id, stream_settings FROM inbounds").fetchall():
|
||||||
|
try:
|
||||||
|
st = json.loads(s)
|
||||||
|
names = st["realitySettings"]["serverNames"]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if isinstance(names, list) and len(names) > 1:
|
||||||
|
st["realitySettings"]["serverNames"] = names[:1]
|
||||||
|
db.execute("UPDATE inbounds SET stream_settings = ? WHERE id = ?", (json.dumps(st, ensure_ascii=False, indent=2), i))
|
||||||
|
db.commit(); db.close()
|
||||||
|
PY
|
||||||
|
|
||||||
local BACKUP_HOST BACKUP_SSL BACKUP_DATE BACKUP_KIT_VERSION
|
local BACKUP_HOST BACKUP_SSL BACKUP_DATE BACKUP_KIT_VERSION
|
||||||
BACKUP_HOST=$(env_get "$tmp/kit-backup.env" BACKUP_HOST)
|
BACKUP_HOST=$(env_get "$tmp/kit-backup.env" BACKUP_HOST)
|
||||||
@@ -2117,6 +2406,7 @@ PY
|
|||||||
systemctl stop x-ui
|
systemctl stop x-ui
|
||||||
# Служебные файлы WAL от базы, созданной установщиком, к базе из копии не подходят: с ними она «битая».
|
# Служебные файлы WAL от базы, созданной установщиком, к базе из копии не подходят: с ними она «битая».
|
||||||
rm -f /etc/x-ui/x-ui.db-wal /etc/x-ui/x-ui.db-shm
|
rm -f /etc/x-ui/x-ui.db-wal /etc/x-ui/x-ui.db-shm
|
||||||
|
rm -f /etc/kit/sni-grace.json /etc/kit-sub/sni-grace.json
|
||||||
install -m 600 "$tmp/etc/x-ui/x-ui.db" /etc/x-ui/x-ui.db
|
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"
|
install -m 600 "$tmp/etc/x-ui/install-result.env" "$XUI_ENV"
|
||||||
local c
|
local c
|
||||||
@@ -2174,6 +2464,8 @@ PY
|
|||||||
sed -i 's/^PANEL_ON=.*/PANEL_ON=ip/' /etc/kit/kit.env
|
sed -i 's/^PANEL_ON=.*/PANEL_ON=ip/' /etc/kit/kit.env
|
||||||
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || { /usr/local/bin/kit __xver 0 >/dev/null 2>&1 || true; warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain"; }
|
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || { /usr/local/bin/kit __xver 0 >/dev/null 2>&1 || true; warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain"; }
|
||||||
fi
|
fi
|
||||||
|
# MTProto: ссылка в панели – на этот сервер и 443 (чужие адреса из копии убираем), секреты – под нынешний сайт.
|
||||||
|
/usr/local/bin/kit __mtproto restore >/dev/null 2>&1 || later "Ссылку MTProto в панели поправить не удалось: kit fix"
|
||||||
brand_xui_menu
|
brand_xui_menu
|
||||||
/usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
|
/usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
|
||||||
/usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || later "Проверка общего лимита трафика не включилась – включите позже: kit fix"
|
/usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || later "Проверка общего лимита трафика не включилась – включите позже: kit fix"
|
||||||
@@ -2201,25 +2493,21 @@ PY
|
|||||||
echo "${G}${B}Готово! Сервер восстановлен из копии: $n_in подключений, $n_cl клиентских записей.${N}"
|
echo "${G}${B}Готово! Сервер восстановлен из копии: $n_in подключений, $n_cl клиентских записей.${N}"
|
||||||
else
|
else
|
||||||
echo "${R}${B}Сервер восстановлен из копии ($n_in подключений, $n_cl клиентских записей), но ядро Xray не запустилось: подключения не работают.${N}"
|
echo "${R}${B}Сервер восстановлен из копии ($n_in подключений, $n_cl клиентских записей), но ядро Xray не запустилось: подключения не работают.${N}"
|
||||||
echo "Причина – в журнале: ${B}tail -n 30 /var/log/x-ui/3xui.log${N}, проверка: ${B}kit check${N}"
|
echo "Причина – в журнале ${B}/var/log/x-ui/3xui.log${N}, проверка: ${B}kit → Проверка сервера${N}"
|
||||||
fi
|
fi
|
||||||
echo "Логин и пароль панели прежние, адрес панели и подписки: ${B}cat $RESULT${N}"
|
echo "Логин и пароль панели прежние. Адрес панели и подписки: файл ${B}$RESULT${N} (виден только root)."
|
||||||
echo
|
local -a tp=()
|
||||||
if [[ -n $old_ip ]]; then
|
if [[ -n $old_ip ]]; then
|
||||||
warn "IP сервера сменился: $old_ip → $HOST."
|
warn "IP сервера сменился: $old_ip → $HOST."
|
||||||
echo "Старые подписки указывают на старый IP, поэтому клиентам нужно один раз добавить"
|
tp+=("Старые подписки указывают на старый IP, поэтому клиентам нужно один раз добавить подписку заново. Новую ссылку покажет kit → Пользователи → Показать ссылки.")
|
||||||
echo "подписку заново: ${B}kit user list${N}, затем ${B}kit user link имя${N}."
|
tp+=("Чтобы в следующий раз переезд прошёл без хлопот для клиентов, ставьте сервер на домен (--cert, --key, --host).")
|
||||||
echo "${D}Чтобы в следующий раз переезд прошёл без хлопот для клиентов, ставьте сервер на домен (--cert, --key, --host).${N}"
|
|
||||||
else
|
else
|
||||||
echo "Клиентам ничего менять не нужно: ключи, ссылки и подписки те же."
|
tp+=("Клиентам ничего менять не нужно: ключи, ссылки и подписки те же.")
|
||||||
fi
|
|
||||||
[[ $PANEL_SSL == custom ]] && echo "Направьте A-запись домена ${B}$HOST${N} на IP этого сервера, если ещё не сделали."
|
|
||||||
if ((${#WARNINGS[@]})); then
|
|
||||||
echo
|
|
||||||
echo "${B}ВНИМАНИЕ${N}"
|
|
||||||
local w
|
|
||||||
for w in "${WARNINGS[@]}"; do echo " ${Y}!${N} $w"; done
|
|
||||||
fi
|
fi
|
||||||
|
[[ $PANEL_SSL == custom ]] && tp+=("Направьте A-запись домена $HOST на IP этого сервера, если ещё не сделали.")
|
||||||
|
tp+=("Управление сервером: введите команду ${B}kit${N}, откроется меню.")
|
||||||
|
tips "Что дальше" "${tp[@]}"
|
||||||
|
if ((${#WARNINGS[@]})); then tips "На что обратить внимание" "${WARNINGS[@]}"; fi
|
||||||
[[ $core_ok == yes ]] || exit 1
|
[[ $core_ok == yes ]] || exit 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -2228,11 +2516,8 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
3X-UI со всеми протоколами одной командой
|
3X-UI со всеми протоколами одной командой
|
||||||
|
|
||||||
--protocols all all (по умолчанию – всё, кроме WireGuard), minimal (только REALITY)
|
--protocols all all (по умолчанию – основной набор), minimal (только REALITY)
|
||||||
или список через запятую: reality,reality2,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto
|
\1 --port 443 порт REALITY (TCP) и Hysteria2 (UDP), по умолчанию 443
|
||||||
(обычный WireGuard работает нестабильно – включайте его, только если сервер и
|
|
||||||
пользователи за границей)
|
|
||||||
--port 443 порт REALITY (TCP) и Hysteria2 (UDP), по умолчанию 443
|
|
||||||
--sni сайт чужой сайт для маскировки (по умолчанию подбирается сам)
|
--sni сайт чужой сайт для маскировки (по умолчанию подбирается сам)
|
||||||
--domain домен свой домен для маскировки (надёжнее): его A-запись должна вести на этот
|
--domain домен свой домен для маскировки (надёжнее): его A-запись должна вести на этот
|
||||||
сервер, порт 80 свободен; сертификат Let's Encrypt установщик получит сам.
|
сервер, порт 80 свободен; сертификат Let's Encrypt установщик получит сам.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём
|
# Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём
|
||||||
# останавливается и не заменяет команду hy2.
|
# останавливается и не заменяет команду hy2.
|
||||||
|
|
||||||
KIT_VERSION="1.2.26"
|
KIT_VERSION="1.2.1.9"
|
||||||
|
|
||||||
cat >&2 <<'MSG'
|
cat >&2 <<'MSG'
|
||||||
Отдельный установщик Hysteria2 больше не поддерживается.
|
Отдельный установщик Hysteria2 больше не поддерживается.
|
||||||
|
|||||||
+102
-164
@@ -73,7 +73,10 @@ kit_local_help() {
|
|||||||
kit local seams какие места форк правит в файлах автора
|
kit local seams какие места форк правит в файлах автора
|
||||||
kit local masking состояние маскировки (у REALITY должен быть свой домен)
|
kit local masking состояние маскировки (у REALITY должен быть свой домен)
|
||||||
kit local hardening сводка по защите сервера и что ещё можно включить
|
kit local hardening сводка по защите сервера и что ещё можно включить
|
||||||
kit local uninstall снести всё, что поставила установка (SSH не трогает)
|
kit local access доступ к панели, сертификат, ссылки и заглушка — одним экраном
|
||||||
|
kit local links подписка, QR-код и ссылки по протоколам (как в меню kit)
|
||||||
|
kit local ports открытые порты: что нужно, что лишнее и как закрыть
|
||||||
|
kit local uninstall то же, что kit uninstall: снести всё, включая сам kit (SSH не трогает)
|
||||||
|
|
||||||
Заглушка (сайт на 443 и ответ Hysteria2 — одна и та же страница)
|
Заглушка (сайт на 443 и ответ Hysteria2 — одна и та же страница)
|
||||||
kit local site поставить случайный сайт из нашего набора (10 штук)
|
kit local site поставить случайный сайт из нашего набора (10 штук)
|
||||||
@@ -420,9 +423,8 @@ Unattended-Upgrade::Automatic-Reboot "false";
|
|||||||
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||||
EOF
|
EOF
|
||||||
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades"
|
systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades"
|
||||||
kl_ok "включено: обновления безопасности ставятся сами"
|
# Одна строка: подробности нужны редко, а в выводе установки они превращались в шум.
|
||||||
kl_ok "панель и ядро это не затрагивает — они ставятся не из apt, а из своих релизов"
|
kl_ok "автообновления безопасности включены (панель и ядро это не затрагивает, сервер сам не перезагружается)"
|
||||||
kl_warn "обновление ядра применится только после перезагрузки, сам сервер не перезагружается"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,6 +902,9 @@ kit_local_cmd() {
|
|||||||
upgrades | auto) shift; kit_local_upgrades "$@" ;;
|
upgrades | auto) shift; kit_local_upgrades "$@" ;;
|
||||||
hardening | hard) shift; kit_local_hardening "$@" ;;
|
hardening | hard) shift; kit_local_hardening "$@" ;;
|
||||||
uninstall | remove) shift; kit_local_uninstall "$@" ;;
|
uninstall | remove) shift; kit_local_uninstall "$@" ;;
|
||||||
|
links | link) shift; kit_local_links "$@" ;;
|
||||||
|
access) shift; kit_local_access "$@" ;;
|
||||||
|
ports) shift; kit_local_ports "$@" ;;
|
||||||
site | stub | stub-site) shift; kit_local_site "$@" ;;
|
site | stub | stub-site) shift; kit_local_site "$@" ;;
|
||||||
*)
|
*)
|
||||||
printf 'Неизвестная команда форка: %s\n' "$1" >&2
|
printf 'Неизвестная команда форка: %s\n' "$1" >&2
|
||||||
@@ -922,169 +927,99 @@ kit_local_cmd() {
|
|||||||
#
|
#
|
||||||
# kit local uninstall спросит подтверждение словом «удалить»
|
# kit local uninstall спросит подтверждение словом «удалить»
|
||||||
# kit local uninstall --dry-run только покажет, что будет удалено
|
# kit local uninstall --dry-run только покажет, что будет удалено
|
||||||
kit_local_uninstall() {
|
kit_local_ports() { # [--close-extra] — что открыто в ufw, что реально нужно и как закрыть лишнее
|
||||||
kl_args "$@"
|
local close=no a
|
||||||
local arg=${KL_REST[0]:-} ans found
|
for a in "$@"; do
|
||||||
local reset_ssh=no i=0
|
case $a in
|
||||||
case $arg in
|
--close-extra) close=yes ;;
|
||||||
"") ;;
|
--help | -h) printf 'kit local ports [--close-extra] — открытые порты, нужные и лишние\n'; return 0 ;;
|
||||||
--dry-run) KL_DRY=yes ;;
|
*) kl_die "kit local ports [--close-extra]"; return 1 ;;
|
||||||
--yes) KIT_LOCAL_UNINSTALL_CONFIRMED=yes ;;
|
esac
|
||||||
--reset-ssh) reset_ssh=yes ;;
|
|
||||||
*) kl_die "kit local uninstall [--dry-run] [--yes] [--reset-ssh]"; return 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Службы: и явный список, и всё, что найдётся по нашим шаблонам (чтобы новые юниты kit не оставались).
|
|
||||||
# kit запускает наши команды с `set -e`: при уборке это недопустимо — любая «не найденная» служба
|
|
||||||
# или пустой grep обрывали бы снос на середине (так и было: файлы оставались). Убираем errexit.
|
|
||||||
set +e
|
|
||||||
|
|
||||||
local -a svc=(x-ui.service kit-sub.service kit-update.timer kit-update.service kit-limit.timer kit-limit.service)
|
|
||||||
local u
|
|
||||||
for u in $(systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' \
|
|
||||||
| grep -E '^(x-ui|kit-|kit\.)' | grep -E '\.(service|timer)$'); do
|
|
||||||
[[ " ${svc[*]} " == *" $u "* ]] || svc+=("$u")
|
|
||||||
done
|
done
|
||||||
# Список собран по коду установщика (scripts/3x-ui.sh) и нашего слоя: убираем всё, что они создают.
|
command -v ufw >/dev/null 2>&1 || { kl_warn "ufw не установлен — порты смотреть нечем"; return 0; }
|
||||||
local -a paths=(
|
local -a opened=() needed=() extra=()
|
||||||
/usr/local/x-ui /etc/x-ui /usr/bin/x-ui /usr/bin/x-ui.new /etc/systemd/system/x-ui.service
|
mapfile -t opened < <(ufw status 2>/dev/null | awk '$2 == "ALLOW" { print $1 }' | sort -u)
|
||||||
/usr/local/bin/kit /usr/local/lib/kit /usr/local/lib/kit-sub /etc/kit /etc/kit-sub
|
# Нужные порты: те, что слушают наши подключения (x-ui и ядро Xray), плюс SSH.
|
||||||
/etc/systemd/system/kit-sub.service /etc/systemd/system/kit-update.service
|
mapfile -t needed < <({ ss -tlnp 2>/dev/null; ss -ulnp 2>/dev/null; } \
|
||||||
/etc/systemd/system/kit-update.timer /etc/systemd/system/kit-limit.service
|
| awk '/x-ui|xray/ { print $4 }' | sed 's/.*://' | sort -un; ss -tlnp 2>/dev/null | awk '/sshd/ { print $4 }' | sed 's/.*://' | sort -un)
|
||||||
/etc/systemd/system/kit-limit.timer
|
local p q keep
|
||||||
/etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu /etc/cron.d/kit-sub-cert
|
for p in "${opened[@]}"; do
|
||||||
/etc/nginx/kit-stream.conf /etc/nginx/conf.d/kit.conf /var/www/kit
|
keep=no
|
||||||
/root/3x-ui.txt /root/cert /root/.acme.sh /etc/letsencrypt /var/log/kit-install-check.log
|
case $p in 80/tcp | 443/tcp) keep=yes ;; esac # 443 — работа, 80 — продление сертификата
|
||||||
/etc/sysctl.d/99-kit-local-icmp.conf /etc/apt/apt.conf.d/52-kit-local-upgrades
|
for q in "${needed[@]}"; do [[ $p == "$q/tcp" || $p == "$q/udp" || $p == "$q" ]] && keep=yes; done
|
||||||
/etc/apt/apt.conf.d/20auto-upgrades
|
[[ $keep == yes ]] && continue
|
||||||
)
|
extra+=("$p")
|
||||||
local -a masks=(/root/kit-local-backup-* /root/kit-backup-* /root/x-ui-before-sni-* /root/x-ui-before-update-*)
|
done
|
||||||
local -a extra=()
|
printf ' открыто в ufw: %s\n' "$(printf '%s ' "${opened[@]}")"
|
||||||
local m
|
if ((${#needed[@]})); then printf ' нужны: %s\n' "$(printf '%s/tcp %s/udp ' "${needed[@]}" "${needed[@]}")"
|
||||||
for m in "${masks[@]}"; do for f in $m; do [[ -e $f ]] && extra+=("$f"); done; done
|
else printf ' нужны: (не определил — смотрите kit check)\n'; fi
|
||||||
|
if ((${#extra[@]})); then
|
||||||
kl_step "Будет удалено"
|
printf ' лишние: %s\n' "$(printf '%s ' "${extra[@]}")"
|
||||||
printf ' службы: %s\n' "${svc[*]}"
|
printf ' закрыть лишние: kit local ports --close-extra\n'
|
||||||
printf ' файлы и каталоги: %s\n' "${paths[*]}"
|
if [[ $close == yes ]]; then
|
||||||
[[ ${#extra[@]} -eq 0 ]] || printf ' копии и журналы: %s\n' "${extra[*]}"
|
for p in "${extra[@]}"; do ufw --force delete allow "$p" >/dev/null 2>&1 && printf ' закрыл %s\n' "$p"; done
|
||||||
printf ' пользователь kit-sub (если создавался)\n'
|
|
||||||
printf ' правила ufw для 80/tcp, 443/tcp и 443/udp; ответ на ping возвращается\n'
|
|
||||||
kl_ok "SSH не трогаем: ни порт, ни конфиги, ни правила — иначе можно потерять доступ"
|
|
||||||
if [[ $reset_ssh == yes ]]; then
|
|
||||||
kl_warn "--reset-ssh: снесу и наш ssh-дроп-ин (порт вернётся к системному). Убедитесь, что вход по нему работает!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if kl_dry; then
|
|
||||||
printf '\n (dry-run) ничего не делаю. Запустить по-настоящему: kit local uninstall\n'
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
kl_need_root || return 1
|
|
||||||
|
|
||||||
if [[ -z ${KIT_LOCAL_UNINSTALL_CONFIRMED:-} ]]; then
|
|
||||||
printf '\n%sЧтобы удалить всё перечисленное, напишите слово «удалить»: %s' "${B:-}" "${N:-}"
|
|
||||||
read -r ans </dev/tty 2>/dev/null || ans=""
|
|
||||||
[[ ${ans,,} == "удалить" ]] || { kl_warn "не подтверждено — ничего не удаляю"; return 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
kl_step "Останавливаю и отключаю службы"
|
|
||||||
local s
|
|
||||||
for s in "${svc[@]}"; do
|
|
||||||
if systemctl list-unit-files 2>/dev/null | grep -q "^$s"; then
|
|
||||||
systemctl disable --now "$s" >/dev/null 2>&1 && kl_ok "остановлено и отключено: $s" || kl_warn "не смог остановить: $s"
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
if kl_have nginx; then
|
|
||||||
local others
|
|
||||||
others=$( { find /etc/nginx/conf.d /etc/nginx/sites-enabled /etc/nginx/streams-enabled -mindepth 1 2>/dev/null || true; } \
|
|
||||||
| { grep -vE 'kit\.conf$|(^|/)default$' || true; } | wc -l)
|
|
||||||
if [[ $others -eq 0 ]]; then
|
|
||||||
systemctl disable --now nginx >/dev/null 2>&1 && kl_ok "nginx остановлен (чужих сайтов в нём не было)"
|
|
||||||
KL_NGINX_PURGE=yes
|
|
||||||
else
|
|
||||||
kl_ok "nginx оставляю: в нём есть другие сайты — уберу только наши строки и файлы"
|
|
||||||
KL_NGINX_PURGE=no
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
kl_step "Убираю файлы"
|
|
||||||
local f
|
|
||||||
for f in "${paths[@]}"; do
|
|
||||||
[[ -e $f ]] || continue
|
|
||||||
rm -rf "$f" && kl_ok "удалено: $f" || kl_warn "не удалось удалить: $f"
|
|
||||||
done
|
|
||||||
for f in ${extra[@]+"${extra[@]}"}; do rm -rf "$f" && kl_ok "удалено: $f"; done
|
|
||||||
|
|
||||||
# Наши строки в чужом nginx: include stream-конфига и симлинки, если были.
|
|
||||||
if [[ -f /etc/nginx/nginx.conf ]] && grep -q 'kit-stream.conf' /etc/nginx/nginx.conf; then
|
|
||||||
kl_backup /etc/nginx/nginx.conf
|
|
||||||
sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf && kl_ok "из nginx.conf убрана наша строка включения"
|
|
||||||
fi
|
|
||||||
local lnk
|
|
||||||
for lnk in /etc/nginx/sites-enabled/*kit* /etc/nginx/streams-enabled/*kit*; do
|
|
||||||
[[ -e $lnk || -L $lnk ]] && rm -f "$lnk" && kl_ok "удалена ссылка: $lnk"
|
|
||||||
done
|
|
||||||
if [[ ${KL_NGINX_PURGE:-no} == yes ]]; then
|
|
||||||
apt-get remove -y --purge nginx nginx-common >/dev/null 2>&1 && kl_ok "пакет nginx удалён"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Пользователь подписки (работает не от root).
|
|
||||||
if id -u kit-sub >/dev/null 2>&1; then
|
|
||||||
pkill -u kit-sub >/dev/null 2>&1 || true
|
|
||||||
userdel -r kit-sub >/dev/null 2>&1 && kl_ok "пользователь kit-sub удалён" || kl_warn "не смог удалить пользователя kit-sub"
|
|
||||||
fi
|
|
||||||
|
|
||||||
kl_step "Возвращаю систему"
|
|
||||||
systemctl daemon-reload
|
|
||||||
if [[ $reset_ssh == yes && -f /etc/systemd/system/ssh.socket.d/99-kit-local-port.conf ]]; then
|
|
||||||
rm -f /etc/systemd/system/ssh.socket.d/99-kit-local-port.conf
|
|
||||||
rm -f /etc/ssh/sshd_config.d/99-kit-local-port.conf
|
|
||||||
systemctl daemon-reload
|
|
||||||
kl_run systemctl restart ssh.socket 2>/dev/null || kl_run systemctl restart ssh 2>/dev/null || true
|
|
||||||
kl_warn "ssh-дроп-ин удалён: порт вернулся к системному. Проверьте вход в НОВОМ окне!"
|
|
||||||
fi
|
|
||||||
if kl_have ufw && kl_ufw_active; then
|
|
||||||
local p
|
|
||||||
for p in 80/tcp 443/tcp 443/udp; do ufw delete allow "$p" >/dev/null 2>&1 && kl_ok "правило ufw снято: $p"; done
|
|
||||||
if [[ -f $KL_UFW_RULES ]]; then
|
|
||||||
kl_backup "$KL_UFW_RULES"; kl_backup "$KL_UFW6_RULES"
|
|
||||||
sed -i 's/^# -A ufw-before-input -p icmp --icmp-type echo-request/-A ufw-before-input -p icmp --icmp-type echo-request/' "$KL_UFW_RULES"
|
|
||||||
[[ -f $KL_UFW6_RULES ]] && sed -i 's/^# -A ufw6-before-input -p icmpv6 --icmpv6-type echo-request/-A ufw6-before-input -p icmpv6 --icmpv6-type echo-request/' "$KL_UFW6_RULES"
|
|
||||||
ufw reload >/dev/null 2>&1 && kl_ok "ответ на ping возвращён"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sysctl -q -w net.ipv4.icmp_echo_ignore_all=0 >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
kl_step "Проверка (теми же условиями, что и установщик)"
|
|
||||||
local left=0
|
|
||||||
if [[ -f /root/3x-ui.txt && -x /usr/local/x-ui/x-ui ]]; then
|
|
||||||
kl_warn "установщик всё ещё считает панель установленной"; left=1
|
|
||||||
fi
|
|
||||||
for s in $(systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' \
|
|
||||||
| grep -E '^(x-ui|kit-|kit\.)' | grep -E '\.(service|timer)$'); do
|
|
||||||
kl_warn "осталась наша служба: $s"; left=1
|
|
||||||
done
|
|
||||||
# Добивка: убираем всё, что осталось и подходит под наши шаблоны (вдруг автор добавил новый файл
|
|
||||||
# или что-то не удалилось с первого раза). Затем проверяем ещё раз.
|
|
||||||
for f in /etc/kit* /usr/local/lib/kit* /usr/local/bin/kit /etc/cron.d/kit-* /var/www/kit \
|
|
||||||
/etc/systemd/system/kit-*.service /etc/systemd/system/kit-*.timer \
|
|
||||||
/etc/systemd/system/x-ui*.service /usr/lib/systemd/system/kit-*.service \
|
|
||||||
/etc/x-ui /usr/local/x-ui /usr/bin/x-ui /usr/bin/x-ui.new /root/3x-ui.txt /root/cert; do
|
|
||||||
[[ -e $f ]] || continue
|
|
||||||
rm -rf "$f" && { kl_ok "добито: $f"; i=1; } || kl_warn "не удалось удалить: $f"
|
|
||||||
done
|
|
||||||
((i)) && systemctl daemon-reload
|
|
||||||
found=$( { ls -d /etc/kit* /usr/local/lib/kit* /usr/local/bin/kit /etc/cron.d/kit-* /var/www/kit \
|
|
||||||
/etc/systemd/system/*kit* /etc/x-ui /usr/local/x-ui /root/3x-ui.txt /root/cert 2>/dev/null || true; } | head -20)
|
|
||||||
[[ -z $found ]] || { kl_warn "осталось на диске:"; printf ' %s\n' $found; left=1; }
|
|
||||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE '[:.]443$' && { kl_warn "порт 443 всё ещё кем-то слушается"; left=1; }
|
|
||||||
if ((left)); then
|
|
||||||
kl_warn "осталось перечисленное выше — пришлите этот вывод, поправим список в kit local uninstall"
|
|
||||||
else
|
else
|
||||||
kl_ok "чисто: от установки не осталось ни файлов, ни служб, порт 443 свободен"
|
printf ' лишних нет\n'
|
||||||
kl_ok "проверка установщика пройдена: он снова поставит сервер с нуля"
|
|
||||||
fi
|
fi
|
||||||
kl_ok "SSH не тронут (если не указан --reset-ssh)"
|
}
|
||||||
kl_step "Готово: сервер в состоянии до установки, можно ставить заново"
|
kit_local_access() { # доступ к панели, ссылки на подключения, заглушка и сертификат — одним экраном
|
||||||
|
local u=${1:-} till2
|
||||||
|
printf '%s\n' "${B:-}Доступ к панели 3X-UI${N:-} (файл /root/3x-ui.txt, виден только root)"
|
||||||
|
if grep -aqE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
|
||||||
|
grep -aE '^[[:space:]]*(Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^[[:space:]]*/ /'
|
||||||
|
else
|
||||||
|
kl_warn "данных панели в /root/3x-ui.txt нет"
|
||||||
|
fi
|
||||||
|
printf '\n%s\n' "${B:-}Сертификат панели${N:-}"
|
||||||
|
if [[ -s /root/cert/ip/fullchain.pem ]] && openssl x509 -in /root/cert/ip/fullchain.pem -noout -issuer 2>/dev/null | grep -q "Let's Encrypt"; then
|
||||||
|
printf ' Let'"'"'s Encrypt на IP — доверенный, браузер не ругается\n'
|
||||||
|
elif [[ -s /root/cert/ip/fullchain.pem ]]; then
|
||||||
|
printf ' самоподписанный — браузер предупредит, подписка в приложениях может не открыться\n'
|
||||||
|
else
|
||||||
|
printf ' сертификата на IP нет: панель открывается только через SSH-туннель\n'
|
||||||
|
fi
|
||||||
|
till2=$(grep -rhoE 'retry after [0-9-]+ [0-9:]+ UTC' /var/log/3x-ui-install.log "$HOME"/.acme.sh/*/ca 2>/dev/null | tail -1 | sed 's/^retry after //')
|
||||||
|
[[ -n $till2 ]] && printf ' лимит Let'"'"'s Encrypt: повтор выпуска возможен после %s (затем запустите установщик снова)\n' "$till2"
|
||||||
|
printf '\n%s\n' "${B:-}Ссылки на подключения и QR-код${N:-}"
|
||||||
|
if [[ -z $u ]]; then u=$(kit user list 2>/dev/null | awk 'NR > 1 && NF { print $1; exit }'); u=${u:-admin}; fi
|
||||||
|
local now; now=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
|
||||||
|
if [[ $(wc -w <<<"$now") -gt 1 ]]; then kit user link "$u" --all; else kit user link "$u"; fi
|
||||||
|
printf '\n%s\n' "${B:-}Заглушка (что видит посторонний, открыв адрес сервера)${N:-}"
|
||||||
|
kit local site --show
|
||||||
|
printf ' посмотреть снаружи: откройте https://<IP сервера>/ в браузере или curl -k https://<IP>/
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
kit_local_links() { # [имя] — подписка, QR-код и ссылки по протоколам (то же, что «kit → Пользователи → Показать ссылки»)
|
||||||
|
local u=${1:-} now
|
||||||
|
if [[ -z $u ]]; then
|
||||||
|
u=$(kit user list 2>/dev/null | awk 'NR > 1 && NF { print $1; exit }')
|
||||||
|
u=${u:-admin}
|
||||||
|
fi
|
||||||
|
printf ' пользователь: %s\n' "$u"
|
||||||
|
now=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
|
||||||
|
# Ссылки на каждый протокол — только если протоколов больше одного; при одном достаточно подписки.
|
||||||
|
if [[ $(wc -w <<<"$now") -gt 1 ]]; then kit user link "$u" --all; else kit user link "$u"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
kit_local_uninstall() {
|
||||||
|
# Наш шов: логику удаления отдаём авторской команде «kit uninstall». Она снимает и сам kit, чтобы на
|
||||||
|
# сервере не осталось следов, поэтому своя реализация здесь больше не нужна — команда оставлена
|
||||||
|
# синонимом, чтобы привычное «kit local uninstall» продолжало работать.
|
||||||
|
local a=() x
|
||||||
|
for x in "$@"; do
|
||||||
|
case $x in
|
||||||
|
--dry-run | --yes | --no-backup) a+=("$x") ;;
|
||||||
|
--help | -h)
|
||||||
|
printf 'kit local uninstall [--dry-run] [--yes] [--no-backup] — то же, что kit uninstall\n'
|
||||||
|
return 0 ;;
|
||||||
|
*) kl_die "kit local uninstall [--dry-run] [--yes] [--no-backup]"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ -x /usr/local/bin/kit ]] || { kl_die "не нашёл /usr/local/bin/kit — удалить вручную: kit uninstall"; return 1; }
|
||||||
|
printf ' удаляю по авторской логике: /usr/local/bin/kit uninstall %s\n' "${a[*]}"
|
||||||
|
exec /usr/local/bin/kit uninstall "${a[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- полная справка `kit help`
|
# ---------------------------------------------------------------- полная справка `kit help`
|
||||||
@@ -1133,7 +1068,10 @@ ${B:-}3X-UI_autoVPS ${KIT_VERSION:-?} — все команды${N:-} (то ж
|
|||||||
kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит
|
kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит
|
||||||
|
|
||||||
Проверка и починка
|
Проверка и починка
|
||||||
kit local uninstall снести установку целиком (панель, туннель, службы kit, сайт, сертификаты); SSH не трогает
|
kit local access [имя] доступ к панели, сертификат, ссылки и заглушка одним экраном
|
||||||
|
kit local links [имя] подписка, QR-код и ссылки по протоколам (по умолчанию — первый пользователь)
|
||||||
|
kit local ports [--close-extra] открытые порты: нужные, лишние; --close-extra закрывает лишние
|
||||||
|
kit local uninstall то же, что kit uninstall: снести установку целиком и сам kit; SSH не трогает
|
||||||
kit check [--deep | --fix] проверка сервера (--deep — ходит наружу как клиент)
|
kit check [--deep | --fix] проверка сервера (--deep — ходит наружу как клиент)
|
||||||
kit fix [--dry-run] безопасные починки: время, службы, конфиги
|
kit fix [--dry-run] безопасные починки: время, службы, конфиги
|
||||||
kit local hardening сводка по защите и что ещё можно включить
|
kit local hardening сводка по защите и что ещё можно включить
|
||||||
|
|||||||
+52
-10
@@ -39,6 +39,16 @@ CLASH_UA = re.compile(r"clash|mihomo|flclash|stash|nyanpasu|meta", re.I)
|
|||||||
# AmneziaWG добавляем только приложениям на ядре Mihomo. Karing, Hiddify и другие на sing-box
|
# AmneziaWG добавляем только приложениям на ядре Mihomo. Karing, Hiddify и другие на sing-box
|
||||||
# тоже могут просить формат Clash (Karing так и делает), но AmneziaWG не умеют.
|
# тоже могут просить формат Clash (Karing так и делает), но AmneziaWG не умеют.
|
||||||
NO_AWG_UA = re.compile(r"karing|hiddify|nekobox|sing-?box|husi|stash|shadowrocket|v2box|streisand|happ|loon|surge|quantumult", re.I)
|
NO_AWG_UA = re.compile(r"karing|hiddify|nekobox|sing-?box|husi|stash|shadowrocket|v2box|streisand|happ|loon|surge|quantumult", re.I)
|
||||||
|
# Hiddify 4.x в строке приложения называет себя и Clash, поэтому панель отдаёт ему Clash-YAML, а он обрезает в нём пробелы и
|
||||||
|
# выдаёт «Unable to determine config format». Панели такой клиент представляем без слов про Clash: она отдаст обычный
|
||||||
|
# список ссылок (base64), который Hiddify читает без проблем.
|
||||||
|
HIDDIFY_UA = re.compile(r"hiddify", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def upstream_ua(ua):
|
||||||
|
return CLASH_UA.sub("", ua).strip() if HIDDIFY_UA.search(ua) else ua
|
||||||
|
|
||||||
|
|
||||||
SUB_ID = re.compile(r"^[A-Za-z0-9_.@-]{1,64}$")
|
SUB_ID = re.compile(r"^[A-Za-z0-9_.@-]{1,64}$")
|
||||||
# Страница подписки для браузера (React от 3X-UI) подгружает скрипты и стили из «<путь>/assets/…»: пропускаем только такие файлы.
|
# Страница подписки для браузера (React от 3X-UI) подгружает скрипты и стили из «<путь>/assets/…»: пропускаем только такие файлы.
|
||||||
ASSET = re.compile(r"^assets/[A-Za-z0-9_.-]{1,128}\.(js|css|woff2?|svg|png|ico|map)$")
|
ASSET = re.compile(r"^assets/[A-Za-z0-9_.-]{1,128}\.(js|css|woff2?|svg|png|ico|map)$")
|
||||||
@@ -65,6 +75,11 @@ DOH_DEFAULT = ["https://1.1.1.1/dns-query", "https://8.8.8.8/dns-query"]
|
|||||||
PRIVATE_NETS = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16"]
|
PRIVATE_NETS = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16"]
|
||||||
# Дополнительные пути, которые панель показывает на странице подписки (Clash и JSON): обрабатываются так же, как основной.
|
# Дополнительные пути, которые панель показывает на странице подписки (Clash и JSON): обрабатываются так же, как основной.
|
||||||
EXTRA = ["/" + x.strip("/") + "/" for x in CONF.get("extra_paths", []) if isinstance(x, str) and re.fullmatch(r"/?[A-Za-z0-9_-]+/?", x)]
|
EXTRA = ["/" + x.strip("/") + "/" for x in CONF.get("extra_paths", []) if isinstance(x, str) and re.fullmatch(r"/?[A-Za-z0-9_-]+/?", x)]
|
||||||
|
# После kit net site прежнее имя сайта REALITY ещё неделю принимается сервером (льготный срок), а панель берёт имя для ссылки
|
||||||
|
# случайно из списка. Карта «прежнее имя → нынешнее» от kit: в подписку уходит только нынешнее. Нет файла – ничего не меняем.
|
||||||
|
GRACE_FILE = os.environ.get("KIT_SUB_GRACE") or "/etc/kit-sub/sni-grace.json"
|
||||||
|
SITE_NAME = re.compile(r"[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+")
|
||||||
|
LINK_SNI = re.compile(r"([?&]sni=)([^&#\s]*)")
|
||||||
|
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
@@ -94,10 +109,32 @@ def fix_userinfo(value):
|
|||||||
JSON_FIX_MAX = 2 * 1024 * 1024
|
JSON_FIX_MAX = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def fix_vless_encryption(body):
|
def load_grace():
|
||||||
|
"""Карта «прежнее имя сайта → нынешнее» (файл от kit). Только имена сайтов, остальное отбрасываем."""
|
||||||
|
try:
|
||||||
|
with open(GRACE_FILE, "rb") as f:
|
||||||
|
raw = f.read(65537)
|
||||||
|
data = json.loads(raw.decode("utf-8")) if len(raw) <= 65536 else {}
|
||||||
|
except (OSError, ValueError, UnicodeError, RecursionError):
|
||||||
|
return {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
return {k: v for k, v in list(data.items())[:100] if isinstance(k, str) and isinstance(v, str)
|
||||||
|
and len(k) <= 253 and len(v) <= 253 and SITE_NAME.fullmatch(k) and SITE_NAME.fullmatch(v)}
|
||||||
|
|
||||||
|
|
||||||
|
def grace_link(line, grace):
|
||||||
|
"""Ссылка REALITY с прежним именем сайта → с нынешним; остальные ссылки как есть."""
|
||||||
|
if not grace or not line.startswith("vless://") or "security=reality" not in line:
|
||||||
|
return line
|
||||||
|
return LINK_SNI.sub(lambda m: m.group(1) + grace.get(m.group(2), m.group(2)), line)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_vless_encryption(body, grace=None):
|
||||||
"""JSON-подписка панели: у vless-пользователя без encryption (нет поля или пусто) ставим "none" – Xray иначе
|
"""JSON-подписка панели: у vless-пользователя без encryption (нет поля или пусто) ставим "none" – Xray иначе
|
||||||
не принимает конфиг. Две формы: vnext[].users[] и плоская (settings.id/encryption). Непустое значение
|
не принимает конфиг. Две формы: vnext[].users[] и плоская (settings.id/encryption). Непустое значение
|
||||||
(например, mlkem) не трогаем. Не разобралось, слишком большое или менять нечего – тело как пришло."""
|
(например, mlkem) не трогаем. Прежнее (льготное) имя сайта REALITY – на нынешнее (grace).
|
||||||
|
Не разобралось, слишком большое или менять нечего – тело как пришло."""
|
||||||
if len(body) > JSON_FIX_MAX:
|
if len(body) > JSON_FIX_MAX:
|
||||||
return body
|
return body
|
||||||
try:
|
try:
|
||||||
@@ -116,6 +153,11 @@ def fix_vless_encryption(body):
|
|||||||
for cfg in (data if isinstance(data, list) else [data]):
|
for cfg in (data if isinstance(data, list) else [data]):
|
||||||
outs = cfg.get("outbounds") if isinstance(cfg, dict) else None
|
outs = cfg.get("outbounds") if isinstance(cfg, dict) else None
|
||||||
for o in outs if isinstance(outs, list) else []:
|
for o in outs if isinstance(outs, list) else []:
|
||||||
|
ss = o.get("streamSettings") if isinstance(o, dict) else None
|
||||||
|
rs = ss.get("realitySettings") if isinstance(ss, dict) else None
|
||||||
|
if grace and isinstance(rs, dict) and rs.get("serverName") in grace:
|
||||||
|
rs["serverName"] = grace[rs["serverName"]]
|
||||||
|
changed = True
|
||||||
st = o.get("settings") if isinstance(o, dict) and o.get("protocol") == "vless" else None
|
st = o.get("settings") if isinstance(o, dict) and o.get("protocol") == "vless" else None
|
||||||
if not isinstance(st, dict):
|
if not isinstance(st, dict):
|
||||||
continue
|
continue
|
||||||
@@ -127,9 +169,9 @@ def fix_vless_encryption(body):
|
|||||||
return json.dumps(data, ensure_ascii=False).encode() if changed else body
|
return json.dumps(data, ensure_ascii=False).encode() if changed else body
|
||||||
|
|
||||||
|
|
||||||
def strip_links(body):
|
def strip_links(body, grace=None):
|
||||||
"""Список ссылок (base64 или текст) без vpn:// и tg:// – их не умеет ни одно VPN-приложение
|
"""Список ссылок (base64 или текст) без vpn:// и tg:// – их не умеет ни одно VPN-приложение
|
||||||
со ссылками: vpn:// – конфиг для AmneziaVPN, tg:// – прокси для Telegram."""
|
со ссылками: vpn:// – конфиг для AmneziaVPN, tg:// – прокси для Telegram. Прежнее имя сайта REALITY – на нынешнее."""
|
||||||
text = body.decode("utf-8", "replace").strip()
|
text = body.decode("utf-8", "replace").strip()
|
||||||
encoded = "://" not in text
|
encoded = "://" not in text
|
||||||
if encoded:
|
if encoded:
|
||||||
@@ -137,7 +179,7 @@ def strip_links(body):
|
|||||||
text = base64.b64decode(text + "=" * (-len(text) % 4)).decode("utf-8", "replace")
|
text = base64.b64decode(text + "=" * (-len(text) % 4)).decode("utf-8", "replace")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return body
|
return body
|
||||||
lines = [l for l in text.splitlines() if l.strip() and not l.startswith(("vpn://", "tg://"))]
|
lines = [grace_link(l, grace) for l in text.splitlines() if l.strip() and not l.startswith(("vpn://", "tg://"))]
|
||||||
out = "\n".join(lines)
|
out = "\n".join(lines)
|
||||||
return base64.b64encode(out.encode()).decode().encode() if encoded else out.encode()
|
return base64.b64encode(out.encode()).decode().encode() if encoded else out.encode()
|
||||||
|
|
||||||
@@ -416,7 +458,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
ua = self.headers.get("User-Agent", "")
|
ua = self.headers.get("User-Agent", "")
|
||||||
host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", ""))
|
host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", ""))
|
||||||
accept = self.headers.get("Accept", "")
|
accept = self.headers.get("Accept", "")
|
||||||
code, headers, body = upstream(sub_id, ua, host, accept, prefix)
|
code, headers, body = upstream(sub_id, upstream_ua(ua), host, accept, prefix)
|
||||||
if code is None:
|
if code is None:
|
||||||
return self.send_html_error(502)
|
return self.send_html_error(502)
|
||||||
|
|
||||||
@@ -435,9 +477,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
elif code == 200 and ("json" in headers.get("content-type", "") or (
|
elif code == 200 and ("json" in headers.get("content-type", "") or (
|
||||||
"text/plain" in headers.get("content-type", "") and body.lstrip()[:1] in (b"[", b"{"))):
|
"text/plain" in headers.get("content-type", "") and body.lstrip()[:1] in (b"[", b"{"))):
|
||||||
# Панель 3.9 отдаёт JSON-подписку с Content-Type text/plain – определяем по телу.
|
# Панель 3.9 отдаёт JSON-подписку с Content-Type text/plain – определяем по телу.
|
||||||
body = fix_vless_encryption(body)
|
body = fix_vless_encryption(body, load_grace())
|
||||||
elif code == 200 and "text/plain" in headers.get("content-type", ""):
|
elif code == 200 and "text/plain" in headers.get("content-type", ""):
|
||||||
body = strip_links(body)
|
body = strip_links(body, load_grace())
|
||||||
if code == 200 and clash and CONF.get("auto", True):
|
if code == 200 and clash and CONF.get("auto", True):
|
||||||
body = add_auto(body)
|
body = add_auto(body)
|
||||||
if code == 200 and clash and CONF.get("dns", True):
|
if code == 200 and clash and CONF.get("dns", True):
|
||||||
@@ -546,7 +588,7 @@ def main():
|
|||||||
cert, key = os.path.join(CREDS, "cert.pem"), os.path.join(CREDS, "key.pem")
|
cert, key = os.path.join(CREDS, "cert.pem"), os.path.join(CREDS, "key.pem")
|
||||||
if not cert:
|
if not cert:
|
||||||
srv = Server((CONF.get("listen", "127.0.0.1"), int(CONF["port"])), Handler)
|
srv = Server((CONF.get("listen", "127.0.0.1"), int(CONF["port"])), Handler)
|
||||||
log(f"kit-sub слушает http://{CONF.get('listen', '127.0.0.1')}:{CONF['port']}{PATH} (TLS снимает nginx)")
|
log(f"kit-sub слушает {CONF.get('listen', '127.0.0.1')}:{CONF['port']} (TLS снимает nginx)") # путь подписки в журнал не пишем: он секретный
|
||||||
srv.serve_forever()
|
srv.serve_forever()
|
||||||
return
|
return
|
||||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
@@ -571,7 +613,7 @@ def main():
|
|||||||
threading.Thread(target=reload_cert, daemon=True).start()
|
threading.Thread(target=reload_cert, daemon=True).start()
|
||||||
srv = Server((CONF.get("listen", "0.0.0.0"), int(CONF["port"])), Handler)
|
srv = Server((CONF.get("listen", "0.0.0.0"), int(CONF["port"])), Handler)
|
||||||
srv.ssl_ctx = ctx
|
srv.ssl_ctx = ctx
|
||||||
log(f"kit-sub слушает {CONF.get('listen', '0.0.0.0')}:{CONF['port']}{PATH}")
|
log(f"kit-sub слушает {CONF.get('listen', '0.0.0.0')}:{CONF['port']}")
|
||||||
srv.serve_forever()
|
srv.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+897
-117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Проверка: подсказка «Выбор [0-N]» в каждом меню kit должна совпадать с числом пунктов.
|
||||||
|
|
||||||
|
Пункты считаются по строкам box (с учётом переносов через «\\»). Смысл проверки: мы добавляем в меню
|
||||||
|
свои пункты — однажды подсказка осталась «0-10» при 11 пунктах, и человек не мог выбрать последний.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
path = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "scripts/kit.sh")
|
||||||
|
text = path.read_text(encoding="utf-8").replace("\\\n", "")
|
||||||
|
lines = text.splitlines()
|
||||||
|
bad = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
m = re.search(r'\bbox\s+"([^"]+)"(.*)$', line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
items = re.findall(r'"(\d+)\.\s', m.group(2))
|
||||||
|
maxn = max((int(x) for x in items), default=0)
|
||||||
|
prompt = None
|
||||||
|
for j in range(i + 1, min(i + 120, len(lines))):
|
||||||
|
pm = re.search(r'ask_num\s+"Выбор \[0-(\d+)\]', lines[j])
|
||||||
|
if pm:
|
||||||
|
prompt = int(pm.group(1))
|
||||||
|
break
|
||||||
|
if prompt != maxn:
|
||||||
|
print(f"! меню «{m.group(1)}»: пунктов {maxn}, а подсказка «Выбор [0-{prompt}]»", file=sys.stderr)
|
||||||
|
bad += 1
|
||||||
|
print(f"меню проверены: {len(lines) and sum(1 for l in lines if 'box \"' in l)} · рассинхронов: {bad}")
|
||||||
|
sys.exit(1 if bad else 0)
|
||||||
+54
-33
@@ -116,9 +116,9 @@ def main():
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
' # SHA256:VDuGgJ8dOeCNXB4nBZf3+kRlWthw3+vh8rfMxzMSRIM (itsnotkubrick, 2026-09-30)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrTGCDhhnm8XO1ekpPJuSWRVCJiFiupEspfQxcbEBmz 3X-UI KIT releases"\n',
|
' # SHA256:bNjI10eGVGjdFeDJnAHvUFi73rlrXVy6vKFf8lmNyxU (itsnotkubrick, 2026-10-14; прежний ключ SHA256:VDuGgJ8d… утерян, снят)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEWDFGq2Wno/ADRS2E6UKsgFbUSLvaiPIfRBvSEGJr81 3X-UI KIT releases"\n',
|
||||||
False,
|
False,
|
||||||
' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n',
|
' # Наш шов: здесь только ключ форка (hacker07). Авторские ключи сняты намеренно — сервер обязан\n # принимать обновления лишь из нашего канала (git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS).\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n',
|
||||||
'авто-шов: replace, строк 2',
|
'авто-шов: replace, строк 2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -191,6 +191,13 @@ def main():
|
|||||||
' echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
|
' echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'scripts/kit.sh',
|
||||||
|
' echo "${B}kit fix${N} – безопасные исправления (службы, права, автообновление, сертификат, время)"\n echo\n',
|
||||||
|
False,
|
||||||
|
' # Наш шов: строку-описание команды не печатаем — при установке это лишний шум,\n # а смысл виден по результату (подробности — в kit help).\n',
|
||||||
|
'авто-шов: replace, строк 2',
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"\n',
|
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"\n',
|
||||||
@@ -214,25 +221,32 @@ def main():
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"\n',
|
' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "10. Удалить KIT с сервера" "0. Выход"\n',
|
||||||
False,
|
False,
|
||||||
' "7. Резервная копия" "8. Версии" "" "9. Удалить всё установленное" "10. Выход" \\\n "11. Панель 3X-UI (меню x-ui)"\n',
|
' "7. Резервная копия" "8. Версии" "" "9. Доступ к панели, ссылки и заглушка" "10. Панель x-ui" "11. Удалить KIT с сервера" "0. Выход"\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
' ask_num "Выбор [0-9]: "; c=$REPLY\n',
|
' ask_num "Выбор [0-10]: "; c=$REPLY\n',
|
||||||
False,
|
False,
|
||||||
' ask_num "Выбор [1-11]: "; c=$REPLY\n',
|
' ask_num "Выбор [0-11]: "; c=$REPLY\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 0 | "") return 0 ;;\n',
|
' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 10) run_action cmd_uninstall; pause ;;\n',
|
||||||
False,
|
False,
|
||||||
' 9) kit_local_uninstall && return 0 ;; # снос — и сразу выход: показывать меню после удаления незачем\n 10 | 0 | "") return 0 ;;\n 11) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n',
|
' # Наш шов: пункт 9 — доступ к панели, ссылки на подключения и состояние заглушки в одном месте.\n 9) kit local access; pause ;;\n 10) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 11) run_action cmd_uninstall; pause ;;\n',
|
||||||
'авто-шов: replace, строк 2',
|
'авто-шов: replace, строк 2',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'scripts/kit.sh',
|
||||||
|
' echo "${B}Полное удаление 3X-UI KIT${N}"\n',
|
||||||
|
False,
|
||||||
|
' echo "${B}Полное удаление 3X-UI_autoVPS${N}"\n',
|
||||||
|
'авто-шов: replace, строк 1',
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'scripts/kit.sh',
|
'scripts/kit.sh',
|
||||||
'${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
|
'${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
|
||||||
@@ -296,13 +310,6 @@ def main():
|
|||||||
' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n # Наш шов: лимит Let\'s Encrypt считается на точный набор имён (5 сертификатов за 7 суток). Если у acme.sh\n # уже лежит годный сертификат на этот домен — берём его и не заказываем новый, лишний заказ упирается в лимит.\n local need_issue=yes\n if [[ -s /root/.acme.sh/${DOMAIN}_ecc/fullchain.cer ]] \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q \'does match\' \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkend 86400 >/dev/null 2>&1; then\n ok "у acme.sh уже есть годный сертификат для ${B}$DOMAIN${N} — беру его, новый не заказываю"\n need_issue=no\n fi\n',
|
' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n # Наш шов: лимит Let\'s Encrypt считается на точный набор имён (5 сертификатов за 7 суток). Если у acme.sh\n # уже лежит годный сертификат на этот домен — берём его и не заказываем новый, лишний заказ упирается в лимит.\n local need_issue=yes\n if [[ -s /root/.acme.sh/${DOMAIN}_ecc/fullchain.cer ]] \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkhost "$DOMAIN" 2>/dev/null | grep -q \'does match\' \\\n && openssl x509 -in "/root/.acme.sh/${DOMAIN}_ecc/fullchain.cer" -noout -checkend 86400 >/dev/null 2>&1; then\n ok "у acme.sh уже есть годный сертификат для ${B}$DOMAIN${N} — беру его, новый не заказываю"\n need_issue=no\n fi\n',
|
||||||
'авто-шов: insert, строк 1',
|
'авто-шов: insert, строк 1',
|
||||||
),
|
),
|
||||||
(
|
|
||||||
'scripts/3x-ui.sh',
|
|
||||||
' local try\n for try in 1 2 3; do\n rc=0\n "$acme" --issue -d "$DOMAIN" --standalone --httpport 80 --server letsencrypt --keylength ec-256 ${rc_force:-} >>"$log" 2>&1 || rc=$?\n [[ $rc == 0 || $rc == 2 ]] && break\n local rc_force="--force"\n sleep 10\n done\n',
|
|
||||||
False,
|
|
||||||
' if [[ $need_issue == yes ]]; then\n local try\n for try in 1 2 3; do\n rc=0\n "$acme" --issue -d "$DOMAIN" --standalone --httpport 80 --server letsencrypt --keylength ec-256 ${rc_force:-} >>"$log" 2>&1 || rc=$?\n [[ $rc == 0 || $rc == 2 ]] && break\n local rc_force="--force"\n sleep 10\n done\n else\n rc=2 # сертификат взяли готовый: для кода ниже это то же, что «уже свежий»\n fi\n',
|
|
||||||
'авто-шов: replace, строк 8',
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' [[ $rc == 0 || $rc == 2 ]] || { warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"; return 1; }\n',
|
' [[ $rc == 0 || $rc == 2 ]] || { warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"; return 1; }\n',
|
||||||
@@ -310,6 +317,20 @@ def main():
|
|||||||
' [[ $rc == 0 || $rc == 2 ]] || {\n # Наш шов: лимит Let\'s Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.\n if grep -qE \'rateLimited|too many certificates\' "$log"; then\n local till\n till=$(grep -oE \'retry after [0-9-]+ [0-9:]+ UTC\' "$log" | tail -1 | sed \'s/^retry after //\')\n warn "Лимит Let\'s Encrypt: на имя ${B}$DOMAIN${N} за 7 суток уже выпущено 5 сертификатов${till:+ — повтор возможен после $till}. С вашим сервером всё в порядке: DNS, порты и ufw не при чём. Подождите до этой даты либо возьмите другое имя (например node3.…): лимит считается на точный набор имён."\n else\n warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"\n fi\n return 1\n }\n',
|
' [[ $rc == 0 || $rc == 2 ]] || {\n # Наш шов: лимит Let\'s Encrypt — частая причина, и она не про сервер. Говорим прямо и с датой повтора.\n if grep -qE \'rateLimited|too many certificates\' "$log"; then\n local till\n till=$(grep -oE \'retry after [0-9-]+ [0-9:]+ UTC\' "$log" | tail -1 | sed \'s/^retry after //\')\n warn "Лимит Let\'s Encrypt: на имя ${B}$DOMAIN${N} за 7 суток уже выпущено 5 сертификатов${till:+ — повтор возможен после $till}. С вашим сервером всё в порядке: DNS, порты и ufw не при чём. Подождите до этой даты либо возьмите другое имя (например node3.…): лимит считается на точный набор имён."\n else\n warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"\n fi\n return 1\n }\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'scripts/3x-ui.sh',
|
||||||
|
' local san="IP:$HOST"\n [[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"\n',
|
||||||
|
False,
|
||||||
|
' local san="IP:$HOST"\n [[ $HOST =~ ^[0-9.]+$ ]] || san="DNS:$HOST"\n # Наш шов: если причина — лимит Let\'s Encrypt, говорим это прямо и с датой повтора, как для домена.\n # Иначе человек читает «частые причины» и ищет проблему у хостера, которой нет.\n local till\n till=$(grep -rhoE \'retry after [0-9-]+ [0-9:]+ UTC\' /var/log/3x-ui-install.log "$HOME"/.acme.sh/*/ca 2>/dev/null | tail -1 | sed \'s/^retry after //\')\n if grep -rqsE \'rateLimited|too many certificates\' /var/log/3x-ui-install.log 2>/dev/null; then\n warn "Причина: лимит Let\'s Encrypt — на этот IP за 7 суток уже выпущено 5 сертификатов${till:+; повтор возможен после $till}. Это ограничение Let\'s Encrypt, а не вашего сервера: порт 80 и файрвол тут не при чём."\n else\n',
|
||||||
|
'авто-шов: insert, строк 2',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'scripts/3x-ui.sh',
|
||||||
|
' echo " – сбой у самого Let\'s Encrypt (лог: /var/log/3x-ui-install.log)."\n',
|
||||||
|
False,
|
||||||
|
' echo " – сбой у самого Let\'s Encrypt (лог: /var/log/3x-ui-install.log)."\n } >&2\n fi\n {\n',
|
||||||
|
'авто-шов: insert, строк 1',
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\\n",
|
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\\n",
|
||||||
@@ -328,14 +349,14 @@ def main():
|
|||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n',
|
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n',
|
||||||
False,
|
False,
|
||||||
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n# ---------------------------------------------------------------------------\n# Наш шов: выбор сайта маскировки по результатам скана подсети.\n# Печатает выбранное имя в stdout (пусто — оставляем автоподбор автора).\n# role — подпись для человека: REALITY или XHTTP/MTProto.\nkl_sni_menu() {\n local role=$1; shift\n local -a found=("$@")\n local -a known=("${SNI_CANDIDATES[@]}")\n local src=${KL_ASK_SRC:-/dev/tty} ans i k n auto_idx known_idx own_idx try\n n=${#found[@]}\n printf \'\\n%s%s%s\\n\' "$B" "$role" "$N" >&2\n if ((n)); then\n printf \'Найдено в подсети:\\n\\n\' >&2\n for i in "${!found[@]}"; do printf \' %d - %s\\n\' "$((i + 1))" "${found[i]}" >&2; done\n auto_idx=$((n + 1)); known_idx=$((n + 2)); own_idx=$((n + 3))\n printf \' %d - автовыбор из списка\\n\' "$auto_idx" >&2\n printf \' %d - выбрать общедоступный (например %s, %s)\\n\' "$known_idx" "${known[0]}" "${known[1]}" >&2\n printf \' %d - ввести свой домен\\n\\n\' "$own_idx" >&2\n else\n auto_idx=0; known_idx=1; own_idx=2\n printf \'В подсети доменов не найдено, выберите:\\n\\n\' >&2\n printf \' 1 - общедоступный (например %s, %s)\\n\' "${known[0]}" "${known[1]}" >&2\n printf \' 2 - ввести свой домен\\n\\n\' >&2\n fi\n printf \'Ваш выбор (Enter — автовыбор): \' >&2\n for try in 1 2 3; do\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ -z $ans ]] && return 0\n [[ $ans =~ ^[0-9]+$ ]] || { printf \' введите номер из списка\\n\' >&2; continue; }\n i=$((10#$ans))\n if ((n)) && ((i >= 1 && i <= n)); then printf \'sni:%s\' "${found[i - 1]}"; return 0; fi\n ((i == auto_idx)) && return 0\n if ((i == known_idx)); then\n printf \'\\nОбщедоступные сайты:\\n\' >&2\n for k in "${!known[@]}"; do printf \' %d - %s\\n\' "$((k + 1))" "${known[k]}" >&2; done\n printf \' 0 - назад\\nВаш выбор: \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then\n printf \'sni:%s\' "${known[10#$ans - 1]}"; return 0\n fi\n printf \' ничего не выбрано — оставляю автовыбор\\n\' >&2\n return 0\n fi\n if ((i == own_idx)); then\n printf \' адрес сайта (например vpn.example.com): \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ $ans =~ ^[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ ]] && { printf \'own:%s\' "$ans"; return 0; }\n printf \' не похоже на адрес сайта — оставляю автовыбор\\n\' >&2\n return 0\n fi\n printf \' такого номера нет\\n\' >&2\n done\n return 0\n}\n\n',
|
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n# ---------------------------------------------------------------------------\n# Наш шов: выбор сайта маскировки по результатам скана подсети.\n# Печатает выбранное имя в stdout (пусто — оставляем автоподбор автора).\n# role — подпись для человека: REALITY или XHTTP/MTProto.\nkl_sni_menu() {\n local role=$1; shift\n local -a found=("$@")\n local -a known=("${SNI_CANDIDATES[@]}")\n local src=${KL_ASK_SRC:-/dev/tty} ans i k n auto_idx known_idx own_idx try\n n=${#found[@]}\n printf \'\\n%s%s%s\\n\' "$B" "$role" "$N" >&2\n if ((n)); then\n printf \'Найдено в подсети:\\n\\n\' >&2\n for i in "${!found[@]}"; do printf \' %d - %s\\n\' "$((i + 1))" "${found[i]}" >&2; done\n auto_idx=$((n + 1)); known_idx=$((n + 2)); own_idx=$((n + 3))\n printf \' %d - автовыбор из списка\\n\' "$auto_idx" >&2\n printf \' %d - выбрать общедоступный (например %s, %s)\\n\' "$known_idx" "${known[0]}" "${known[1]}" >&2\n printf \' %d - ввести свой домен\\n\\n\' "$own_idx" >&2\n else\n auto_idx=0; known_idx=1; own_idx=2\n printf \'В подсети доменов не найдено, выберите:\\n\\n\' >&2\n printf \' 1 - общедоступный (например %s, %s)\\n\' "${known[0]}" "${known[1]}" >&2\n printf \' 2 - ввести свой домен\\n\\n\' >&2\n fi\n printf \'Ваш выбор (Enter — автовыбор): \' >&2\n for try in 1 2 3; do\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ -z $ans ]] && return 0\n [[ $ans =~ ^[0-9]+$ ]] || { printf \' введите номер из списка\\n\' >&2; continue; }\n i=$((10#$ans))\n if ((n)) && ((i >= 1 && i <= n)); then printf \'sni:%s\' "${found[i - 1]}"; return 0; fi\n ((i == auto_idx)) && return 0\n if ((i == known_idx)); then\n printf \'\\nОбщедоступные сайты:\\n\' >&2\n for k in "${!known[@]}"; do printf \' %d - %s\\n\' "$((k + 1))" "${known[k]}" >&2; done\n # Наш шов: последним пунктом — автовыбор из этого списка (раньше выбор был только по номеру).\n printf \' %d - автовыбор из списка\\n\' "$((${#known[@]} + 1))" >&2\n printf \' 0 - назад\\nВаш выбор: \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans >= 1 && 10#$ans <= ${#known[@]})); then\n printf \'sni:%s\' "${known[10#$ans - 1]}"; return 0\n fi\n if [[ $ans =~ ^[0-9]+$ ]] && ((10#$ans == ${#known[@]} + 1)); then\n local autopick="${known[RANDOM % ${#known[@]}]}"\n printf \' автовыбор: %s\\n\' "$autopick" >&2\n printf \'sni:%s\' "$autopick"; return 0\n fi\n printf \' ничего не выбрано — оставляю автовыбор\\n\' >&2\n return 0\n fi\n if ((i == own_idx)); then\n printf \' адрес сайта (например vpn.example.com): \' >&2\n ans=""\n read -r -t 300 ans <"$src" 2>/dev/null || true\n ans=${ans//[[:space:]]/}\n [[ $ans =~ ^[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$ ]] && { printf \'own:%s\' "$ans"; return 0; }\n printf \' не похоже на адрес сайта — оставляю автовыбор\\n\' >&2\n return 0\n fi\n printf \' такого номера нет\\n\' >&2\n done\n return 0\n}\n\n',
|
||||||
'авто-шов: insert, строк 3',
|
'авто-шов: insert, строк 3',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && ! -f $XUI_ENV && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
' elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||||||
False,
|
False,
|
||||||
' # Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе\n # человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.\n elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && ! -f $XUI_ENV && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
' # Наш шов: когда выбор сайта делает наше меню (KL_PICK_SNI=1), меню автора не показываем — иначе\n # человек отвечает на «Свой домен / Сосед / Свой сайт», и до скана подсети дело не доходит.\n elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 && ${KL_PICK_SNI:-0} != 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -349,7 +370,7 @@ def main():
|
|||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' if ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n',
|
' if ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n',
|
||||||
False,
|
False,
|
||||||
' # Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.\n if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : </dev/tty; } 2>/dev/null; then\n local pick pick2 own_sni=no\n pick=$(kl_sni_menu "REALITY (TCP 443) — под какой сайт маскировать" ${nb[@]+"${nb[@]}"})\n case $pick in\n own:*)\n own_sni=yes\n DOMAIN=${pick#own:}\n if domain_points_here "$DOMAIN" 2>/dev/null; then\n SNI=$DOMAIN\n ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let\'s Encrypt выпустит установщик)"\n # Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.\n # У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой\n # домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.\n panel_on_choice\n else\n SNI=$DOMAIN; later "домен $DOMAIN не указывает на этот сервер — маскируюсь под него как под чужой сайт, панель и подписка остаются по IP"\n fi\n ;;\n sni:*) SNI=${pick#sni:} ;;\n esac\n if [[ -n $SNI ]]; then\n if [[ $own_sni == no ]] && ! sni_ok "$SNI"; then\n warn "$SNI не отвечает по TLS 1.3 + HTTP/2 — беру найденное в подсети"\n SNI="${nb[0]:-$SNI}"\n fi\n ok "маскировка REALITY: ${B}$SNI${N}"\n fi\n if [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n pick2=$(kl_sni_menu "XHTTP и MTProto (различаются по имени сайта) — какой сайт взять" ${nb[@]+"${nb[@]}"})\n [[ $pick2 == sni:* ]] && pick2=${pick2#sni:}\n [[ $pick2 == own:* ]] && pick2=${pick2#own:}\n [[ -n $pick2 ]] && SNI2=$pick2\n [[ -n $SNI2 ]] && ok "маскировка XHTTP/MTProto: ${B}$SNI2${N}"\n fi\n fi\n if ((${#nb[@]})); then ok "подходящих соседей найдено: ${#nb[@]}"\n else warn "в подсети подходящих сайтов не нашлось (адреса проверены, откликов нет)"; fi\n if [[ -z $SNI ]] && ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${SNI2:-${nb[1]:-}}; SNI3=${nb[2]:-}\n',
|
' # Наш шов: показываем найденное и даём выбрать — отдельно для REALITY и для XHTTP/MTProto.\n if [[ ${KL_PICK_SNI:-} == 1 && $yes == no ]] && { : </dev/tty; } 2>/dev/null; then\n local pick pick2 own_sni=no\n pick=$(kl_sni_menu "REALITY (TCP 443) — под какой сайт маскировать" ${nb[@]+"${nb[@]}"})\n case $pick in\n own:*)\n own_sni=yes\n DOMAIN=${pick#own:}\n # Наш шов: домен вводится заново, пока он не указывает на этот сервер. Раньше при промахе\n # установка молча маскировалась под чужой сайт, а при домене из флага — обрывалась.\n # A-запись у регистратора расходится не мгновенно, поэтому первым пунктом — «проверить снова».\n while :; do\n if domain_points_here "$DOMAIN"; then\n SNI=$DOMAIN\n ok "маскировка под свой домен: ${B}$DOMAIN${N} (сертификат Let\'s Encrypt выпустит установщик)"\n # Наш шов: спрашиваем, где открывать панель и подписку, и при интерактивном вводе домена тоже.\n # У автора этот вопрос задавался только при домене из флага --domain, а при выборе «ввести свой\n # домен» в меню маскировки — нет, и панель молча оставалась по IP с самоподписанным сертификатом.\n panel_on_choice\n break\n fi\n echo " Домен ${B}$DOMAIN${N} пока не указывает на этот сервер." >&2\n echo " ${B}1${N} проверить снова A-запись расходится по DNS от нескольких минут до нескольких часов" >&2\n echo " ${B}2${N} ввести другой домен" >&2\n echo " ${B}3${N} без своего домена: маскировка соседом по подсети" >&2\n ask_tty " Выбор [1]: " || break\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || break\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] && DOMAIN=$d ;;\n 3) own_sni=no; DOMAIN=""; break ;;\n *) sleep 5 ;;\n esac\n done\n ;;\n sni:*) SNI=${pick#sni:} ;;\n esac\n if [[ -n $SNI ]]; then\n if [[ $own_sni == no ]] && ! sni_ok "$SNI"; then\n warn "$SNI не отвечает по TLS 1.3 + HTTP/2 — беру найденное в подсети"\n SNI="${nb[0]:-$SNI}"\n fi\n ok "маскировка REALITY: ${B}$SNI${N}"\n fi\n if [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n pick2=$(kl_sni_menu "XHTTP и MTProto (различаются по имени сайта) — какой сайт взять" ${nb[@]+"${nb[@]}"})\n [[ $pick2 == sni:* ]] && pick2=${pick2#sni:}\n [[ $pick2 == own:* ]] && pick2=${pick2#own:}\n [[ -n $pick2 ]] && SNI2=$pick2\n [[ -n $SNI2 ]] && ok "маскировка XHTTP/MTProto: ${B}$SNI2${N}"\n fi\n fi\n if ((${#nb[@]})); then ok "подходящих соседей найдено: ${#nb[@]}"\n else warn "в подсети подходящих сайтов не нашлось (адреса проверены, откликов нет)"; fi\n if [[ -z $SNI ]] && ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${SNI2:-${nb[1]:-}}; SNI3=${nb[2]:-}\n',
|
||||||
'авто-шов: replace, строк 2',
|
'авто-шов: replace, строк 2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -375,9 +396,9 @@ def main():
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' later "XHTTP и MTProto используют известные сайты из общего списка (достаточно соседей по подсети не нашлось). Заменить: kit net site"\n',
|
' issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя. Исправьте причину и запустите скрипт снова."\n',
|
||||||
False,
|
False,
|
||||||
' # Наш шов: про XHTTP и MTProto говорим только если они выбраны — иначе это шум.\n [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]] && \\\n later "XHTTP и MTProto используют известные сайты из общего списка (достаточно соседей по подсети не нашлось). Заменить: kit net site"\n',
|
' # Наш шов: провал сертификата не обрывает установку. Человек может повторить попытку (DNS доехал,\n # лимит Let\'s Encrypt кончился) или ввести другое имя — установка продолжится с него же.\n while ! issue_domain_cert; do\n echo\n echo " ${B}Что делать дальше?${N}"\n echo " ${B}1${N} повторить попытку если ждали обновления A-записи или лимита Let\'s Encrypt"\n echo " ${B}2${N} ввести другой домен проверим, что он указывает на этот сервер, и повторим"\n echo " ${B}3${N} остановиться поставить без своего домена: перезапустите и выберите соседа по подсети"\n ask_tty " Выбор [1]: " || die "Сертификат для $DOMAIN не выпущен. Подробности: /var/log/kit-domain-cert.log"\n case ${REPLY//[[:space:]]/} in\n 2) ask_tty " адрес сайта (например vpn.example.com): " || continue\n local d=${REPLY//[[:space:]]/}\n [[ -n $d ]] || continue\n if domain_points_here "$d"; then DOMAIN=$d; SNI=$DOMAIN\n else warn "оставляю прежний домен $DOMAIN — новый не указывает на этот сервер"; fi ;;\n 3) # Наш шов: продолжаем без своего домена прямо здесь — маскировка соседом по подсети\n # (или известным сайтом), панель и подписка по IP. Перезапускать установку не нужно.\n local nb2\n nb2=$(nearby_sites 2>/dev/null | shuf | head -1)\n [[ -n $nb2 ]] || nb2=dl.google.com\n DOMAIN=""; PANEL_ON=ip; LINK_HOST=$HOST\n SNI=$nb2; SNI2=${SNI2:-$nb2}\n ok "продолжаю без своего домена: маскировка ${B}$SNI${N}, панель и подписка — по IP"\n break ;;\n *) sleep 5 ;;\n esac\n done\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 1',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -403,17 +424,17 @@ def main():
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' echo "${G}${B}Готово! Сервер работает.${N}"\n [[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let\'s Encrypt продлевается сам."\n [[ $SELF_IP_CERT == yes ]] && later "Сертификат на IP самоподписанный: пользуйтесь ссылками на отдельные подключения, подписка в приложениях может не открыться."\n echo\n echo "${B}ПАНЕЛЬ${N}"\n if [[ $TRUSTED == yes ]]; then\n echo " $panel_url"\n else\n echo " Только через SSH-туннель. На вашем компьютере:"\n echo " ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST"\n echo " Затем в браузере: http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"\n fi\n echo " Логин: ${B}$XUI_USERNAME${N} Пароль: ${B}$XUI_PASSWORD${N}"\n echo " ${D}(сохранено в $RESULT, файл виден только root)${N}"\n echo\n if [[ $TRUSTED == yes ]]; then\n echo "${B}ПОДПИСКА${N} – одна ссылка для всех протоколов"\n echo " ${B}$SUB_URL${N}"\n echo " Вставьте в Happ, Hiddify, Karing, v2rayN, Clash Verge или FlClash."\n qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true\n echo\n KIT_NO_QR=1 /usr/local/bin/kit __links "$NAME" "$SUBID" || true\n else\n /usr/local/bin/kit __links "$NAME" "$SUBID" || true\n fi\n echo\n echo "${D}Порты, выключение и включение протоколов: ${N}${B}kit net${N}"\n',
|
' } >"$RESULT"\n\n echo\n echo "${G}${B}Готово! Сервер работает.${N}"\n [[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let\'s Encrypt продлевается сам."\n [[ $SELF_IP_CERT == yes ]] && later "Сертификат на IP самоподписанный: пользуйтесь ссылками на отдельные подключения, подписка в приложениях может не открыться."\n',
|
||||||
False,
|
False,
|
||||||
' echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."\n',
|
' } >"$RESULT"\n\n echo\n',
|
||||||
'авто-шов: replace, строк 27',
|
'авто-шов: delete, строк 6',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' ((${#WARNINGS[@]} > 3)) && echo " … и ещё $((${#WARNINGS[@]} - 3)): kit check"\n fi\n echo\n echo "${B}ДАЛЬШЕ${N}"\n echo " ${B}kit${N} меню управления"\n echo " ${B}kit user add имя --gb 50 --days 30${N} добавить пользователя"\n echo " ${B}kit check --deep${N} проверить, что всё работает"\n echo " ${B}kit backup${N} копия сервера (держите в надёжном месте)"\n',
|
' echo "${B}ПАНЕЛЬ${N}"\n if [[ $TRUSTED == yes ]]; then\n echo " $panel_url"\n else\n echo " Только через SSH-туннель. На вашем компьютере:"\n echo " ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST"\n echo " Затем в браузере: http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"\n fi\n echo " Логин: ${B}$XUI_USERNAME${N} Пароль: ${B}$XUI_PASSWORD${N}"\n echo " ${D}(сохранено в $RESULT, файл виден только root)${N}"\n echo\n if [[ $TRUSTED == yes ]]; then\n echo "${B}ПОДПИСКА${N} – одна ссылка для всех протоколов"\n echo " ${B}$SUB_URL${N}"\n echo " Вставьте в Happ, Hiddify, Karing, v2rayN, Clash Verge или FlClash."\n qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true\n else\n /usr/local/bin/kit __links "$NAME" "$SUBID" || true\n fi\n local -a tp=(\n "Управление сервером: введите команду ${B}kit${N} – откроется меню. Запоминать команды не нужно."\n "Ссылки на отдельные протоколы (REALITY, Hysteria2, Telegram и другие): kit → Пользователи → Показать ссылки."\n "Добавить пользователя: kit → Пользователи → Добавить."\n "Резервная копия: kit → Резервная копия. Храните её как пароль.")\n tips "Что дальше" "${tp[@]}"\n',
|
||||||
False,
|
False,
|
||||||
' ((${#WARNINGS[@]} > 3)) && echo " … и ещё $((${#WARNINGS[@]} - 3)): kit check"\n fi\n\n',
|
' echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."\n',
|
||||||
'авто-шов: replace, строк 8',
|
'авто-шов: replace, строк 25',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
@@ -473,17 +494,17 @@ def main():
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||||||
False,
|
False,
|
||||||
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
' && die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||||||
False,
|
False,
|
||||||
' && die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
|
||||||
'авто-шов: replace, строк 1',
|
'авто-шов: replace, строк 2',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'scripts/3x-ui.sh',
|
'scripts/3x-ui.sh',
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ for fn in ask_yes ask_ssh_port ssh_ports_now ask_protocols proto_parse proto_men
|
|||||||
[[ -z $c || $c -gt $d ]] || die "scripts/3x-ui-local.sh: $fn вызывается (строка $c) раньше, чем определена ($d)"
|
[[ -z $c || $c -gt $d ]] || die "scripts/3x-ui-local.sh: $fn вызывается (строка $c) раньше, чем определена ($d)"
|
||||||
done
|
done
|
||||||
ok "Все наши функции определены раньше своих вызовов"
|
ok "Все наши функции определены раньше своих вызовов"
|
||||||
|
# python3 есть не везде (на Windows обычно только python) — берём тот, что есть.
|
||||||
|
py=$(command -v python3 || command -v python || true)
|
||||||
|
[[ -n $py ]] || die "нет python — им проверяются меню (tools/check-menus.py)"
|
||||||
|
"$py" tools/check-menus.py >/dev/null || die "подсказка «Выбор [0-N]» не совпадает с числом пунктов меню (tools/check-menus.py)"
|
||||||
|
ok "Подсказки меню совпадают с числом пунктов"
|
||||||
|
|
||||||
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
|
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user