Compare commits

...
57 Commits
Author SHA1 Message Date
hacker07 0d2af45bdb подпись релиза 1.2.1.5
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 17:34:04 +03:00
hacker07 a02650311e v1.2.1.5: пункт меню «Доступ к панели, ссылки и заглушка» + kit local access 2026-10-14 17:31:58 +03:00
hacker07 2b57e6f6d7 подпись релиза 1.2.1.4
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 17:16:10 +03:00
hacker07 df8a99bb78 v1.2.1.4: короткий итог установки, автовыбор в общедоступных сайтах, kit local ports, без дампа ссылок 2026-10-14 17:06:23 +03:00
hacker07 47328b33b3 подпись релиза 1.2.1.3
Синхронизация с автором / sync (push) Failing after 2s
2026-10-14 16:51:06 +03:00
hacker07 776b7a8c5e v1.2.1.3: ссылки и QR — в меню kit (и kit local links), в итоге видно сертификат панели 2026-10-14 16:49:45 +03:00
hacker07 2d63136853 подпись релиза 1.2.1.2
Синхронизация с автором / sync (push) Failing after 2s
2026-10-14 16:32:58 +03:00
hacker07 1b85a4bb23 v1.2.1.2: отказ сертификата на IP — с датой повтора; «без домена» продолжает установку вместо остановки 2026-10-14 16:31:54 +03:00
hacker07 b80c44077c подпись релиза 1.2.1.1 (база автора 3X-UI KIT 1.2.1)
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 16:19:59 +03:00
hacker07 f894b48cd2 сборка на базе автора 1.2.1: версия 1.2.1.1, швы пересобраны, удаление через kit uninstall, номера протоколов через пробел 2026-10-14 16:08:14 +03:00
hacker07 bddef5b8d3 v1.2.27: свой домен вводится заново при промахе; провал сертификата не обрывает установку 2026-10-14 15:51:03 +03:00
hacker07 53cb694206 подпись релиза 1.2.26
Синхронизация с автором / sync (push) Failing after 1s
2026-10-14 14:51:24 +03:00
hacker07 5c22cbf4fe v1.2.26: лимит Let-Encrypt называется прямо; готовый сертификат переиспользуется вместо нового заказа 2026-10-14 14:44:47 +03:00
hacker07 a7b4586bc3 подпись релиза 1.2.25
Синхронизация с автором / sync (push) Failing after 3s
2026-10-14 14:38:07 +03:00
hacker07 cec5246e47 release.sh: понятная подсказка при неверной парольной фразе ключа 2026-10-14 14:36:44 +03:00
hacker07 151d510495 v1.2.25: вопрос про панель (IP/домен) в интерактивном выборе домена; понятнее блок про мягкую защиту 2026-10-14 14:34:27 +03:00
hacker07 f9940026dc подпись релиза 1.2.24
Синхронизация с автором / sync (push) Successful in 1s
2026-10-14 14:08:14 +03:00
hacker07 6d7ebf234e FORK.md: как ведёт себя release.sh — проверки, проба GitHub, парольная фраза ключа 2026-10-14 13:59:31 +03:00
hacker07 41092d4cd1 release.sh: проба связи с GitHub (нет связи — проверки пропускаются сразу, без многосекундных зависаний), warn объявлен 2026-10-14 13:58:11 +03:00
hacker07 78cf3c05dc release.sh: исправлен continue вне цикла (обрывал прогон), connect-timeout 30с и 5 повторов на медленной сети 2026-10-14 13:46:19 +03:00
hacker07 83d9b11c5c release.sh: недоступный GitHub не блокирует подпись (предупреждение вместо стопа), реальное расхождение хешей по-прежнему стоп 2026-10-14 13:40:34 +03:00
hacker07 1fbf27e07c release.sh: повторы и понятная ошибка при сетевых сбоях до GitHub 2026-10-14 13:39:23 +03:00
hacker07 9571e93d46 v1.2.24: свой домен → панель и подписка по домену; предупреждение при панели по IP 2026-10-14 13:36:24 +03:00
hacker07 9dfb4e7957 швы: якоря теперь уникальны и в дереве (чистая проверка) 2026-10-14 13:27:36 +03:00
hacker07 51c35dd2c1 v1.2.23: убран дубль доступов, заглушка сразу наша, ссылки только для нескольких протоколов 2026-10-14 13:26:48 +03:00
hacker07 3f3db5b04c Релиз v1.2.22: подпись
Синхронизация с автором / sync (push) Successful in 3s
2026-10-14 12:51:11 +03:00
hacker07 8fd5572450 v1.2.22: 80 в ufw открывается перед выпуском сертификата; точная причина сбоя 2026-10-14 12:24:03 +03:00
hacker07 01ab468e24 Релиз v1.2.21: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-14 00:18:01 +03:00
hacker07 b20fd6af77 v1.2.21: проверка правил 80/443 в ufw после установки 2026-10-14 00:14:32 +03:00
hacker07 bc18fd354e v1.2.21: порты 80/443 и понятная ошибка сертификата; блок про SSH-порт в конце 2026-10-14 00:14:05 +03:00
hacker07 25e25232b2 Релиз v1.2.20: подпись
Синхронизация с автором / sync (push) Successful in 2s
2026-10-14 00:03:57 +03:00
hacker07 6a9a6ed07e v1.2.20: после сноса меню не показывается; update_hint терпит отсутствие /etc/kit 2026-10-14 00:02:26 +03:00
hacker07 d127a92785 v1.2.19: extra объявлена пустой (set -u валил установку) + правило в FORK.md 2026-10-13 23:59:29 +03:00
hacker07 f38aeefcbc Релиз v1.2.18: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-13 23:53:22 +03:00
hacker07 d0bfb2dc38 v1.2.18: свой домен из меню не подменяется запасным сайтом (не перезаписывать выбор, не проверять TLS своего домена) 2026-10-13 23:52:02 +03:00
hacker07 a0df115616 Релиз v1.2.17: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-13 23:45:13 +03:00
hacker07 f4cf157293 release.sh: гейт «определение раньше вызова» больше не обрывает релиз (set -e + пустой grep) 2026-10-13 23:44:16 +03:00
hacker07 73754dbc01 v1.2.17: меню маскировки первым (меню автора не перебивает) + исправление ask_yes 2026-10-13 23:39:50 +03:00
hacker07 c261d27b80 v1.2.16: возвращён ask_yes; гейт на «определение раньше вызова» 2026-10-13 23:34:02 +03:00
hacker07 fa9e7225ea Релиз v1.2.15: подпись
Синхронизация с автором / sync (push) Successful in 2s
2026-10-13 23:22:16 +03:00
hacker07 4a9e452e3e v1.2.15: меню 9/10/11, снос с добивкой, меню маскировки как просил, быстрый скан, швы пересобраны (63 шва, воспроизведение побайтово) 2026-10-13 23:20:18 +03:00
hacker07 d034e207da docs: README под новые возможности; журнал версий в порядке убывания
Синхронизация с автором / sync (push) Successful in 1s
2026-10-13 22:59:43 +03:00
hacker07 22b9925b0c Релиз v1.2.14: подпись
Синхронизация с автором / sync (push) Successful in 2s
2026-10-13 22:57:13 +03:00
hacker07 9aa93fad37 v1.2.14: меню выбора сайта маскировки по скану + снос не обрывается на set -e 2026-10-13 22:52:33 +03:00
hacker07 5558314849 Релиз v1.2.13: подпись
Синхронизация с автором / sync (push) Successful in 3s
2026-10-13 22:40:27 +03:00
hacker07 b778ce4428 v1.2.13: вернул заголовок раздела действий 2026-10-13 22:39:04 +03:00
hacker07 143b41c84e v1.2.13: вопросы до установки (доступы в самом конце) + гейты по выбранным протоколам 2026-10-13 22:38:23 +03:00
hacker07 01662ff039 v1.2.12: не сканировать соседей по подсети, если XHTTP и MTProto не выбраны 2026-10-13 22:33:23 +03:00
hacker07 04a69f3ae3 Релиз v1.2.11: подпись
Синхронизация с автором / sync (push) Successful in 2s
2026-10-12 22:57:16 +03:00
hacker07 0daaf65b5c v1.2.11: снос убирает kit-limit и находит свои службы по шаблонам 2026-10-12 22:55:04 +03:00
hacker07 64796e0f53 Релиз v1.2.10: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 22:47:40 +03:00
hacker07 b7834d5f5c FORK.md: правило — main двигается только с подписанным релизом 2026-10-12 22:41:02 +03:00
hacker07 77f98da99a v1.2.10: убран мусор в выводе установщика + гейт на строки-сироты 2026-10-12 22:40:05 +03:00
hacker07 6bb4f55f33 Релиз v1.2.9: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 22:38:17 +03:00
hacker07 6fb7d6c59c v1.2.9: снос убирает всё (список по коду установщика) + проверка после сноса + флаг --clean у установщика 2026-10-12 22:35:57 +03:00
hacker07 ad7b66dbf8 Релиз v1.2.8: подпись
Синхронизация с автором / sync (push) Successful in 1s
2026-10-12 22:28:03 +03:00
hacker07 6323c28533 v1.2.8: доступ к панели в конце установки; kit local uninstall + пункт 10 в меню (SSH не трогает) 2026-10-12 22:26:47 +03:00
14 changed files with 2808 additions and 525 deletions
+308 -10
View File
@@ -1,15 +1,311 @@
## 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 виден прямо, готовый сертификат переиспользуется
* **Причина отказа теперь называется честно.** Let's Encrypt разрешает 5 сертификатов на точный набор имён
за 7 суток; при повторных установках лимит выбирается, и acme.sh отвечает `rateLimited`, а установщик
писал про «A-запись, порт 80 или Cloudflare» — то есть отправлял искать причину не там. Теперь в выводе
дата, после которой повтор сработает, и подсказка взять другое имя (`node3.…` — лимит считается на набор имён).
* **Готовый сертификат переиспользуется.** Если у acme.sh уже лежит годный сертификат на этот домен,
новый не заказывается — лишний заказ и упирался в лимит. Заодно не тратится время на повторные попытки.
* Итоговая ошибка установщика больше не повторяет догадки про порт и DNS: причина печатается выше,
а лог остаётся в /var/log/kit-domain-cert.log.
## v1.2.25: вопрос про панель возвращён, блок про защиту переписан понятнее
* **Снова спрашиваем, где открывать панель и подписку.** У автора этот вопрос задавался только когда
домен передан флагом `--domain`; при выборе «ввести свой домен» в меню маскировки его не было, и панель
молча оставалась по IP с самоподписанным сертификатом — то самое «панель на это ругается». Теперь:
выбрали свой домен → сразу вопрос «По IP (как раньше) или По домену» (`https://домен/…`, сертификат
Let's Encrypt, браузер не ругается).
* В отчёте защиты блок «чего здесь нет» больше не ссылается на сторонний проект без пояснений: сказано,
что именно сделано мягче (без полного обновления системы, без автоперезагрузки, без общесистемного
запрета ICMP) и почему — панель и ядро закреплённых версий, сервер не должен вставать сам.
## v1.2.24: сертификат идёт вслед за выбором «домен или IP»
* Выбрали в меню маскировки **свой домен** — панель и подписка тоже переезжают на домен:
`https://ваш.домен/...` и открываются тем же сертификатом Let's Encrypt, без предупреждений браузера.
Раньше сертификат выписывался только для маскировки, а панель оставалась по IP с самоподписанным —
отсюда ругань и в панели, и в подписке.
* Если выбрали соседа или общедоступный сайт (своего домена нет) — панель остаётся по IP, и в итоговом
блоке прямо сказано: сертификат самоподписанный, браузер предупредит; перейти на домен потом —
`kit net panel domain`.
* Домен, который не указывает на этот сервер, используется только как маска (и об этом сообщается),
панель в этом случае остаётся по IP.
## v1.2.23: чистый вывод — без дублей доступов, без авторской заглушки, без лишних ссылок
* **Убран дублирующий блок автора.** Он печатал «Готово! Сервер работает» с адресом панели, логином,
паролем, подпиской, QR-кодом и ссылками — а потом то же самое выводил наш финальный блок.
Теперь авторский блок заканчивается строкой «Установка завершена» и списком предупреждений;
доступы и подписка печатаются один раз, в самом конце.
* **Авторская заглушка больше не ставится.** Наш сайт кладётся до установщика, а его страница из
релиза убирается — иначе она перезаписывала нашу на шаге сборки сайта. Заодно пропадает лишний
шаг «поставить поверх» и его бэкап.
* **Ссылки по протоколам — только если протоколов больше одного.** При одном REALITY печатается
подписка и QR-код, без списка «отдельные ссылки на каждый протокол».
* Самопроверка в конце установки больше не перечисляет чужие протоколы («REALITY, XHTTP и
Hysteria2») — пишет те, что реально созданы. Предупреждение про XHTTP/MTProto показывается только
если эти протоколы выбраны.
## v1.2.22: 80 в ufw открывается прямо перед выпуском сертификата
* Настоящая причина сбоя сертификата (проверено на живом сервере): установщик открывает список
портов в ufw **позже**, чем пробует выпустить сертификат. На свежем сервере ufw в этот момент
разрешает только 22, поэтому проверка Let's Encrypt доходила до сервера и «проглатывалась» —
в логе acme `Timeout during connect (likely firewall problem)`. Провайдер тут ни при чём:
с настоящим слушателем 80 и 443 отвечают снаружи нормально (проверено в том числе с другого сервера).
* Теперь 80/tcp открывается в ufw непосредственно перед `issue_domain_cert`, а формулировка ошибки
в установщике исправлена: сначала проверяем ufw, потом хостера.
## v1.2.21: порты до выпуска сертификата и понятный блок про SSH-порт
* **Порты до сертификата.** Установщик теперь сам открывает 80 и 443 в ufw перед работой и прямо
предупреждает, что такие же правила нужны в файрволе хостинга: частая причина срыва — порт 80
закрыт именно у провайдера, а не в сервере. Если сертификат всё же не выпустился, в выводе
разбирается причина: при `Timeout during connect (likely firewall problem)` пишется, что делать
(открыть 80 в панели хостинга) и предлагается вариант без своего домена (тогда сертификат домена
не нужен, панель остаётся на самоподписанном). После установки 80 и 443 в ufw проверяются ещё раз.
* **Смена порта SSH — с понятным завершением.** Прежние порты запоминаются до смены, и в конце
печатается блок: новый порт, какие прежние ещё открыты, команда проверки входа во втором окне и
готовая команда `kit local ssh --close-old <новый>` для закрытия прежних. Строка про SSH-порт
добавлена и в итоговый блок с доступом к панели и подпиской.
## v1.2.20: после сноса меню не показывается, и нет ошибок про latest-version
* Удаление из меню (`kit`, пункт 9) теперь завершается сразу: печатается «Готово: сервер в состоянии
до установки» — и выход. Раньше меню перерисовывалось на уже удалённом сервере и сыпало
`touch: cannot touch '/etc/kit/latest-version'`.
* `update_hint` больше не пытается что-либо писать, если каталога `/etc/kit` уже нет.
* Входит исправление 1.2.19: `extra` объявлена пустой (иначе `set -u` валил установку на шаге
«[6/6] Проверка» сообщением `extra: unbound variable`).
## v1.2.19: исправлено «extra: unbound variable» при установке
* На середине установки (после проверки протоколов) скрипт падал: `extra: unbound variable`.
Причина — моя правка v1.2.13: присваивание ссылок legacy-подписок завёрнуто в `if`, а переменная
объявлялась как `local extra` без значения. Авторский скрипт работает с `set -u`, поэтому при
невыполненном условии (AWG и MTProto не выбраны) чтение `$extra` валило установку. Объявлена пустой.
* Правило записано в FORK.md: оборачивая авторское присваивание в `if`, объявляй переменную пустой —
`local extra=""`, иначе `set -u` обрывает установку на середине.
## v1.2.18: выбранный «свой домен» больше не подменяется соседом
* Что было: после выбора «ввести свой домен» установщик всё равно брал запасной сайт из подсети.
Две причины, обе в моём коде: (1) выбор перезаписывался сырым значением меню (`own:домен`)
вместо самого домена; (2) домен проверялся на «TLS 1.3 + HTTP/2» — а до установки на нём ещё нет
сертификата, поэтому проверка честно не проходила и срабатывал откат на соседа. Теперь для своего
домена проверка TLS не делается (он указывает на этот сервер — установщик сам выпустит
сертификат Let's Encrypt), а автоподбор и запасной сайт включаются только если вы НЕ выбрали
ничего (автовыбор).
## v1.2.17: меню маскировки идёт ПЕРВЫМ (меню автора больше не перебивает)
* Причина, по которой меню не было видно: **меню спрашивал сам установщик** («Свой домен / Сосед /
Свой сайт») — и до нашего скана подсети дело не доходило. Теперь, когда включено наше меню,
меню автора не показывается вообще: сразу скан, список найденных доменов и выбор — из найденных,
автовыбор, общедоступный или свой домен. Отключить и вернуть поведение автора: `--auto-sni`.
* Ответ меню помечается: если вы ввели домен, который указывает на этот сервер, маскировка идёт
«под свой домен» (сертификат Let's Encrypt выпустит установщик); если не указывает — маскируюсь
под него как под чужой сайт и прямо об этом сообщаю.
* Входит исправление 1.2.16: возвращён `ask_yes` (в 1.2.13–1.2.15 при установке печаталось
`ask_yes: command not found`, и значения брались по умолчанию) + гейт «определение раньше вызова».
## v1.2.16: возвращён ask_yes (вылезало «command not found» при установке)
* При переносе вопросов наверх (v1.2.13) потерялось определение функции `ask_yes` — установщик
писал `ask_yes: command not found` на двух строках и молча брал значения по умолчанию:
ping оставался открытым, автообновления выключались. Скрипт при этом не падал, поэтому
ошибку было видно, но установку она не срывала.
* Почему не поймали: гейт в релизе прогонял только `--dry-run`, а он выходит ДО вопросов.
Добавлена статическая проверка: каждая наша функция должна быть определена раньше первого
вызова (`ask_yes`, `ask_ssh_port`, `ask_protocols`, `interactive`, `clean_previous` и др.).
## v1.2.15: меню выбора сайта в вашем виде, быстрый скан, снос с добивкой
* Меню маскировки выглядит так, как и просили. Если соседи нашлись — их список по номерам,
затем `автовыбор из списка`, `выбрать общедоступный` (отдельным списком) и `ввести свой домен`.
Если в подсети ничего не нашлось — сразу «общедоступный» или «свой домен» (ничего лишнего).
Enter — автовыбор. Общедоступные сайты предлагаются и для REALITY, и для XHTTP/MTProto.
* **Скан подсети стал быстрым.** Было: адреса проверялись по 8 сразу, а найденные имена —
по очереди (до 40 имён × три проверки с сетевыми запросами ≈ минуты). Стало: 32 адреса сразу
и имена проверяются параллельно по 8; таймаут проверки адреса 3 с вместо 4.
* В выводе видно, сколько соседей нашлось (раньше было не понять, работает ли скан).
* После нашего меню автор больше не спрашивает «продолжить с запасным сайтом?» — выбор уже сделан.
* В меню `kit`: **9 — «Удалить всё установленное», 10 — «Выход»** (меню панели `x-ui` переехало
в пункт 11).
* **`kit local uninstall` доводит дело до конца:** после явного списка идёт добивка по шаблонам
(`/etc/kit*`, `/usr/local/lib/kit*`, юниты `kit-*`/`x-ui*`, `/var/www/kit`, сертификаты), повторная
проверка и внятный итог. Появился флаг `--yes` — снести без вопроса.
## v1.2.14: выбор сайта маскировки по результатам скана + снос, который не обрывается
* **Появилось меню маскировки.** Сначала один скан подсети (~250 обращений к 443 соседних адресов),
потом выбор: номер из найденного, `0` — ввести адрес вручную, Enter — автоподбор. Отдельно для
**REALITY** (найденное или свой) и отдельно для **XHTTP/MTProto** (найденное, свой или известный
сайт — список известных расширен Microsoft, AMD, Intel, NVIDIA). Если XHTTP/MTProto не выбраны,
второго меню не будет. Отключить меню: `--auto-sni`.
* **Исправлено: `kit local uninstall` обрывался на середине.** `kit` запускает наши команды
с `set -e`, и подсчёт чужих сайтов nginx (`find | grep | wc -l`, где grep не находит ничего и
возвращает 1) убивал скрипт сразу после остановки служб — файлы и `/usr/local/bin/kit` оставались,
поэтому `kit help` продолжал работать. Теперь снос выполняется с отключённым `set -e` и все
подобные конвейеры защищены; проверка после сноса это подтверждает списком, если что-то осталось.
## v1.2.13: вопросов после показа доступов нет, и никакой работы для невыбранных протоколов
* **Все вопросы — до установщика.** Раньше после установки (когда на экране уже были данные входа
в панель) скрипт спрашивал про ping, автообновления, порт SSH и пароль root — доступы терялись
в потоке вопросов. Теперь спрашиваем всё заранее, а действия выполняем после установки; последнее,
что видит человек, — доступ к панели, подписка и QR-код.
* Автор тянет подписки «-awg» и «-tg» (наследие установок kit < 1.1) всегда — теперь только если
AmneziaWG или MTProto выбраны.
* Наши команды помнят выбор: установщик пишет список протоколов в `/etc/kit/kl-protos`, и
`kit local site` не синхронизирует ответ Hysteria2, если Hysteria2 не ставился (раньше он это делал
и выдавал предупреждение про отсутствующий Hysteria2).
## v1.2.12: не сканируем соседей, когда они не нужны
* Автор подбирал сайты-соседи для XHTTP и MTProto **всегда**, когда маскировка доверенная («панель по IP»)
и всё висит на 443 — независимо от того, выбраны ли эти протоколы. При установке одного REALITY это
давало ~250 обращений к чужим адресам впустую. Теперь скан идёт только если XHTTP или MTProto выбраны;
иначе имена SNI2/SNI3 берутся из общего списка сайтов автора (как и раньше при неудаче скана).
# Что нового в сборке форка # Что нового в сборке форка
Здесь записи о наших версиях. Файл автора (`CHANGELOG.md`) мы не трогаем — так обновления Здесь записи о наших версиях. Файл автора (`CHANGELOG.md`) мы не трогаем — так обновления
автора не конфликтуют с нашими записями. автора не конфликтуют с нашими записями.
## v1.2.11: снос знает про kit-limit и находит свои службы сам
## v1.2.6: вопросы при установке * В списке сноса не было `kit-limit.service` и `kit-limit.timer` (юнит «общий лимит трафика» из kit) —
его создаёт установка, а `kit local uninstall` его не убирал. Из-за этого после сноса оставался
работающий таймер, который раз в 5 минут дёргал уже удалённый `kit` и падал.
* Теперь снос берёт список служб **и из явного перечня, и по шаблонам `x-ui*`/`kit*`** из systemd —
новая служба, добавленная автором позже, не останется незамеченной. Проверка после сноса ищет
по тем же шаблонам и показывает всё найденное.
* Флаг `--clean` у установщика использует тот же список.
## v1.2.10: убран мусор в выводе установщика
* **README.md переписан целиком:** убрано авторское описание с его командой установки, добавлены * В шапке `scripts/3x-ui-local.sh` один из флагов потерял `#` и **при каждом запуске печаталось
актуальная команда, флаги, таблица протоколов, список сайтов-заглушек, команды `kit` и `kit local`, `--dry-run: command not found`** (до `set -e` — потому и не роняло установку, но выглядело как ошибка).
канал обновлений и оговорки. Две картинки — наши (`manuals/assets/fork-banner.svg` и Исправлено, заодно выровнены строки справки.
`manuals/assets/script-3x-ui-local.svg`, терминал собран из настоящего вывода скрипта). * В релизный гейт добавлена проверка, которая такое ловит: холостой прогон установщика, чей вывод в stderr
* Шов в README больше не нужен (`tools/fork_apply.py`): файл наш целиком, как `kit-local.sh`. содержит «command not found / syntax error / bad substitution», релиз не собирается.
## v1.2.9: снос действительно убирает всё, повторная установка после сноса
* Список удаляемого в `kit local uninstall` собран заново **по коду установщика** и дополнен тем, чего
не хватало: `/usr/local/lib/kit-sub`, `/root/kit-backup-*` и `/root/x-ui-before-*` (копии kit),
`/var/log/kit-install-check.log`, `/usr/bin/x-ui.new`, строка `include kit-stream.conf` в `nginx.conf`,
симлинки в `sites-enabled`/`streams-enabled`, файл автообновлений безопасности, пользователь `kit-sub`.
Из-за пропущенных файлов установщик после сноса отказывался ставить заново («3X-UI уже установлена»).
* После сноса печатается проверка **теми же условиями, что у установщика**, и список того, что осталось
на диске — вместо бодрого «удалено» вслепую.
* Флаг `--reset-ssh`: снести и наш ssh-дроп-ин (порт возвращается к системному). По умолчанию SSH не трогаем.
* У установщика появился флаг **`--clean`**: убрать прошлую установку и поставить заново. Если установщик
всё-таки упал на «уже установлена», в подсказке прямо сказано запустить с `--clean`.
## v1.2.8: доступ к панели в конце установки и снос сервера из меню
* В конце установки (перед подпиской) печатается **доступ к панели 3X-UI**: адрес, логин и пароль из
файла `/root/3x-ui.txt` — раньше это было видно только в общем потоке вывода установщика.
* **`kit local uninstall`** — снос установки целиком: панель и служба `x-ui`, ядро Xray, службы и файлы
kit (`kit-sub`, автообновление, cron), конфиги nginx и сайт-заглушка, сертификаты (acme.sh),
наши правки системы (правило ICMP, обновления безопасности), наши бэкапы и файл с данными панели.
Правила ufw для 80/443 снимаются, ответ на ping возвращается. Спрашивает подтверждение словом
«удалить», есть `--dry-run`.
**SSH не трогается** — ни порт, ни конфиги, ни правила для него: иначе легко потерять доступ к серверу.
В главном меню `kit` появился пункт **10. Удалить всё установленное**.
## v1.2.7: имя 3X-UI_autoVPS, справка kit help, подписка с QR, живые кнопки на заглушках ## v1.2.7: имя 3X-UI_autoVPS, справка kit help, подписка с QR, живые кнопки на заглушках
* **Сборка называется `3X-UI_autoVPS`** (было `3X-UI KIT`): баннер сервера — `Hacker 07` в том же * **Сборка называется `3X-UI_autoVPS`** (было `3X-UI KIT`): баннер сервера — `Hacker 07` в том же
@@ -57,7 +353,13 @@
* Флаг `--harden` теперь действительно существует (раньше он уходил установщику автора неизвестным * Флаг `--harden` теперь действительно существует (раньше он уходил установщику автора неизвестным
параметром и ронял установку): закрыть ping и включить автообновления без вопросов. параметром и ронял установку): закрыть ping и включить автообновления без вопросов.
* `--dry-run` показывает, что именно будет спрошено или сделано по умолчанию. * `--dry-run` показывает, что именно будет спрошено или сделано по умолчанию.
## v1.2.6: вопросы при установке
* **README.md переписан целиком:** убрано авторское описание с его командой установки, добавлены
актуальная команда, флаги, таблица протоколов, список сайтов-заглушек, команды `kit` и `kit local`,
канал обновлений и оговорки. Две картинки — наши (`manuals/assets/fork-banner.svg` и
`manuals/assets/script-3x-ui-local.svg`, терминал собран из настоящего вывода скрипта).
* Шов в README больше не нужен (`tools/fork_apply.py`): файл наш целиком, как `kit-local.sh`.
## v1.2.5: суммы релиза в формате, который понимает kit ## v1.2.5: суммы релиза в формате, который понимает kit
* Исправлена ошибка нашего релизного конвейера: на Windows `sha256sum` пишет суммы как * Исправлена ошибка нашего релизного конвейера: на Windows `sha256sum` пишет суммы как
@@ -68,14 +370,12 @@
другой машине. другой машине.
* Проверка теперь повторяет логику самого `kit`: подпись `SHA256SUMS` + строгая сверка имени * Проверка теперь повторяет логику самого `kit`: подпись `SHA256SUMS` + строгая сверка имени
каждого файла из подписанного списка (а не только `sha256sum -c`, который звёздочку прощает). каждого файла из подписанного списка (а не только `sha256sum -c`, который звёздочку прощает).
## v1.2.4: проверка релиза отдельной командой ## v1.2.4: проверка релиза отдельной командой
* Флаг `--verify-only`: скачать релиз, проверить подпись `SHA256SUMS` и SHA256 установщика и выйти — * Флаг `--verify-only`: скачать релиз, проверить подпись `SHA256SUMS` и SHA256 установщика и выйти —
можно проверить канал на сервере, ничего не устанавливая. можно проверить канал на сервере, ничего не устанавливая.
* Порядок шагов изменён: проверка подписи теперь идёт до предполётных проверок и до любых * Порядок шагов изменён: проверка подписи теперь идёт до предполётных проверок и до любых
изменений на сервере, чтобы чужой код не запускался раньше проверки. изменений на сервере, чтобы чужой код не запускался раньше проверки.
## v1.2.3: установка одной командой (`scripts/3x-ui-local.sh`) ## v1.2.3: установка одной командой (`scripts/3x-ui-local.sh`)
* Один скрипт на чистом VPS: проверка сервера → скачивание нашего релиза → проверка подписи * Один скрипт на чистом VPS: проверка сервера → скачивание нашего релиза → проверка подписи
@@ -90,7 +390,6 @@
передаются как есть; есть `--dry-run` (план без изменений) и `--help`. передаются как есть; есть `--dry-run` (план без изменений) и `--help`.
* Публичный ключ форка вшит в обёртку — подпись проверяется до запуска чужого кода; при смене * Публичный ключ форка вшит в обёртку — подпись проверяется до запуска чужого кода; при смене
ключа обновляется и здесь, и в `kit`. ключа обновляется и здесь, и в `kit`.
## v1.2.2: команды защиты сервера (поведение из StressOzz/vps-setup) ## v1.2.2: команды защиты сервера (поведение из StressOzz/vps-setup)
* Новые команды `kit local`: `ssh <порт>` (добавить порт SSH), `ssh --close-old` (закрыть прежние * Новые команды `kit local`: `ssh <порт>` (добавить порт SSH), `ssh --close-old` (закрыть прежние
@@ -107,7 +406,6 @@
а закрытие старых портов не делается автоматически — там ошибка запирает доступ к серверу. а закрытие старых портов не делается автоматически — там ошибка запирает доступ к серверу.
* Ping отключается правилом ufw (`before.rules`), а не общесистемным `icmp_echo_ignore_all`: * Ping отключается правилом ufw (`before.rules`), а не общесистемным `icmp_echo_ignore_all`:
видно в `ufw status`, отменяется одной командой и не глушит ICMP для внутренних интерфейсов. видно в `ufw status`, отменяется одной командой и не глушит ICMP для внутренних интерфейсов.
## v1.2.1: сборка форка поверх 3X-UI_autoVPS 1.2 ## v1.2.1: сборка форка поверх 3X-UI_autoVPS 1.2
* Источник файлов и обновлений — свой репозиторий Gitea: установка и `kit update` берут релиз * Источник файлов и обновлений — свой репозиторий Gitea: установка и `kit update` берут релиз
+41 -1
View File
@@ -124,7 +124,31 @@ Hysteria2 (UDP 443) работают как работали; исходящий
если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает если автор изменил нужную строку — инструмент останавливается и говорит об этом, а не делает
полу-патч. полу-патч.
## Ветки и релизы ## Ветки
`tools/release.sh` — **наш файл** (как README): авторский скрипт подписи мы переписали под себя
(проверка LF, гейт справки, шапка `SHA256SUMS` в двух строках, свой список файлов). Швов к нему нет;
при обновлении из авторского проекта файл конфликтует и решается в нашу пользу (как README).
Важно при правках швов: авторский скрипт работает с `set -u`, поэтому если оборачивать его присваивание
в `if`, переменную нужно объявить пустой (`local extra=""`, а не `local extra`) — иначе при невыполненном
условии любое чтение `$extra` валит установку на середине с «unbound variable».
Проверка корректности швов (делать после каждой правки и после синхронизации с автором):
накатать `tools/fork_apply.py` на чистую копию авторского кода и сверить с нашим деревом — расхождений
быть не должно, кроме наших собственных файлов. Это единственный способ увидеть правки, которых нет в швах.
**`main` двигается только вместе с подписанным релизом.** Серверы читают версию из `main` (`KIT_RAW`),
поэтому в `main` не должно попадать то, что ещё не подписано: иначе `kit update` и однострочник
установки упираются в отсутствующий тег или в неподписанные суммы. Порядок такой:
1. поднять версию и собрать изменения в `dev`;
2. пользователь подписывает (`tools/release.sh`);
3. коммит с `SHA256SUMS` и `SHA256SUMS.sig` → тег `v<версия>` → и только теперь `dev` вливается в `main`.
Проверка перед публикацией в `main`: `curl -fsSL .../raw/tag/v<версия>/VERSION` должен отдавать ту же
версию, а `bash <(curl ... raw/branch/main/scripts/3x-ui-local.sh) --verify-only` — писать, что подпись сошлась.
и релизы
- `upstream` — точная копия ветки автора (`main`), только вперёд. - `upstream` — точная копия ветки автора (`main`), только вперёд.
- `dev` — наша сборка: `upstream` + швы + наши файлы; из неё делаются релизы. - `dev` — наша сборка: `upstream` + швы + наши файлы; из неё делаются релизы.
@@ -166,6 +190,22 @@ 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 + подпись нашим ключом
**Нумерация версий.** Основа — версия автора, наша надстройка добавляется через точку с единицы:
у автора 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, проверяет справку и то, что установщик не ругается на пустые
строки, сверяет панель и ядро Xray с официальными хешами GitHub (если GitHub недоступен — предупреждает и
пропускает эти проверки за секунды, а не висит; расхождение хешей при живом GitHub по-прежнему останавливает
выпуск). Далее скрипт ждёт **парольную фразу ключа** — её вводит человек, поэтому запускать надо в окне, где
можно набрать пароль (в PowerShell — да, в фоне — нет).
git add -A && git commit -m "Релиз v$(cat VERSION): …" && git tag v$(cat VERSION) git add -A && git commit -m "Релиз v$(cat VERSION): …" && git tag v$(cat VERSION)
git push origin dev v$(cat VERSION) upstream git push origin dev v$(cat VERSION) upstream
``` ```
+33 -12
View File
@@ -28,9 +28,15 @@
bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh) bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui-local.sh)
``` ```
Без флагов скрипт **спрашивает** всё по ходу: какие протоколы ставить (Enter — REALITY + AmneziaWG-3.1), Без флагов скрипт **спрашивает всё заранее, до установки**: какие протоколы ставить (Enter — REALITY +
какую маскировку и домен, закрывать ли ответ на ping, включать ли автообновления безопасности, AmneziaWG-3.1), менять ли порт SSH (Enter — оставить 22), менять ли пароль root, закрывать ли ответ на
менять ли порт SSH (Enter — оставить 22), менять ли пароль root. ping, включать ли автообновления безопасности. Затем сканирует подсеть и **предлагает выбрать сайт
маскировки** — отдельно для REALITY и, если выбраны XHTTP/MTProto, отдельно для них: номер из найденных
соседей, `0` — ввести адрес вручную, для XHTTP/MTProto ещё и известный сайт (Google, Microsoft, AMD…).
После установщика скрипт **ничего не спрашивает** — только выполняет выбранное, проверяет сервер и в
самом конце показывает то, что нужно унести: **доступ к панели 3X-UI** (адрес, логин, пароль) и
**подписку с QR-кодом** и ссылками по протоколам.
<img alt="Установка одной командой" src="manuals/assets/script-3x-ui-local.svg" width="860"> <img alt="Установка одной командой" src="manuals/assets/script-3x-ui-local.svg" width="860">
@@ -67,6 +73,9 @@ bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run # что имен
| `--harden` / `--no-harden` | закрыть ping и включить автообновления безопасности / не трогать это | | `--harden` / `--no-harden` | закрыть ping и включить автообновления безопасности / не трогать это |
| `--yes` | не задавать вопросы (для автоматизации) | | `--yes` | не задавать вопросы (для автоматизации) |
| `--host IP`, `--user имя`, `--port N`, `--restore файл` | адрес в ссылках, имя первого клиента, порт REALITY, восстановление из копии `kit backup` | | `--host IP`, `--user имя`, `--port N`, `--restore файл` | адрес в ссылках, имя первого клиента, порт REALITY, восстановление из копии `kit backup` |
| `--clean` | убрать прошлую (оборвавшуюся) установку и поставить заново — SSH не трогает |
| `--auto-sni` | не показывать меню выбора сайта маскировки, подобрать самим (автоподбор автора) |
| `--no-sub` | не показывать в конце доступ к панели, подписку и QR-код |
| `--no-time`, `--no-check`, `--verify-only`, `--dry-run`, `-h` | служебные | | `--no-time`, `--no-check`, `--verify-only`, `--dry-run`, `-h` | служебные |
## Что происходит по шагам ## Что происходит по шагам
@@ -74,15 +83,17 @@ bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run # что имен
1. **Проверка сервера**: root, Debian/Ubuntu, память, место на диске, занятые порты 80/443. 1. **Проверка сервера**: root, Debian/Ubuntu, память, место на диске, занятые порты 80/443.
2. **Проверка релиза**: файлы берутся из этого репозитория по тегу, проверяется **подпись `SHA256SUMS`** 2. **Проверка релиза**: файлы берутся из этого репозитория по тегу, проверяется **подпись `SHA256SUMS`**
нашим ключом и SHA256 самого установщика. Не сошлось — ничего не запускается. нашим ключом и SHA256 самого установщика. Не сошлось — ничего не запускается.
3. **Выбор протоколов** и запуск установщика: панель 3X-UI, выбранные подключения, nginx на 443, 3. **Наши вопросы** (до установки): протоколы, порт SSH, пароль root, ping, автообновления.
сертификат, сайт-заглушка, служба подписки, правила ufw. 4. **Установщик**: панель 3X-UI, только выбранные подключения, nginx на 443, сертификат, сайт-заглушка,
4. **Сайт-заглушка**: ставится случайный правдоподобный ИТ-сайт из нашего набора (10 штук) и служба подписки, правила ufw. Маскировку спрашивает он: свой домен, сосед по подсети или свой сайт.
синхронизируется в ответ Hysteria2. Если выбран сосед — сначала идёт **скан подсети**, затем меню выбора сайта (см. выше).
5. **Наши вопросы и правки**: закрыть ответ на ping, включить обновления безопасности, сменить порт 5. **Наши действия** (без вопросов): правдоподобный ИТ-сайт-заглушка вместо авторской страницы,
SSH, сменить пароль root. `kit fix` (время и безопасные починки), закрыть ответ на ping, включить обновления безопасности,
6. **`kit fix`** — синхронизация времени и безопасные починки. перевести SSH на новый порт, сменить пароль root.
7. **Проверка**: `kit check` (порты, службы, подписка, сертификат, маскировка) и сводка 6. **Проверка**: `kit check` (порты, службы, подписка, сертификат, маскировка) и сводка
`kit local hardening`. В конце — как забрать ссылки и подписку. `kit local hardening`.
7. **В самом конце** — **доступ к панели 3X-UI** (адрес, логин, пароль из `/root/3x-ui.txt`) и
**подписка с QR-кодом** и ссылками по протоколам. После них скрипт уже ничего не делает и не спрашивает.
## Протоколы ## Протоколы
@@ -110,6 +121,15 @@ bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run # что имен
дополнительных объяснений на клиенте. Маскировку и сайт можно менять потом: `kit net site`, дополнительных объяснений на клиенте. Маскировку и сайт можно менять потом: `kit net site`,
`kit local site`. `kit local site`.
**Сайт маскировки выбираете вы.** После скана подсети установщик спрашивает отдельно для REALITY
(найденный сосед или свой адрес) и отдельно для XHTTP/MTProto (найденный, свой или известный сайт —
список известных расширен Google, Amazon, Samsung, Yahoo, Microsoft, AMD, Intel, NVIDIA). Выбранный
адрес проверяется на TLS 1.3 + HTTP/2. Если XHTTP и MTProto не выбраны, второго вопроса нет.
Для протоколов, которые **не** выбраны, не делается ничего: ни подключений, ни записей в nginx, ни
ссылок в подписке, ни лишних обращений к сети (например, обход соседей запускается только когда он
действительно нужен).
## Сайт-заглушка ## Сайт-заглушка
На 443 при неизвестном имени отдаётся страница нашего набора: небольшие правдоподобные ИТ-сайты На 443 при неизвестном имени отдаётся страница нашего набора: небольшие правдоподобные ИТ-сайты
@@ -163,6 +183,7 @@ kit local site --show # какой стоит сейчас
| `kit local ping off\|on\|status` | ответ на ICMP echo правилом ufw (видно в `ufw status`, отменяется одной командой) | | `kit local ping off\|on\|status` | ответ на ICMP echo правилом ufw (видно в `ufw status`, отменяется одной командой) |
| `kit local passwd [юзер]`, `--generate` | смена пароля (спрашивает дважды, в логи не попадает) | | `kit local passwd [юзер]`, `--generate` | смена пароля (спрашивает дважды, в логи не попадает) |
| `kit local upgrades on\|off\|status` | обновления безопасности | | `kit local upgrades on\|off\|status` | обновления безопасности |
| `kit local uninstall` | **снести установку целиком**: панель, службы и файлы kit, nginx и сайт, сертификаты, наши правки системы и правила ufw для 80/443. Спрашивает слово «удалить», есть `--dry-run`. **SSH не трогает** |
| `kit local hardening`, `masking`, `channel`, `seams`, `version` | сводка защиты, состояние маскировки, канал обновлений, какие места форк правит у автора | | `kit local hardening`, `masking`, `channel`, `seams`, `version` | сводка защиты, состояние маскировки, канал обновлений, какие места форк правит у автора |
У любой правки есть `--dry-run` — показывает план и ничего не меняет. Копии изменённых файлов У любой правки есть `--dry-run` — показывает план и ничего не меняет. Копии изменённых файлов
+9 -9
View File
@@ -1,9 +1,9 @@
# 3X-UI KIT 1.2.7 # 3X-UI KIT 1.2.1.5
# 3X-UI_autoVPS 1.2.7 # 3X-UI_autoVPS 1.2.1.5
6076d7484223bfa00189f13266b4a131e940a77ebc05ae4bf0373f7245e81c55 scripts/3x-ui.sh e1ab2a1736228e332226bb857998ecf8a82ca6fd9aa8d2e590cffac65be02c14 scripts/3x-ui.sh
2865f428bfd05e0310ca0cbdd41808c2e78c5daf34a3547d607711b248eb596c scripts/hysteria2.sh f27933e32e8424ef556046e99710cccef3a53ba7fe9f996851512cdea39c0a1e scripts/hysteria2.sh
5cceb4d526a883424c445c79b70cf9122d0ee87ef7f625798c197d4d5a93b777 scripts/kit.sh 0b5fec8694b8793e3721040b54661629346cf91af5619df684657d0448d765df scripts/kit.sh
4027ea2c6dbc9fd176356acac3f676445419c2f021146ed25dfe5a304c3ac5fb scripts/kit-sub.py f214671464e1c2e174e5af9df42c95567a9b4a9141f31a39f7df2e971e89d240 scripts/kit-sub.py
9bb4284d60d79f6e9b7674c53e05553c7ee788952398bde95a240fabec2e8a8f scripts/kit-local.sh 857e7c7e08dd80fe0fe1963e908094f8e8ff4d439eb72ae55b96aa92cddbb169 scripts/kit-local.sh
fa6708e34d6208e45b95b7247808d440d4c20520dbe2ff23f5df280891d555d9 scripts/3x-ui-local.sh c5bb4070427effe3717ae733e8eb1f519df6ebcc462aa45d8eca1f57abe60f35 scripts/3x-ui-local.sh
d9e8e8429c6a7d0b965e4fac26a5efff7103f055faa98cceb5bd4731fd24603a VERSION d9aefff7d6235afc8a7f1581041719422d58b84b93b89c25d3ecb8f7d74567cb VERSION
+2 -2
View File
@@ -1,6 +1,6 @@
-----BEGIN SSH SIGNATURE----- -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgozRJ+f9RB6qO9S9j3BWCDtC0bV
FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA FHakRSSqtQPljLoQUAAAARM3gtdWkta2l0LXJlbGVhc2UAAAAAAAAABnNoYTUxMgAAAFMA
AAALc3NoLWVkMjU1MTkAAABA5fMLiArp8/6KqJMfwZpwlBu6zf9YkXaayCinxpSb1U2gCx AAALc3NoLWVkMjU1MTkAAABABuQ7Bk9OINTS/AAmMA/6YpaWINss4u0QgANgvIpQr+PxXV
bib/uVxP3BCGQlVIKHXbsZKSFkBHc+uYcTy6XkDA== XBYxM0M/XtgQ7vsC6bOrH6f2KgANdwJ4ieSjlkCQ==
-----END SSH SIGNATURE----- -----END SSH SIGNATURE-----
+1 -1
View File
@@ -1 +1 @@
1.2.7 1.2.1.5
+252 -96
View File
@@ -34,6 +34,8 @@
# --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет) # --yes ни о чём не спрашивать (ping закрою, автообновления включу, порт и пароль — нет)
# --no-time не запускать kit fix # --no-time не запускать kit fix
# --no-check не запускать в конце kit check # --no-check не запускать в конце kit check
# --clean убрать прошлую установку и поставить заново (SSH не трогает)
# --auto-sni не предлагать выбор сайта маскировки (иначе покажем меню по скану подсети)
# --verify-only только проверить релиз (подпись и SHA256) и выйти # --verify-only только проверить релиз (подпись и SHA256) и выйти
# --dry-run показать план и ничего не делать # --dry-run показать план и ничего не делать
# -h | --help эта справка # -h | --help эта справка
@@ -42,7 +44,7 @@
set -Eeuo pipefail set -Eeuo pipefail
KIT_VERSION="1.2.7" # версия сборки форка; должна совпадать с VERSION релиза KIT_VERSION="1.2.1.5" # версия сборки форка; должна совпадать с 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"
@@ -76,10 +78,12 @@ usage() {
--no-harden не трогать ping и автообновления --no-harden не трогать ping и автообновления
--ssh-port N перевести SSH на порт N (прежний закроется после проверки входа) --ssh-port N перевести SSH на порт N (прежний закроется после проверки входа)
--yes ни о чём не спрашивать (для автоматизации) --yes ни о чём не спрашивать (для автоматизации)
--site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или --site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или
не трогать авторскую страницу (то же: --no-site) не трогать авторскую страницу (то же: --no-site)
--no-sub не показывать в конце подписку, QR-код и ссылки по протоколам --no-sub не показывать в конце подписку, доступ к панели и QR-код
--no-time не запускать kit fix (время и безопасные починки) --clean снести прошлую установку и поставить заново (если прошлый запуск оборвался)
--auto-sni не предлагать выбор сайта маскировки (автоподбор автора)
--no-time не запускать kit fix (время и безопасные починки)
--no-check не запускать в конце kit check --no-check не запускать в конце kit check
--verify-only только проверить релиз (подпись и SHA256) и выйти --verify-only только проверить релиз (подпись и SHA256) и выйти
--dry-run показать план и ничего не делать --dry-run показать план и ничего не делать
@@ -92,6 +96,8 @@ EOF
INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no
site=random # заглушка: случайный сайт из нашего набора (off — не трогать) site=random # заглушка: случайный сайт из нашего набора (off — не трогать)
show_sub=yes # в самом конце показать подписку, QR-код и ссылки show_sub=yes # в самом конце показать подписку, QR-код и ссылки
clean=no # --clean: убрать прошлую установку перед новой
pick_sni=yes # показывать меню выбора сайта маскировки по результатам скана
while (($#)); do while (($#)); do
case $1 in case $1 in
--harden) harden=yes ;; --harden) harden=yes ;;
@@ -103,6 +109,8 @@ while (($#)); do
--site=*) site=${1#*=} ;; --site=*) site=${1#*=} ;;
--no-site) site=off ;; --no-site) site=off ;;
--no-sub) show_sub=no ;; --no-sub) show_sub=no ;;
--clean) clean=yes ;;
--auto-sni) pick_sni=no ;;
--no-time) fix_time=no ;; --no-time) fix_time=no ;;
--no-check) do_check=no ;; --no-check) do_check=no ;;
--verify-only) verify_only=yes ;; --verify-only) verify_only=yes ;;
@@ -134,6 +142,9 @@ if [[ $dry == yes ]]; then
say "план установки (--dry-run: ничего не делаю)" say "план установки (--dry-run: ничего не делаю)"
pre "версия сборки: $KIT_VERSION" pre "версия сборки: $KIT_VERSION"
pre "откуда: $GITEA (тег v$KIT_VERSION), подпись проверяется ключом форка" pre "откуда: $GITEA (тег v$KIT_VERSION), подпись проверяется ключом форка"
[[ $clean == yes ]] && pre "прошлая установка: уберу её перед установкой (--clean), SSH не трону"
if [[ $pick_sni == yes ]]; then pre "маскировка: просканирую подсеть и предложу выбрать сайт (--auto-sni — автоподбор)"
else pre "маскировка: подберу сайт сам, без вопросов (--auto-sni)"; fi
if ((${#INSTALLER_ARGS[@]})); then pre "установщик автора: bash scripts/3x-ui.sh ${INSTALLER_ARGS[*]}"; else pre "установщик автора: bash scripts/3x-ui.sh — спросит про маскировку и домен"; fi if ((${#INSTALLER_ARGS[@]})); then pre "установщик автора: bash scripts/3x-ui.sh ${INSTALLER_ARGS[*]}"; else pre "установщик автора: bash scripts/3x-ui.sh — спросит про маскировку и домен"; fi
if [[ $harden == auto ]]; then if [[ $harden == auto ]]; then
if interactive; then pre "защита: спрошу про ответ на ping и автообновления" if interactive; then pre "защита: спрошу про ответ на ping и автообновления"
@@ -153,7 +164,7 @@ if [[ $dry == yes ]]; then
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 "в самом конце: подписка, QR-код и ссылки по протоколам (--no-sub — не надо)" [[ $show_sub == yes ]] && pre "в самом конце: доступ к панели 3X-UI, подписка, QR-код и ссылки (--no-sub — не надо)"
echo echo
say "запуск без --dry-run выполнит всё это на сервере" say "запуск без --dry-run выполнит всё это на сервере"
exit 0 exit 0
@@ -265,16 +276,75 @@ 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[*]}")"
} }
# ------------------------------------------------------------------ снос прошлой установки (--clean)
# Нужен, когда прошлая установка оборвалась: авторский установщик в таком случае отказывается работать
# («3X-UI уже установлена этим скриптом»). Убираем то, что создаёт установка, и ставим заново.
# SSH не трогаем: ни порт, ни конфиги — иначе можно потерять доступ к серверу.
clean_previous() {
say "убираю прошлую установку (--clean)"
local s p
for s in x-ui kit-sub kit-update.timer kit-update.service kit-limit.timer kit-limit.service \
$(systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' | grep -E '^(x-ui|kit-|kit\.)' | grep -E '\.(service|timer)$'); do
systemctl list-unit-files 2>/dev/null | grep -q "^$s" || continue
systemctl disable --now "$s" >/dev/null 2>&1 || true
printf ' остановлено: %s\n' "$s"
done
for p in /usr/local/x-ui /etc/x-ui /usr/bin/x-ui /usr/bin/x-ui.new /etc/systemd/system/x-ui.service \
/usr/local/bin/kit /usr/local/lib/kit /usr/local/lib/kit-sub /etc/kit /etc/kit-sub \
/etc/systemd/system/kit-sub.service /etc/systemd/system/kit-update.service \
/etc/systemd/system/kit-update.timer /etc/systemd/system/kit-limit.service \
/etc/systemd/system/kit-limit.timer \
/etc/cron.d/kit-nginx-reload /etc/cron.d/kit-xui-menu /etc/cron.d/kit-sub-cert \
/etc/nginx/kit-stream.conf /etc/nginx/conf.d/kit.conf /var/www/kit \
/root/3x-ui.txt /root/cert /var/log/kit-install-check.log \
/root/kit-local-backup-* /root/kit-backup-* /root/x-ui-before-sni-* /root/x-ui-before-update-*; do
[[ -e $p ]] || continue
rm -rf $p && printf ' удалено: %s\n' "$p"
done
if [[ -f /etc/nginx/nginx.conf ]] && grep -q 'kit-stream\.conf' /etc/nginx/nginx.conf; then
sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf
printf ' из nginx.conf убрана наша строка включения\n'
fi
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 || true
printf ' удалён пользователь kit-sub\n'
fi
systemctl daemon-reload
if [[ -f /root/3x-ui.txt && -x /usr/local/x-ui/x-ui ]]; then
warn "часть файлов прошлой установки осталась — проверьте: ls -la /usr/local/x-ui /root/3x-ui.txt"
else
printf ' готово: место под установку свободно\n'
fi
}
installer_failed() {
if grep -q "likely firewall problem" /var/log/kit-domain-cert.log 2>/dev/null \
|| grep -q "Timeout during connect" /var/log/3x-ui-install.log 2>/dev/null; then
printf '\n%s!%s %s\n' "$Y" "$N" \
"Let's Encrypt не смог достучаться до порта 80 (в логе acme «Timeout during connect»)." >&2
printf ' Проверьте по порядку: открыт ли 80 в ufw (скрипт делает это сам) и не режет ли его хостер.\n' >&2
printf ' Либо ставьте без своего домена: в меню маскировки выберите «общедоступный сайт» —\n' >&2
printf ' сертификат для домена тогда не нужен, панель останется на самоподписанном.\n' >&2
fi
if [[ -f /root/3x-ui.txt && -x /usr/local/x-ui/x-ui ]]; then
printf '\n%s!%s %s\n' "$Y" "$N" \
"На сервере осталась прошлая установка (установщик так и пишет). Запустите ещё раз с флагом --clean — он уберёт старое и поставит заново." >&2
fi
die "установщик остановился с ошибкой — смотрите его вывод выше"
}
# ------------------------------------------------------------------ установка # ------------------------------------------------------------------ установка
if [[ -z $proto_flag ]]; then if [[ -z $proto_flag ]]; then
@@ -285,18 +355,9 @@ else
fi fi
say "протоколы: $protos — под остальные не создаётся ничего" say "протоколы: $protos — под остальные не создаётся ничего"
say "запускаю установщик автора (панель 3X-UI, протоколы, сайт-заглушка, подписка)" # ------------------------------------------------------------------ вопросы (всё спросим ДО установки)
if ((${#INSTALLER_ARGS[@]})); then printf ' флаги: %s\n' "${INSTALLER_ARGS[*]}"; else printf ' флагов нет: установщик задаст вопросы (маскировка, домен)\n'; fi # После установщика на экране появляются доступы к панели: там вопросы уже неуместны. Поэтому
# stdin: если есть терминал — отдаём его установщику (там есть вопросы), иначе /dev/null, # спрашиваем всё заранее, а действия выполняем после установки (см. «наши дополнения» ниже).
# иначе установщик начнёт читать наш собственный скрипт вместо ответов.
if [[ -r /dev/tty ]]; then
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || die "установщик остановился с ошибкой — смотрите его вывод выше"
else
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || die "установщик остановился с ошибкой (нет терминала — вопросы задать некому)"
fi
command -v kit >/dev/null || die "после установки нет команды kit — что-то пошло не так, смотрите вывод выше"
# ------------------------------------------------------------------ наши дополнения
ask_yes() { # "вопрос" y|n — по умолчанию ask_yes() { # "вопрос" y|n — по умолчанию
local q=$1 def=$2 ans="" local q=$1 def=$2 ans=""
@@ -308,43 +369,6 @@ ask_yes() { # "вопрос" y|n — по умолчанию
[[ $ans =~ ^(y|yes|д|да)$ ]] [[ $ans =~ ^(y|yes|д|да)$ ]]
} }
# Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт.
# Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2,
# иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи).
case $site in
off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;;
random)
say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора"
kit local site || warn "не удалось поставить заглушку — позже: kit local site"
;;
*)
say "ставлю сайт-заглушку №$site"
kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site"
;;
esac
if [[ $fix_time == yes ]]; then
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
fi
# ping и автообновления: --harden/--no-harden решают без вопросов, иначе спрашиваем
want_ping=$harden; want_update=$harden
[[ $want_ping == auto ]] && want_ping=yes
[[ $want_update == auto ]] && want_update=yes
if [[ $harden == auto ]] && interactive; then
if ask_yes "Закрыть ответ сервера на ping? Снаружи пинг перестанет отвечать (подписка и VPN не затрагиваются)." y; then want_ping=yes; else want_ping=no; fi
if ask_yes "Включить автообновления безопасности? Только security-обновления, без перезагрузки." y; then want_update=yes; else want_update=no; fi
fi
if [[ $want_ping == yes ]]; then
say "закрываю ответ на ping (правилом ufw)"
kit local ping off || warn "не удалось — повторите: kit local ping off"
fi
if [[ $want_update == yes ]]; then
say "включаю автообновления безопасности"
kit local upgrades on || warn "не удалось — повторите: kit local upgrades on"
fi
# порт SSH: по флагу или по вопросу. Смена делается в два шага: сначала ДОБАВИТЬ новый порт # порт SSH: по флагу или по вопросу. Смена делается в два шага: сначала ДОБАВИТЬ новый порт
# (прежний продолжает работать), потом закрыть прежние — иначе одна опечатка оставляет сервер без доступа. # (прежний продолжает работать), потом закрыть прежние — иначе одна опечатка оставляет сервер без доступа.
ssh_ports_now() { ssh_ports_now() {
@@ -381,54 +405,186 @@ ask_ssh_port() { # пусто — оставить как есть; иначе
warn "оставляю прежний порт ($cur)" warn "оставляю прежний порт ($cur)"
} }
want_ping=$harden; want_update=$harden
[[ $want_ping == auto ]] && want_ping=yes
[[ $want_update == auto ]] && want_update=yes
if [[ $harden == auto ]] && interactive; then
if ask_yes "Закрыть ответ сервера на ping? Снаружи пинг перестанет отвечать (подписка и VPN не затрагиваются)." y; then want_ping=yes; else want_ping=no; fi
if ask_yes "Включить автообновления безопасности? Только security-обновления, без перезагрузки." y; then want_update=yes; else want_update=no; fi
fi
[[ -n $ssh_port ]] || ssh_port=$(ask_ssh_port) [[ -n $ssh_port ]] || ssh_port=$(ask_ssh_port)
ssh_ports_before=$(ssh_ports_now) # для подсказки в конце: какие порты закрывать
want_passwd=no
if interactive; then
if ask_yes "Сменить пароль root прямо сейчас? Новый пароль введёте в следующем окне." n; then want_passwd=yes; fi
fi
[[ $clean == yes ]] && clean_previous
# Порты для сертификата и маскировки: открываем правила ufw ДО установщика и говорим, где смотреть,
# если сертификат не выпустится (частая причина — свой файрвол у хостера, а не ufw).
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then
for _p in 80/tcp 443/tcp; do ufw allow "$_p" >/dev/null 2>&1 || true; done
say "порты 80 и 443 открыты в ufw (нужны для сертификата и работы маскировки)"
fi
printf ' %sесли у хостера свой файрвол — 80 и 443 должны быть разрешены и там.%s\n' "$D" "$N"
printf ' %sпризнак закрытого 80: сертификат не выпускается, в логе acme «Timeout during connect (likely firewall problem)»%s\n' "$D" "$N"
# Меню выбора сайта маскировки: сканируем подсеть один раз и предлагаем выбрать сайт отдельно для
# REALITY и (если выбраны) для XHTTP/MTProto. Отключается флагом --auto-sni (тогда автоподбор автора).
if [[ $pick_sni == yes ]]; then export KL_PICK_SNI=1; else export KL_PICK_SNI=0; fi
say "запускаю установщик автора (панель 3X-UI, протоколы, сайт-заглушка, подписка)"
if ((${#INSTALLER_ARGS[@]})); then printf ' флаги: %s\n' "${INSTALLER_ARGS[*]}"; else printf ' флагов нет: установщик задаст вопросы (маскировка, домен)\n'; fi
# stdin: если есть терминал — отдаём его установщику (там есть вопросы), иначе /dev/null,
# иначе установщик начнёт читать наш собственный скрипт вместо ответов.
if [[ -r /dev/tty ]]; then
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/tty || installer_failed
else
bash "$tmp/scripts/3x-ui.sh" ${INSTALLER_ARGS[@]+"${INSTALLER_ARGS[@]}"} </dev/null || installer_failed
fi
command -v kit >/dev/null || die "после установки нет команды kit — что-то пошло не так, смотрите вывод выше"
# Ставим нашу заглушку СРАЗУ (а не поверх авторской): страницу кладём до установщика, а его собственную
# страницу из релиза убираем — иначе она перезапишет нашу на шаге сборки сайта.
stub_staged=no
if [[ $site != off ]]; then
stub_n=""
case $site in
random) stub_n=$(( RANDOM % 10 )) ;;
*) [[ $site =~ ^[0-9]+$ ]] && stub_n=$((10#$site - 1)) ;;
esac
if [[ -n $stub_n ]]; then
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>"$tmp/stub.err" \
&& [[ -s /var/www/kit/index.html ]]; then
stub_staged=yes
rm -f "$tmp/var/www/kit/index.html"
say "заглушка: наш сайт №$((stub_n + 1)) — ставится сразу, авторская страница не используется"
else
warn "не смог подготовить нашу заглушку — поставлю после установки (kit local site). Причина: $(head -c 200 "$tmp/stub.err" 2>/dev/null | tr '\n' ' ')"
fi
fi
fi
# Запоминаем, какие протоколы выбраны: наши команды (kit local site и др.) смотрят сюда и не
# делают ничего для протоколов, которых в этой установке нет.
printf '%s\n' "${protos//,/ }" | tr ' ' '\n' | sed '/^$/d' > /etc/kit/kl-protos 2>/dev/null || true
# ------------------------------------------------------------------ наши дополнения (только действия)
# Ничего не спрашиваем: все ответы собраны до установщика. Здесь только выполняем.
# 443 нужен для маскировки и VPN, 80 — для выпуска сертификата: проверяем правила ufw ещё раз.
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "^Status: active"; then
for _p in 80/tcp 443/tcp; do ufw allow "$_p" >/dev/null 2>&1 || true; done
fi
# Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт.
# Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2,
# иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи).
case ${stub_staged:-no} in
yes) say "заглушка уже стоит (поставлена при установке, Hysteria2 синхронизирован тем же файлом)" ;;
*) case $site in
off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;;
random)
say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора"
kit local site || warn "не удалось поставить заглушку — позже: kit local site"
;;
*)
say "ставлю сайт-заглушку №$site"
kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site"
;;
esac ;;
esac
if [[ $fix_time == yes ]]; then
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
fi
# Действия по ответам, которые были заданы ДО установщика (см. блок вопросов выше).
if [[ $want_ping == yes ]]; then
say "закрываю ответ на ping (правилом ufw)"
kit local ping off || warn "не удалось — повторите: kit local ping off"
fi
if [[ $want_update == yes ]]; then
say "включаю автообновления безопасности"
kit local upgrades on || warn "не удалось — повторите: kit local upgrades on"
fi
if [[ $want_passwd == yes ]]; then
say "меняю пароль root"
kit local passwd || warn "не получилось — повторите: kit local passwd"
fi
# порт SSH: новый порт добавляем, прежний остаётся рабочим до вашей проверки входа.
if [[ -n $ssh_port ]]; then if [[ -n $ssh_port ]]; then
say "перевожу SSH на порт $ssh_port: новый добавляю, прежний пока остаётся рабочим" say "перевожу SSH на порт $ssh_port: новый добавляю, прежний пока остаётся рабочим"
kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port" kit local ssh "$ssh_port" || warn "порт не добавился — сделайте отдельно: kit local ssh $ssh_port"
fi fi
# пароль root: в флагах его нет намеренно — значение в командной строке видно в списке процессов # ------------------------------------------------------------------ итог: коротко и по делу
if interactive; then
if ask_yes "Сменить пароль root прямо сейчас?" n; then if [[ $do_check == yes || $show_sub == yes ]]; then
kit local passwd || warn "не получилось — повторите: kit local passwd" # kit check печатает много строк: человеку нужны только замечания и одна строка «всё ли в порядке».
chk=$(kit check 2>&1 || true)
problems=$(printf '%s\n' "$chk" | grep -cE '^❌|^⚠|^!' || true)
[[ $problems == 0 ]] || printf '%s\n' "$chk" | grep -E '^❌|^⚠|^!' | sed 's/^/ /'
ports_open=""
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 }')
os_show=$( . /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-}" )
cert_show="без сертификата: панель открывается через SSH-туннель"
if [[ -s /root/cert/ip/fullchain.pem ]]; then
if openssl x509 -in /root/cert/ip/fullchain.pem -noout -issuer 2>/dev/null | grep -q "Let's Encrypt"; then
cert_show="${G}Let's Encrypt на IP — доверенный, браузер не ругается${N}"
else
cert_show="${Y}самоподписанный — браузер предупредит${N}"
fi
fi fi
fi protos_show=$(tr '\n' ' ' </etc/kit/kl-protos 2>/dev/null || true)
cat <<EOF
# ------------------------------------------------------------------ проверка ${B}────────────── Установка завершена ──────────────${N}
сборка: 3X-UI_autoVPS $KIT_VERSION (канал: наш Gitea)
сервер: ${ip_show:-?}${os_show:+ · $os_show}
протоколы: ${protos_show:-—}
сертификат: $cert_show
проверка: $([[ $problems == 0 ]] && echo 'замечаний нет' || echo "замечаний: $problems (подробности: kit check)")
${B}───────────────────────────────────────────────────${N}
if [[ $do_check == yes ]]; then ${B}Доступ к панели${N} (файл /root/3x-ui.txt, виден только root)
say "проверяю, что получилось"
kit check || warn "kit check нашёл замечания — читайте выше; безопасные починки: kit fix"
kit local hardening
fi
cat <<EOF
${G}${B}Готово.${N} Сервер поставлен из нашей сборки (3X-UI_autoVPS $KIT_VERSION, канал: наш Gitea).
подписка и ссылки: kit user link admin (или страница подписки из вывода установщика)
если что-то не так: kit check --deep, затем kit fix
вернуться к авторскому каналу: положить авторский kit поверх и выполнить kit update --force
EOF EOF
if grep -aqE '^ (Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
# ------------------------------------------------------------------ подписка, QR-код и доступы grep -aE '^ (Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^/ /'
else
# Показываем в самом конце, после всех проверок: это то, что человек уносит с собой. warn "данных панели в /root/3x-ui.txt нет — смотрите вывод установщика выше"
# Печатает авторская команда (подписка + 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"
if ! kit user link "$sub_user" --all; then
warn "не смог показать ссылки для «$sub_user» — список пользователей: kit user list"
fi fi
else cat <<EOF
say "ссылки и QR-код не показываю (--no-sub). Посмотреть: kit user link admin --all"
${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
fi fi
+526 -125
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@
# Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём # Код выхода 1 при любом запуске (и при `__update_binary`) – старый `hy2 update` на нём
# останавливается и не заменяет команду hy2. # останавливается и не заменяет команду hy2.
KIT_VERSION="1.2.7" KIT_VERSION="1.2.1.5"
cat >&2 <<'MSG' cat >&2 <<'MSG'
Отдельный установщик Hysteria2 больше не поддерживается. Отдельный установщик Hysteria2 больше не поддерживается.
+126 -4
View File
@@ -73,6 +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 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 штук)
@@ -442,12 +446,14 @@ kit_local_hardening() {
cat <<'EOF' cat <<'EOF'
Чего здесь НЕТ и почему (в отличие от vps-setup): Что сделано намеренно МЯГЧЕ, чем в популярных скриптах первичной настройки:
* apt full-upgrade — тянет систему вперёд и может сломать закреплённые панель и ядро; * apt full-upgrade — тянет систему вперёд и может сломать закреплённые панель и ядро;
вместо него включены обновления безопасности; вместо него включены обновления безопасности;
* автоматическая перезагрузка после правок — сервер не должен вставать без вашего решения; * автоматическая перезагрузка после правок — сервер не должен вставать без вашего решения;
* общесистемный icmp_echo_ignore_all — правило ufw видно в `ufw status` и отменяется одной * общесистемный запрет ICMP для всех интерфейсов — здесь он выключен на уровне ufw: правило видно
командой, не глушит диагностику внутренних интерфейсов. в `ufw status` и отменяется одной командой, а внутренняя диагностика не глушится.
Причина одна: перечисленное может увести систему вперёд или выключить сервер, а панель и ядро у нас
закреплённых версий.
Что включить при желании: Что включить при желании:
kit local ssh 2222 добавить порт SSH kit local ssh 2222 добавить порт SSH
@@ -873,7 +879,9 @@ kit_local_site() {
# Порт 443 отдаёт файл через nginx (перезагрузка не нужна). UDP-порт Hysteria2 держит копию # Порт 443 отдаёт файл через nginx (перезагрузка не нужна). UDP-порт Hysteria2 держит копию
# страницы внутри подключения — её надо обновить, иначе ответы разойдутся. # страницы внутри подключения — её надо обновить, иначе ответы разойдутся.
if kl_have kit; then if kl_have kit; then
if kit net masq on >/dev/null 2>&1; then if [[ -f /etc/kit/kl-protos ]] && ! grep -qE '(^|[ ,])hy2([ ,]|$)' /etc/kit/kl-protos; then
kl_ok "Hysteria2 в этой установке не ставился — синхронизировать нечего"
elif kit net masq on >/dev/null 2>&1; then
kl_ok "Hysteria2 отвечает той же страницей" kl_ok "Hysteria2 отвечает той же страницей"
else else
kl_warn "Hysteria2 не синхронизирован (может, его нет в этой установке) — проверьте: kit net masq on" kl_warn "Hysteria2 не синхронизирован (может, его нет в этой установке) — проверьте: kit net masq on"
@@ -894,6 +902,10 @@ kit_local_cmd() {
passwd | pass) shift; kit_local_passwd "$@" ;; passwd | pass) shift; kit_local_passwd "$@" ;;
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 "$@" ;;
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
@@ -905,6 +917,112 @@ kit_local_cmd() {
# Точка входа: вызывается из kit.sh с аргументами командной строки. # Точка входа: вызывается из kit.sh с аргументами командной строки.
# Если команда не наша — возвращаем 0, и kit работает ровно как раньше. # Если команда не наша — возвращаем 0, и kit работает ровно как раньше.
# ---------------------------------------------------------------- снос установки целиком
# Убираем всё, что появляется после установки: панель 3X-UI и службу x-ui, ядро Xray, службы и файлы
# kit (kit-sub, автообновление, cron), конфиги nginx и сайт-заглушку, сертификаты, правила ufw для 80/443,
# наши правки системы (правило ICMP, обновления безопасности), наши бэкапы и файл с данными панели.
#
# ЧЕГО НЕ ТРОГАЕМ: SSH — ни порт, ни конфиги, ни правила ufw для него, ни наш ssh-дроп-ин. Иначе легко
# остаться без доступа к серверу. Если SSH-порт когда-то менялся, верните его сами: kit local ssh 22
# (и закрыть лишние: kit local ssh --close-old 22).
#
# kit local uninstall спросит подтверждение словом «удалить»
# kit local uninstall --dry-run только покажет, что будет удалено
kit_local_ports() { # [--close-extra] — что открыто в ufw, что реально нужно и как закрыть лишнее
local close=no a
for a in "$@"; do
case $a in
--close-extra) close=yes ;;
--help | -h) printf 'kit local ports [--close-extra] — открытые порты, нужные и лишние\n'; return 0 ;;
*) kl_die "kit local ports [--close-extra]"; return 1 ;;
esac
done
command -v ufw >/dev/null 2>&1 || { kl_warn "ufw не установлен — порты смотреть нечем"; return 0; }
local -a opened=() needed=() extra=()
mapfile -t opened < <(ufw status 2>/dev/null | awk '$2 == "ALLOW" { print $1 }' | sort -u)
# Нужные порты: те, что слушают наши подключения (x-ui и ядро Xray), плюс SSH.
mapfile -t needed < <({ ss -tlnp 2>/dev/null; ss -ulnp 2>/dev/null; } \
| awk '/x-ui|xray/ { print $4 }' | sed 's/.*://' | sort -un; ss -tlnp 2>/dev/null | awk '/sshd/ { print $4 }' | sed 's/.*://' | sort -un)
local p q keep
for p in "${opened[@]}"; do
keep=no
case $p in 80/tcp | 443/tcp) keep=yes ;; esac # 443 — работа, 80 — продление сертификата
for q in "${needed[@]}"; do [[ $p == "$q/tcp" || $p == "$q/udp" || $p == "$q" ]] && keep=yes; done
[[ $keep == yes ]] && continue
extra+=("$p")
done
printf ' открыто в ufw: %s\n' "$(printf '%s ' "${opened[@]}")"
if ((${#needed[@]})); then printf ' нужны: %s\n' "$(printf '%s/tcp %s/udp ' "${needed[@]}" "${needed[@]}")"
else printf ' нужны: (не определил — смотрите kit check)\n'; fi
if ((${#extra[@]})); then
printf ' лишние: %s\n' "$(printf '%s ' "${extra[@]}")"
printf ' закрыть лишние: kit local ports --close-extra\n'
if [[ $close == yes ]]; then
for p in "${extra[@]}"; do ufw --force delete allow "$p" >/dev/null 2>&1 && printf ' закрыл %s\n' "$p"; done
fi
else
printf ' лишних нет\n'
fi
}
kit_local_access() { # доступ к панели, ссылки на подключения, заглушка и сертификат — одним экраном
local u=${1:-} till2
printf '%s\n' "${B:-}Доступ к панели 3X-UI${N:-} (файл /root/3x-ui.txt, виден только root)"
if grep -aqE '^ (Панель|Логин|Пароль):' /root/3x-ui.txt 2>/dev/null; then
grep -aE '^ (Панель|Логин|Пароль):' /root/3x-ui.txt | sed 's/^/ /'
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`
# Категории и все команды: сначала авторские, затем наши. Если автор добавит команду, а справка # Категории и все команды: сначала авторские, затем наши. Если автор добавит команду, а справка
# про неё не знает — сборку релиза остановит проверка tools/check-help.sh. # про неё не знает — сборку релиза остановит проверка tools/check-help.sh.
@@ -951,6 +1069,10 @@ ${B:-}3X-UI_autoVPS ${KIT_VERSION:-?} — все команды${N:-} (то ж
kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит
Проверка и починка Проверка и починка
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
View File
@@ -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()
+896 -113
View File
File diff suppressed because it is too large Load Diff
+521 -136
View File
@@ -86,145 +86,530 @@ def main():
version_re = r'^KIT_VERSION="[0-9.]+"$' version_re = r'^KIT_VERSION="[0-9.]+"$'
edits = [ edits = [
# --- scripts/kit.sh -------------------------------------------------
("scripts/kit.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"),
( (
"scripts/kit.sh", 'scripts/kit.sh',
'# kit – управление сервером 3X-UI KIT: пользователи, обновление, резервная копия.\n',
False,
'# kit – управление сервером 3X-UI_autoVPS: пользователи, обновление, резервная копия.\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }\n',
False,
'kit_ref_raw() { echo "https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$1"; }\n\n# --- 3X-UI_KIT (форк hacker07): локальные команды и хуки ---\n# Подключаем scripts/kit-local.sh, если он установлен. Наш файл: правки автора его не касаются.\n# Все команды форка — под «kit local …» (см. kit local help).\nKIT_LOCAL_LIB="${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh}"\nif [[ -r $KIT_LOCAL_LIB ]]; then . "$KIT_LOCAL_LIB"; kit_local_dispatch "$@"; fi\n\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'Description=3X-UI KIT: общий лимит трафика пользователей (сумма по всем записям)\n',
False,
'Description=3X-UI_autoVPS: общий лимит трафика пользователей (сумма по всем записям)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'Description=3X-UI KIT: проверка общего лимита трафика раз в 5 минут\n',
False,
'Description=3X-UI_autoVPS: проверка общего лимита трафика раз в 5 минут\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' # SHA256:bNjI10eGVGjdFeDJnAHvUFi73rlrXVy6vKFf8lmNyxU (itsnotkubrick, 2026-10-14; прежний ключ SHA256:VDuGgJ8d… утерян, снят)\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEWDFGq2Wno/ADRS2E6UKsgFbUSLvaiPIfRBvSEGJr81 3X-UI KIT releases"\n',
False,
' # Наш шов: здесь только ключ форка (hacker07). Авторские ключи сняты намеренно — сервер обязан\n # принимать обновления лишь из нашего канала (git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS).\n "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKM0Sfn/UQeqjvUvY9wVgg7QtG1RR2pEUkqrUD5Yy6EF 3X-UI_autoVPS fork releases (hacker07)"\n',
'авто-шов: replace, строк 2',
),
(
'scripts/kit.sh',
'update_hint() {\n',
False,
'update_hint() { # подсказка про новую версию\n [[ -d /etc/kit ]] || return 0 # kit удалён — подсказывать нечего (раньше тут сыпались ошибки про latest-version)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' echo "3X-UI KIT $KIT_VERSION"\n',
False,
' echo "3X-UI_autoVPS $KIT_VERSION"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'\n',
False,
' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py; do\n',
False,
' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS" || { warn "Подписанный релиз не той версии – ничего не ставлю."; return 1; }\n for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do\n',
'авто-шов: replace, строк 2',
),
(
'scripts/kit.sh',
'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n',
False,
'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'Description=3X-UI KIT: автообновление kit и kit-sub (только подписанные релизы)\n',
False,
'Description=3X-UI_autoVPS: автообновление kit и kit-sub (только подписанные релизы)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'Description=3X-UI KIT: проверка обновлений раз в сутки\n',
False,
'Description=3X-UI_autoVPS: проверка обновлений раз в сутки\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI KIT $KIT_VERSION → $latest"; fi\n',
False,
' if [[ $latest == "$KIT_VERSION" ]]; then say "Применяю исправления версии $latest"; else say "3X-UI_autoVPS $KIT_VERSION → $latest"; fi\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n',
False,
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n install -d -m 755 /usr/local/lib/kit\n install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n',
'авто-шов: insert, строк 1',
),
(
'scripts/kit.sh',
' echo "${G}✓ Готово: 3X-UI KIT $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
False,
' echo "${G}✓ Готово: 3X-UI_autoVPS $latest.${N} Пользователи, ссылки и подписки не менялись."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI KIT"\n',
False,
'split_panel_ours() { # наш профиль: JSON или ссылка happ://routing/onadd/<base64> с Name "3X-UI_autoVPS"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' grep -q \'"Name":"3X-UI KIT"\' <<<"$v"\n',
False,
' grep -q \'"Name":"3X-UI_autoVPS"\' <<<"$v"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' box "3X-UI KIT $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n',
False,
' box "3X-UI_autoVPS $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \\\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' "7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "10. Удалить KIT с сервера" "0. Выход"\n',
False,
' "7. Резервная копия" "8. Версии" "" "9. Доступ к панели, ссылки и заглушка" "10. Панель x-ui" "11. Удалить KIT с сервера" "0. Выход"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
' 9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;\n 10) run_action cmd_uninstall; pause ;;\n',
False,
' # Наш шов: пункт 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',
),
(
'scripts/kit.sh',
' echo "${B}Полное удаление 3X-UI KIT${N}"\n',
False,
' echo "${B}Полное удаление 3X-UI_autoVPS${N}"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit.sh',
'${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
False,
'${B}3X-UI_autoVPS $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"\n',
False,
'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/tag/v$KIT_VERSION"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
'SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)\n',
False,
'# Наш шов: добавлены известные сайты (Microsoft, AMD, Intel, NVIDIA) — их можно выбрать вручную.\nSNI_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)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
" seq 1 254 | xargs -P 8 -I{} bash -c '\n",
False,
" # Наш шов: 32 потока вместо 8 и таймаут 3 с — скан подсети занимает секунды, а не минуту.\n seq 1 254 | xargs -P 32 -I{} bash -c '\n",
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true\n',
False,
' echo | timeout 3 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \\\n | tr "," "\\n" | sed -n "s/^ *DNS://p"\' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -60 || true\n',
'авто-шов: replace, строк 2',
),
(
'scripts/3x-ui.sh',
' local me a b c base n names found=0\n',
False,
' local me a b c base n names out\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n',
False,
' [[ $me =~ ^[0-9]{1,3}(\\.[0-9]{1,3}){3}$ ]] || return 0\n IFS=. read -r a b c _ <<<"$me"\n # Наш шов: имена проверяются параллельно (8 сразу) — раньше по очереди, и это была основная\n # причина «долго сканирует». Функции передаём в подпроцессы через export -f.\n export -f same_net sni_alive sni_trusted sni_quality sni_ok host_ip\n export HOST\n',
'авто-шов: insert, строк 2',
),
(
'scripts/3x-ui.sh',
' names=$(nearby_scan "$base")\n for n in $names; do\n [[ $n == "$me" || $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n if same_net "$n" && sni_alive "$n" && sni_trusted "$n" && sni_quality "$n"; then echo "$n"; found=$((found + 1)); ((found >= 8)) && return 0; fi\n done\n ((found > 0)) && return 0\n',
False,
' names=$(nearby_scan "$base" | grep -vE "^(${me})$" || true)\n [[ -n $names ]] || continue\n out=$(while IFS= read -r n; do\n [[ $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE || $n =~ $SNI_BAD_RE ]] && continue\n printf \'%s\\n\' "$n"\n done <<<"$names" | xargs -P 8 -I{} bash -c \'same_net "$1" && sni_alive "$1" && sni_trusted "$1" && sni_quality "$1" && printf "%s\\n" "$1"\' _ {} 2>/dev/null | head -8)\n [[ -n $out ]] && { printf \'%s\\n\' "$out"; return 0; }\n',
'авто-шов: replace, строк 6',
),
(
'scripts/3x-ui.sh',
' [[ -x $acme ]] || { warn "Не нашёл acme.sh, которым установщик 3X-UI получает сертификаты: не могу выпустить сертификат для $DOMAIN."; return 1; }\n',
False,
' [[ -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',
),
(
'scripts/3x-ui.sh',
' [[ $rc == 0 || $rc == 2 ]] || { warn "Let\'s Encrypt не выдал сертификат для $DOMAIN. Обычно дело в одном из трёх: A-запись ещё не обновилась, порт 80 закрыт у хостера или домен за проксированием Cloudflare. Лог: $log"; return 1; }\n',
False,
' [[ $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',
),
(
'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',
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\\n",
False,
" _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/\n",
'авто-шов: replace, строк 5',
),
(
'scripts/3x-ui.sh',
' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."\n',
False,
' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."\n fi\n\n',
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 # Наш шов: последним пунктом — автовыбор из этого списка (раньше выбор был только по номеру).\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',
),
(
'scripts/3x-ui.sh',
' elif [[ -z $SNI && $yes == no && $PANEL_SSL == ip && $multi == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
False,
' # Наш шов: когда выбор сайта делает наше меню (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',
),
(
'scripts/3x-ui.sh',
' echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)"\n',
False,
' echo "Сканирую подсеть: около 250 коротких обращений к порту 443 соседних адресов (около минуты)."\n echo "По результатам предложу сайты для маскировки. Не нужно – запустите с --sni или --domain."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' if ((${#nb[@]})); then\n SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}\n',
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 # Наш шов: домен вводится заново, пока он не указывает на этот сервер. Раньше при промахе\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',
),
(
'scripts/3x-ui.sh',
' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n else\n',
False,
' ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"\n elif [[ -z $SNI ]]; then\n',
'авто-шов: replace, строк 2',
),
(
'scripts/3x-ui.sh',
' if [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
False,
' if [[ -n ${KL_PICK_SNI:-} && ${KL_PICK_SNI} == 1 && -z $SNI ]]; then\n : # сайт уже выбран в нашем меню — вопрос автора не нужен\n elif [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then\n',
False,
' # Наш шов: если XHTTP и MTProto не выбраны, имена для них сейчас не нужны — не сканируем соседей\n # по подсети впустую (около 250 обращений к чужим 443). Имена SNI2/SNI3 возьмутся из общего списка ниже.\n if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]] \\\n && [[ " ${PROTOS[*]} " == *" xhttp "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя. Исправьте причину и запустите скрипт снова."\n',
False,
' # Наш шов: провал сертификата не обрывает установку. Человек может повторить попытку (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',
),
(
'scripts/3x-ui.sh',
' ok "подключения работают: REALITY, XHTTP и Hysteria2 проверены клиентом с самого сервера"\n',
False,
' ok "подключения работают (проверено клиентом с самого сервера): ${CREATED[*]:-нет}"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' local extra\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n',
False,
' local extra="" # пустая строка, иначе set -u валит скрипт, когда AWG/MTProto не выбраны\n # Наш шов: подписки «-awg»/«-tg» есть только у установок с AmneziaWG и MTProto — не тянем их,\n # если эти протоколы не выбраны (никаких лишних обращений к панели).\n if [[ " ${PROTOS[*]} " == *" awg "* || " ${PROTOS[*]} " == *" awg3 "* || " ${PROTOS[*]} " == *" mtproto "* ]]; then\n extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)\n fi\n',
'авто-шов: replace, строк 2',
),
(
'scripts/3x-ui.sh',
' echo "3X-UI KIT (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n',
False,
' echo "3X-UI_autoVPS (3X-UI $XUI_VERSION) – данные для входа (файл виден только root)"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' } >"$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,
' } >"$RESULT"\n\n echo\n',
'авто-шов: delete, строк 6',
),
(
'scripts/3x-ui.sh',
' 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,
' echo "${G}${B}Установка завершена.${N} Доступы к панели, подписка и QR-код — в самом конце вывода."\n',
'авто-шов: replace, строк 25',
),
(
'scripts/3x-ui.sh',
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\n',
False,
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"\n',
'авто-шов: insert, строк 1',
),
(
'scripts/3x-ui.sh',
' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n',
False,
' else curl -fsSL --retry 3 -o /usr/local/bin/kit "$KIT_CLI_URL" && chmod 755 /usr/local/bin/kit; fi\n # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n install -d -m 755 /usr/local/lib/kit\n if [[ -n $src && -f $d/kit-local.sh ]]; then\n install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n else\n curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n fi\n',
'авто-шов: insert, строк 1',
),
(
'scripts/3x-ui.sh',
'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"\n',
False,
'KIT_INSTALL_CMD="bash <(curl -fsSL https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main/scripts/3x-ui.sh)"\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' echo "# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске."\n',
False,
' echo "# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
'# Сгенерировано 3x-ui.sh (3X-UI KIT) – перезаписывается при повторном запуске.\n',
False,
'# Сгенерировано 3x-ui.sh (3X-UI_autoVPS) – перезаписывается при повторном запуске.\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI KIT" --arg uri "$uri" \'\n',
False,
' upd=$(jq -c --arg path "$SUB_PATH" --argjson ip "$SUB_INTERNAL" --arg title "3X-UI_autoVPS" --arg uri "$uri" \'\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI KIT" \'\n',
False,
' upd=$(jq -c --arg path "$SUB_PATH" --arg title "3X-UI_autoVPS" \'\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
'Description=kit-sub: подписка с учётом приложения (3X-UI KIT)\n',
False,
'Description=kit-sub: подписка с учётом приложения (3X-UI_autoVPS)\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
False,
' [[ $(LC_ALL=C tr -d \'\\n -~\' <"$f" | wc -c) -eq 0 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
'авто-шов: replace, строк 2',
),
(
'scripts/3x-ui.sh',
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."\n',
False,
' [[ $rc -eq 1 ]] \\\n || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (файл ${f##*/}). Ничего не менял."\n',
'авто-шов: replace, строк 2',
),
(
'scripts/3x-ui.sh',
' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n',
False,
' [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' *) die "Это не резервная копия 3X-UI KIT или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n',
False,
' *) die "Это не резервная копия 3X-UI_autoVPS или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' || die "Это не резервная копия 3X-UI KIT или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n',
False,
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (значение $n в ${f##*/}). Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI KIT или она повреждена. Ничего не менял."\n',
False,
' python3 - "$file" "$tmp" <<\'PY\' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена. Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' || die "Это не резервная копия 3X-UI KIT или она повреждена (порт панели). Ничего не менял."\n',
False,
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (порт панели). Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."\n',
False,
' [[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (адрес сервера). Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' || die "Это не резервная копия 3X-UI KIT или она повреждена (нет своего сертификата). Ничего не менял."\n',
False,
' || die "Это не резервная копия 3X-UI_autoVPS или она повреждена (нет своего сертификата). Ничего не менял."\n',
'авто-шов: replace, строк 1',
),
(
'scripts/3x-ui.sh',
' [[ -n $DOMAIN ]] && { issue_domain_cert || die "Не удалось получить сертификат для $DOMAIN: порт 80 должен быть свободен, а A-запись домена указывать на этот сервер. Лог: /var/log/kit-domain-cert.log"; }\n',
False,
' # Наш шов: открываем 80 в ufw ПРЯМО ПЕРЕД выпуском сертификата. Установщик открывает список портов\n # позже (после сборки nginx), поэтому на свежем сервере проверка Let\'s Encrypt доходила до сервера и\n # «проглатывалась» — в логе acme это «Timeout during connect (likely firewall problem)».\n command -v ufw >/dev/null 2>&1 && ufw allow 80/tcp >/dev/null 2>&1 || true\n [[ -n $DOMAIN ]] && { issue_domain_cert || die "Без сертификата для $DOMAIN продолжать нельзя — причина выше, подробности в /var/log/kit-domain-cert.log"; }\n',
'авто-шов: replace, строк 1',
),
(
'scripts/hysteria2.sh',
'# установку 3X-UI KIT (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n',
False,
'# установку 3X-UI_autoVPS (scripts/3x-ui.sh). Файл оставлен, чтобы старая команда `hy2 update`\n',
'авто-шов: replace, строк 1',
),
(
'scripts/hysteria2.sh',
'Hysteria2 входит в основную установку 3X-UI KIT (один из 11 протоколов).\n',
False,
'Hysteria2 входит в основную установку 3X-UI_autoVPS (один из 11 протоколов).\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit-sub.py',
' data = {"Name": "3X-UI KIT", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n',
False,
' data = {"Name": "3X-UI_autoVPS", "GlobalProxy": "false", "DomainStrategy": "IPIfNonMatch", "ProxySites": sites,\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit-sub.py',
' data = {"Name": "3X-UI KIT", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n',
False,
' data = {"Name": "3X-UI_autoVPS", "DomainStrategy": "IPIfNonMatch", "RouteOrder": "block-proxy-direct",\n',
'авто-шов: replace, строк 1',
),
(
'scripts/kit-sub.py',
' main()\n',
False,
' main()\n\n',
'авто-шов: insert, строк 1',
),
(
'scripts/kit.sh',
version_re,
True,
f'KIT_VERSION="{version}"',
'версия сборки форка',
),
(
'scripts/3x-ui.sh',
version_re,
True,
f'KIT_VERSION="{version}"',
'версия сборки форка',
),
(
'scripts/hysteria2.sh',
version_re,
True,
f'KIT_VERSION="{version}"',
'версия сборки форка',
),
(
'scripts/kit.sh',
'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"', 'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"',
False, False,
'KIT_RAW="%s/raw/branch/main"' % GITEA, 'KIT_RAW="https://git.rukavishnikoff.ru/hacker07/3X-UI_autoVPS/raw/branch/main"',
"источник VERSION и подсказки об обновлении", 'канал обновлений: наш Gitea вместо GitHub',
),
(
"scripts/kit.sh",
'kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }',
False,
'kit_ref_raw() { echo "%s/raw/tag/v$1"; }' % GITEA + "\n" + HOOK,
"файлы релиза из нашего тега + подключение kit-local.sh",
),
(
"scripts/kit.sh",
r' # SHA256:[^\n]*\n "ssh-ed25519 [^\n]*"\n',
True,
' # Ключ подписи форка (владелец: hacker07). Закрытая часть — только на рабочей станции.\n'
' "%s"\n' % pub,
"ключ проверки подписи релизов",
),
(
"scripts/kit.sh",
"for f in scripts/kit.sh scripts/kit-sub.py; do",
False,
"for f in scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh; do",
"наш файл в списке проверяемых и скачиваемых",
),
(
"scripts/kit.sh",
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit',
False,
' install -m 755 "$tmp/kit.sh" /usr/local/bin/kit.new && mv -f /usr/local/bin/kit.new /usr/local/bin/kit\n'
" # Форк: ставим наши локальные дополнения из того же подписанного релиза.\n"
" install -d -m 755 /usr/local/lib/kit\n"
' install -m 644 "$tmp/kit-local.sh" /usr/local/lib/kit/kit-local.sh',
"установка kit-local.sh при обновлении",
),
# --- scripts/3x-ui.sh -----------------------------------------------
("scripts/3x-ui.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"),
(
"scripts/3x-ui.sh",
'KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"',
False,
'KIT_RAW="%s/raw/tag/v$KIT_VERSION"' % GITEA,
"запасная загрузка kit из нашего релиза",
),
(
"scripts/3x-ui.sh",
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"',
False,
'KIT_CLI_URL="$KIT_RAW/scripts/kit.sh"\nKIT_LOCAL_URL="$KIT_RAW/scripts/kit-local.sh"',
"адрес нашего файла для установщика",
),
(
"scripts/3x-ui.sh",
'KIT_INSTALL_CMD="bash <(curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main/scripts/3x-ui.sh)"',
False,
'KIT_INSTALL_CMD="bash <(curl -fsSL %s/raw/branch/main/scripts/3x-ui.sh)"' % GITEA,
"команда установки, которую показывает панель",
),
(
"scripts/3x-ui.sh",
' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"',
False,
" # Форк: ставим наши локальные дополнения (см. scripts/kit-local.sh).\n"
" install -d -m 755 /usr/local/lib/kit\n"
' if [[ -n $src && -f $d/kit-local.sh ]]; then\n'
' install -m 644 "$d/kit-local.sh" /usr/local/lib/kit/kit-local.sh\n'
" else\n"
' curl -fsSL --retry 3 -o /usr/local/lib/kit/kit-local.sh "$KIT_LOCAL_URL" \\\n'
' || warn "Локальные дополнения kit не скачались: kit local не будет работать"\n'
" fi\n"
' bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"',
"установка kit-local.sh установщиком",
),
# --- scripts/hysteria2.sh -------------------------------------------
("scripts/hysteria2.sh", version_re, True, 'KIT_VERSION="%s"' % version, "версия сборки форка"),
# --- tools/release.sh -----------------------------------------------
(
"tools/release.sh",
"FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py VERSION)",
False,
"FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)",
"наш файл в подписанном списке релиза",
),
(
"tools/release.sh",
'{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}"; } >SHA256SUMS',
False,
"{ echo \"# 3X-UI KIT $v\"; echo \"# 3X-UI_autoVPS $v\"; sha \"${FILES[@]}\" | awk '{ sub(/^\\*/, \"\", $2); print $1\" \"$2 }'; } >SHA256SUMS",
"суммы в POSIX-виде без звёздочки (на Windows sha256sum её ставит, и kit её не понимает)",
),
# --- свои записи о версии (наш файл, авторский CHANGELOG не трогаем) ----
(
"tools/release.sh",
'grep -q "^## v$v" CHANGELOG.md || die "В CHANGELOG.md нет раздела ## v$v"',
False,
'(grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md)'
' || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)"',
"раздел версии форка — в нашем файле",
),
(
"tools/release.sh",
'[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md) ]]',
False,
'[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]]',
"наш файл записей тоже должен быть закоммичен",
),
(
"scripts/kit.sh",
' curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" \'',
False,
' { curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null; curl -fsS -m 5 "$KIT_RAW/FORK-CHANGELOG.md" 2>/dev/null; } | awk -v v="## v$1" \'',
"«что нового» ищет и в наших записях",
),
(
"scripts/3x-ui.sh",
" _ _ _ _ _ _ _\n (_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __\n | | __/ __| '_ \\ / _ \\| __| |/ / | | | '_ \\| '__| |/ __| |/ /\n | | |_\\__ \\ | | | (_) | |_| <| |_| | |_) | | | | (__| <\n |_|\\__|___/_| |_|\\___/ \\__|_|\\_\\\\__,_|_.__/|_| |_|\\___|_|\\_\\",
False,
" _ _ _ ___ _____\n | | | | __ _ ___| | _____ _ __ / _ \\___ |\n | |_| |/ _` |/ __| |/ / _ \\ '__| | | | | / /\n | _ | (_| | (__| < __/ | | |_| |/ /\n |_| |_|\\__,_|\\___|_|\\_\\___|_| \\___//_/",
"баннер сервера: наша надпись вместо авторской",
),
(
"scripts/3x-ui.sh",
' echo "${B}3X-UI KIT $KIT_VERSION${N} · it\'s not Kubrick. it\'s just a VPN."',
False,
' echo "${B}3X-UI_autoVPS $KIT_VERSION${N} · сборка форка: панель 3X-UI и VPN-протоколы на одном порту"',
"подпись под баннером: наше имя сборки",
),
(
"scripts/kit.sh",
' grep -qx "# 3X-UI KIT $v" "$d/SHA256SUMS"',
False,
' grep -qxE "# (3X-UI KIT|3X-UI_autoVPS) $v" "$d/SHA256SUMS"',
"проверка шапки подписанного релиза принимает и старое, и новое имя",
), ),
] ]
+40 -5
View File
@@ -13,6 +13,7 @@ cd "$(dirname "$0")/.."
die() { printf '✗ %s\n' "$*" >&2; exit 1; } die() { printf '✗ %s\n' "$*" >&2; exit 1; }
ok() { printf '✓ %s\n' "$*"; } ok() { printf '✓ %s\n' "$*"; }
warn() { printf '! %s\n' "$*" >&2; } # сеть до GitHub отвалилась: проверку пропускаем, релиз не блокируем
sha() { if command -v sha256sum >/dev/null; then sha256sum "$@"; else shasum -a 256 "$@"; fi; } sha() { if command -v sha256sum >/dev/null; then sha256sum "$@"; else shasum -a 256 "$@"; fi; }
key=${1:-$HOME/.ssh/3xui_kit_release} key=${1:-$HOME/.ssh/3xui_kit_release}
@@ -39,12 +40,40 @@ ok "Переводы строк во всех файлах релиза — LF"
export PY export PY
bash tools/check-help.sh || die "справка kit help отстала от набора команд" bash tools/check-help.sh || die "справка kit help отстала от набора команд"
# «Строки-сироты»: строка с флагом без команды (например потерянный "#" в комментарии) не ловится
# ни bash -n, ни справкой, но при каждом запуске печатает «command not found». Проверяем холостым прогоном.
err=$(bash scripts/3x-ui-local.sh --dry-run 2>&1 >/dev/null || true)
if grep -qE "command not found|syntax error|bad substitution" <<<"$err"; then
printf '%s\n' "$err" | grep -E "command not found|syntax error|bad substitution" | head -3
die "установщик ругается на строку без команды — поправьте файл (см. выше)"
fi
ok "Установщик не ругается на пустые строки и опечатки"
# Наши функции в установщике обязаны быть определены РАНЬШЕ первого вызова: `bash -n` и холостой
# прогон этого не видят, а человек видит «command not found» посреди установки (так и было 13.10).
for fn in ask_yes ask_ssh_port ssh_ports_now ask_protocols proto_parse proto_menu interactive clean_previous installer_failed; do
# || true обязателен: без него grep, не найдя совпадений, возвращает 1 и set -e молча обрывает релиз.
d=$(grep -n "^$fn()" scripts/3x-ui-local.sh | head -1 | cut -d: -f1 || true)
c=$(grep -n "^[^#]*[^a-z_]$fn[ ;\"]" scripts/3x-ui-local.sh | grep -v "^${d:-0}:" | head -1 | cut -d: -f1 || true)
[[ -n $d ]] || die "scripts/3x-ui-local.sh: функция $fn не определена, а вызывается"
[[ -z $c || $c -gt $d ]] || die "scripts/3x-ui-local.sh: $fn вызывается (строка $c) раньше, чем определена ($d)"
done
ok "Все наши функции определены раньше своих вызовов"
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит." [[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
# Часть проверок сверяется с GitHub, а он из домашней сети отвечает через раз. Сначала короткая проба:
# нет связи — сразу предупреждаем и пропускаем внешние проверки (подписи это не мешает), а не висим минутами.
gh_ok=yes
curl -fsI --connect-timeout 8 --max-time 12 https://github.com >/dev/null 2>&1 || gh_ok=no
[[ $gh_ok == yes ]] || warn "GitHub недоступен из вашей сети — установщик, суммы ядра и панели не сверяю. Подписи это не мешает."
gh_curl() { [[ ${gh_ok:-no} == yes ]] || return 1; curl -fsSL --retry 2 --retry-delay 2 --connect-timeout 15 --max-time 90 "$@"; }
# Установщик 3X-UI: хеш в скрипте должен совпадать с тем, что сейчас лежит на GitHub. # Установщик 3X-UI: хеш в скрипте должен совпадать с тем, что сейчас лежит на GitHub.
xv=$(sed -n 's/^XUI_VERSION="\(.*\)"/\1/p' scripts/3x-ui.sh) xv=$(sed -n 's/^XUI_VERSION="\(.*\)"/\1/p' scripts/3x-ui.sh)
xs=$(sed -n 's/^XUI_INSTALL_SHA256="\(.*\)"/\1/p' scripts/3x-ui.sh) xs=$(sed -n 's/^XUI_INSTALL_SHA256="\(.*\)"/\1/p' scripts/3x-ui.sh)
got=$(curl -fsSL "https://raw.githubusercontent.com/MHSanaei/3x-ui/$xv/install.sh" | sha | awk '{print $1}') got=$(gh_curl "https://raw.githubusercontent.com/MHSanaei/3x-ui/$xv/install.sh" | sha | awk '{print $1}') \
|| { warn "GitHub недоступен — проверку установщика 3X-UI пропускаю (релиз не блокирую)"; got=$xs; }
[[ $got == "$xs" ]] || die "Установщик 3X-UI $xv изменился: $got вместо $xs. Проверьте, что поменялось, прежде чем выпускать." [[ $got == "$xs" ]] || die "Установщик 3X-UI $xv изменился: $got вместо $xs. Проверьте, что поменялось, прежде чем выпускать."
ok "Установщик 3X-UI $xv совпадает с закреплённым SHA256" ok "Установщик 3X-UI $xv совпадает с закреплённым SHA256"
@@ -52,7 +81,8 @@ ok "Установщик 3X-UI $xv совпадает с закреплённы
xc=$(sed -n 's/^XRAY_CORE="\(.*\)"/\1/p' scripts/3x-ui.sh) xc=$(sed -n 's/^XRAY_CORE="\(.*\)"/\1/p' scripts/3x-ui.sh)
for a in 64 arm64-v8a; do for a in 64 arm64-v8a; do
pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/3x-ui.sh) pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/3x-ui.sh)
off=$(curl -fsSL "https://github.com/XTLS/Xray-core/releases/download/$xc/Xray-linux-$a.zip.dgst" | awk '/^SHA2-256=/ {print $2}') off=$(gh_curl "https://github.com/XTLS/Xray-core/releases/download/$xc/Xray-linux-$a.zip.dgst" | awk '/^SHA2-256=/ {print $2}' || true)
[[ -n $off ]] || { warn "GitHub недоступен — суммы Xray для $a пропускаю"; continue; }
[[ -n $pin && $pin == "$off" ]] || die "Сумма ядра Xray $xc ($a) в 3x-ui.sh не совпадает с официальной: «$pin» вместо «$off»." [[ -n $pin && $pin == "$off" ]] || die "Сумма ядра Xray $xc ($a) в 3x-ui.sh не совпадает с официальной: «$pin» вместо «$off»."
done done
ok "Суммы архивов ядра Xray $xc совпадают с официальными" ok "Суммы архивов ядра Xray $xc совпадают с официальными"
@@ -65,11 +95,13 @@ kx=$(sed -n 's/^XRAY_PIN="\(.*\)"/\1/p' scripts/kit.sh)
[[ $kx == "$xc" ]] || die "XRAY_PIN в kit.sh ($kx) не совпадает с XRAY_CORE в 3x-ui.sh ($xc)." [[ $kx == "$xc" ]] || die "XRAY_PIN в kit.sh ($kx) не совпадает с XRAY_CORE в 3x-ui.sh ($xc)."
for a in amd64 arm64; do for a in amd64 arm64; do
pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/kit.sh) pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/kit.sh)
off=$(curl -fsSL "https://github.com/MHSanaei/3x-ui/releases/download/$kp/x-ui-linux-$a.tar.gz.sha256" | awk 'NR == 1 {print $1}') off=$(gh_curl "https://github.com/MHSanaei/3x-ui/releases/download/$kp/x-ui-linux-$a.tar.gz.sha256" | awk 'NR == 1 {print $1}' || true)
[[ -n $off ]] || { warn "GitHub недоступен — сумму панели для $a пропускаю"; continue; }
[[ -n $pin && $pin == "$off" ]] || die "Сумма архива панели $kp ($a) в kit.sh не совпадает с официальной: «$pin» вместо «$off»." [[ -n $pin && $pin == "$off" ]] || die "Сумма архива панели $kp ($a) в kit.sh не совпадает с официальной: «$pin» вместо «$off»."
done done
shs=$(sed -n 's/^XUI_SH_SHA256=\(.*\)/\1/p' scripts/kit.sh) shs=$(sed -n 's/^XUI_SH_SHA256=\(.*\)/\1/p' scripts/kit.sh)
got=$(curl -fsSL "https://raw.githubusercontent.com/MHSanaei/3x-ui/$kp/x-ui.sh" | sha | awk '{print $1}') got=$(gh_curl "https://raw.githubusercontent.com/MHSanaei/3x-ui/$kp/x-ui.sh" | sha | awk '{print $1}' || true)
[[ -n $got ]] || { warn "GitHub недоступен — проверку x-ui.sh $kp пропускаю"; got=$shs; }
[[ $shs == "$got" ]] || die "Сумма x-ui.sh $kp в kit.sh не совпадает с официальной: «$shs» вместо «$got»." [[ $shs == "$got" ]] || die "Сумма x-ui.sh $kp в kit.sh не совпадает с официальной: «$shs» вместо «$got»."
ok "Панель $kp и ядро $kx в kit.sh совпадают с установщиком и официальными суммами" ok "Панель $kp и ядро $kx в kit.sh совпадают с установщиком и официальными суммами"
@@ -82,7 +114,10 @@ ok "Ключ подписи записан в kit"
{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}" | awk '{ sub(/^\*/, "", $2); print $1" "$2 }'; } >SHA256SUMS { echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}" | awk '{ sub(/^\*/, "", $2); print $1" "$2 }'; } >SHA256SUMS
rm -f SHA256SUMS.sig rm -f SHA256SUMS.sig
ssh-keygen -Y sign -f "$key" -n "$NS" SHA256SUMS >/dev/null # Ключ защищён парольной фразой: её вводит человек. Понятная подсказка на случай опечатки.
echo "Подписываю ключом $key — если он защищён парольной фразой, введите её сейчас."
ssh-keygen -Y sign -f "$key" -n "$NS" SHA256SUMS >/dev/null \
|| die "Ключ не подписал. Чаще всего это неверная парольная фраза («incorrect passphrase») — запустите команду ещё раз и введите её внимательно. Файл SHA256SUMS при этом не портится."
tmp=$(mktemp) tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT trap 'rm -f "$tmp"' EXIT
signers scripts/kit.sh | while read -r k; do printf '%s namespaces="%s" %s\n' "$ID" "$NS" "$k"; done >"$tmp" signers scripts/kit.sh | while read -r k; do printf '%s namespaces="%s" %s\n' "$ID" "$NS" "$k"; done >"$tmp"